메모리에 대해 알아보자 (feat. Heap and Stack)

#JavaScript#CS#메모리2024-03-09

가비지 컬렉션에 대해 알아보는 과정에서, 메모리와 운영체제 등 관련 정보를 보다 깊게 알아보기 위해 이번 글을 작성하게 되었습니다. 이번 글에서는 메모리와 메모리 구조에 대한 전반과 자바스크립트에서의 메모리 관리를 정리하며, 가비지 컬렉션 포스팅과 함께 읽을 수 있습니다.

🤔 메모리란 무엇일까?

메모리란 컴퓨터에서 데이터를 일시적으로 저장하는 공간입니다. 컴퓨터가 작업을 수행할 때 필요한 데이터와 프로그램을 저장하고, CPU가 즉시 접근할 수 있도록 하는 역할을 합니다.

메모리는 주로 다음 두 가지 주요 유형으로 나눌 수 있습니다.

  • RAM (Random Access Memory)

    임의 접근 메모리라 불리며, 데이터와 프로그램이 실행되는 동안 임시적으로 저장됩니다. 전원이 꺼지면 저장된 정보는 사라지며 컴퓨터의 속도와 성능에 직접적인 영향을 미칩니다.

  • ROM (Read-Only Memory)

    읽기 전용 메모리로, 컴퓨터의 기본적인 부트업 과정이나 시스템 운영에 필요한 기본적인 소프트웨어가 저장되어 있습니다. ROM에 저장된 데이터는 전원이 꺼져도 지워지지 않습니다.

이번 글에서는 컴퓨터 작동에 직접적으로 관여하는, 즉 우리가 메모리 구조라고 말했을때 메모리에 해당하는 RAM에 대해 다뤄볼 예정입니다.

일반적으로, 일상 사용자나 대부분의 소프트웨어 개발자들은 ROM을 직접적으로 다루지 않습니다.

ROM의 내용은 대개 제조 과정에서 프로그래밍되며, 이후에는 변경되지 않도록 설계되었습니다. 그러나 특정 상황에서 ROM을 다루는 경우는 있습니다

  1. 펌웨어 업데이트
  2. 임베디드 시스템 개발
  3. 시스템 레벨 프로그래밍

이러한 경우를 제외하고, 대부분의 사용자와 개발자는 ROM에 저장된 데이터나 프로그램을 직접적으로 다루는 일이 없습니다. 대신, ROM의 작동은 대체로 투명하게, 즉 사용자가 명시적으로 인식하지 않는 방식으로 이루어집니다. 펌웨어 업데이트나 임베디드 시스템 개발과 같은 특별한 상황에서만 ROM에 직접 접근하게 됩니다.

🎞️ RAM은 왜 중요할까?

그렇다면 RAM은 왜 중요할까요? RAM이 컴퓨터의 속도와 성능에 직접적인 영향을 미친다고 했는데 왜 그런걸까요?

RAM은 식료품점을 걸을 때 쇼핑 목록을 기억하는 것과 같은, 작업에 필요한 즉각적인 데이터를 저장하는데 도움이 되는 자신의 단기 기억 장치라고 생각할 수 있습니다. 단기 기억력이 좋지 않거나 단기 기억력을 잃게 되면 아무것도 기억할 수 없습니다.

그러나 더 많은 단기 기억력을 가지게 되면, 즉 한번에 많은 쇼핑 목록을 기억할 수 있다면, 한번에 많은 목록에 대해 기억하고, 추가로 여러 작업을 동시에 수행할 수 있습니다.

이와 동일하게, 컴퓨터의 RAM 메모리 용량이 높을수록 CPU가 작업을 더 빨리 완료하고 더 원활하게 사용할 수 있습니다. RAM이 많을수록 일반적으로 주어진 순간에 더 많은 데이터를 관리할 수 있기 때문에, 앱이 더 원활하게 작동하고 더 나은 멀티태스킹이 가능합니다. 또한 메모리를 많이 사용하는 앱과 비디오 게임을 사용할 수 있습니다.

🎨 메모리 구조를 살펴보자

그렇다면 이제 본격적으로 메모리의 구조에 대해 살펴보겠습니다. 메모리 구조를 간단히 그림으로 나타내면 아래와 같습니다.

프로그램이 실행되기 위해서는 우선 프로그램이 메모리에 로드되어야 합니다. 이때 컴퓨터의 운영체제는 프로그램의 실행을 위해 메모리 공간을 할당해주게 됩니다. (이렇게 운영체제는 제한된 리소스를 여러 개의 프로세스들에 효율적이고 공정하게 분배하여 멀티 태스킹이 가능하게 합니다. 자세한 내용은 프로세스와 스레드를 확인하세요)

이때 할당되는 메모리의 공간은 코드, 데이터, 힙, 스택 영역으로 나눠지게 됩니다.

Untitled

code 영역

  • 소스코드가 기계어 형태로 저장됩니다.
  • CPU가 해당 영역에 저장된 명령어를 하나씩 가져가 처리하게 됩니다.
  • 프로그램 시작부터 종료까지 메모리에 남아있습니다.

data 영역

데이터 영역은 프로그램의 전역 변수와 정적(static) 변수가 저장되는 영역입니다.

프로그램의 시작과 함께 할당되며 프로그램이 종료하면 소멸됩니다.

  • 자바스크립트의 경우 프로그램의 실행동안 그 값이 변경되지 않는 전역 변수나 정적 변수가 이 영역에 할당됩니다.

    const PI = 3.14159
    const count = 0
    

Stack 영역

프로그램이 자동으로 사용하는 임시 메모리 영역입니다. 함수 호출 시 생성되는 지역변수와 매개변수가 저장되는 영역이고, 함수 호출이 완료되면 사라집니다.

즉 함수 호출 시 지역 변수와 함수의 반환 주소가 스택에 저장되고 호출이 끝나면 자동으로 메모리가 해제되는데, 스택은 메모리의 할당과 해제가 매우 빠르게 이루어져야 하기 때문입니다.

  • 스택 영역은 JS 엔진 컴파일시 데이터의 크기를 알고 있는 정적 데이터를 저장하는 영역입니다.
    • JS에서는 원시값과 객체 및 함수를 가리키는 참조(reference)가 이에 해당합니다.
  • 스택은 후입선출인 LIFO(Last-In-First-Out) 방식으로 관리되며 호출이 완료되면 소멸합니다.
  • 높은 주소 → 낮은 주소 방향으로 메모리가 저장됩니다.
Untitled

Heap 영역

힙 영역은 개발자가 할당/해제하는 메모리 공간입니다. Java, JavaScript, Python에서는 가비지 컬렉션이 자동으로 해제합니다.

이 공간에 메모리를 할당하는 것을 동적 할당(Dynamic Memory Allocation)이라고도 합니다. 왜냐하면 힙 메모리는 대규모 데이터 혹은 동적으로 크기가 변하는 데이터 구조에 사용되기 때문입니다.

  • 스택과 달리 컴파일 시점에 크기를 알 수 없으므로, JS 엔진은 고정된 크기의 메모리를 할당하지 않고 필요에 따라 더 많은 공간을 할당합니다. →이를 동적 메모리 할당이라고 합니다.
  • 힙은 선입선출인 FIFO(First-In-First-Out)의 방식으로 관리되며 가자 먼저 들어온 데이터가 가장 먼저 인출됩니다.
  • 낮은 주소 → 높은 주소 방향으로 메모리가 저장됩니다.
Untitled

💡 그림으로 보는 메모리 구조

Untitled

💥 스택 오버플로우는 사이트가 아니다

스택 오버플로우라는 사이트를 한번쯤은 들어가 보셨을 것이라 생각합니다. 그럼 여기서 이 스택 오버플로우(stack overflow)는 도대체 무엇일까요?

스택 오버플로우에서의 stack 메모리를 의미합니다. 이때, 오버플로우(overflow)란 사전적의미로 ‘넘치다, 넘쳐흐르다’라는 뜻을 가지고 있습니다. 즉 말 그대로 한정된 메모리 공간에 넘쳐서 생기는**, 메모리 공간이 부족하여 그 안의 데이터가 넘쳐 흐르는 현상**입니다.

메모리 구조에 스택과 힙이 있듯이 스택 오버플로우 외에도 힙 오버플로우도 있습니다. 스택오버플로우와 힙오버플로우는 버퍼 오버플로우의 종류로, 정확한 명칭은 스택 버퍼 오버플로우(stack buffer overflow), 힙 버퍼 오버플로우(heap buffer overflow)입니다.

  • 버퍼 오버플로우란?

    버퍼 오버플로우는 데이터를 저장하는 버퍼의 크기를 초과하여 데이터를 쓸 때 발생하며, 스택, 힙, 데이터 세그먼트 등 프로그램의 어떤 부분에서도 발생할 수 있습니다.

    기술적 배경:

    • 버퍼는 데이터를 일시적으로 저장하는 메모리 영역으로, 고정된 크기를 가집니다.
    • 프로그램이 버퍼의 경계를 넘어서 데이터를 쓰면 버퍼 오버플로우가 발생합니다.

    보안 취약점:

    • 버퍼 오버플로우는 인접 메모리를 덮어쓰면서 프로그램의 다른 부분에 영향을 줄 수 있습니다.
    • 이 오류는 시스템의 보안 메커니즘을 우회하고, 원격 코드 실행이나 시스템 권한 획득 등의 심각한 보안 위협으로 이어질 수 있습니다.

스택 오버플로우

스택 오버플로우는 스택 영역에 할당된 메모리를 초과하여 데이터를 쓸 때 발생하며 주요 원인과 결과는 다음과 같습니다.

원인

  • 과도한 지역 변수 할당

    한 함수 내에서 너무 많은 지역변수를 선언하거나, 변수의 크기가 필요 이상으로 큰 경우

  • 깊은 재귀 호출

    함수가 자신을 반복적으로 호출하고, 적절한 종료 조건이 없이 계속될 경우 각 호출마다 스택에 새로운 프레임이 추가되어 스택의 한계를 초과할 수 있습니다.

결과

  • 메모리 침범

    스택 오버플로우 발생하면, 하나의 스택 프레임이 다른 스택 프레임을 침범할 수 있습니다.

  • 보안 취약점

    스택 오버플로우는 보안 취약점으로 이어질 수 있습니다. 악의적인 공격자는 이 취약점을 이용해 임의의 코드를 실행하거나 시스템을 제어할 수 있습니다. (이는 버퍼 오버플로우를 이용한 공격의 일반적인 형태로, 특히 네트워크 기반 응용 프로그램에서 빈번하게 발견됩니다.)

Untitled

힙 오버플로우

힙 오버플로우는 동적 메모리 할당 영역인 힙에서 발생합니다. 프로그램이 실행 중에 메모리를 필요로 할 때마다 힙에 메모리를 할당하고 해제하는데, 이 과정에서 메모리를 할당한 이상의 데이터를 쓰려고 하면 힙 오버플로우가 발생할 수 있습니다.

  • 부적절한 메모리 할당 크기 계산

프로그래머가 필요한 메모리 크기를 잘못 계산하거나, 메모리 할당 시 요청한 크기가 실제 데이터를 저장하기에 충분하지 않을 때 발생할 수 있습니다. 예를 들어, 객체의 필요 크기보다 작은 메모리를 할당받은 후, 전체 데이터를 저장하려고 하면 인접 메모리 영역으로 데이터가 넘칠 수 있습니다.

  • 잘못된 포인터 사용

프로그램 내에서 포인터가 잘못된 메모리 주소를 가리키거나, 해제된 메모리를 다시 사용하려고 할 때 문제가 발생할 수 있습니다. 이러한 상황은 특히 메모리를 재사용할 때 자주 발생하며, 포인터가 예상치 못한 위치를 가리키게 되어 힙 메모리에 무분별하게 접근할 수 있습니다.

  • 버퍼 오버플로우

힙에 저장된 데이터, 예를 들어 문자열이나 배열 등에 대해, 버퍼의 크기를 초과하는 데이터를 쓰려고 할 때 버퍼 오버플로우가 발생할 수 있습니다. 이는 특히 문자열 복사나 데이터 복사 작업에서 자주 발생합니다.

  • 동적 할당된 객체의 메소드 오용

객체 지향 프로그래밍에서, 객체의 동적 할당 후 메소드를 통해 객체의 상태를 변경할 때, 객체의 크기를 초과하는 작업을 수행하면 힙 오버플로우가 발생할 수 있습니다. 예를 들어, 객체의 한계를 넘어서는 속성이 추가되거나, 배열 같은 멤버 변수가 그 크기를 넘어서 확장될 수 있습니다.

  • 잘못된 예외 처리

프로그램에서 예외 상황을 잘못 처리하여, 예외가 발생했을 때 적절한 메모리 정리가 이루어지지 않으면, 이후 메모리 할당이나 접근에 문제가 생겨 힙 오버플로우로 이어질 수 있습니다.

자바스크립트의 메모리 관리

자바스크립트에서는 변수, 함수, 객체 등을 만들 때 JS엔진은 메모리를 할당하고, 더이상 필요가 없어지면 메모리에서 해제합니다. 이러한 과정을 메모리 생명 주기(Life Cycle)이라고 합니다.

메모리 할당은 메모리의 빈 공간을 예약하는 과정이고, 메모리를 해제하면 다른 목적으로 사용할 수 있는 공간이 확보됩니다. 변수를 할당하거나 함수를 생성할 때마다 메모리에서는 항상 아래와 같은 과정을 거치게 됩니다.

Untitled
  • 메모리 할당

    자바스크립트에서는 C언어와 다르게 자동으로 우리가 생성한 객체(object)에 필요한 메모리를 할당합니다.

    메모리 관리 측면에서의 “객체(Object)”는 JS 객체들만이 아닌, 함수들 또는 함수 스코프들도 포함됩니다.

  • 메모리 사용

    메모리를 사용하는 것은 우리가 작성한 코드에서 하는 것으로 변수에서 읽거나 쓰는 것을 말합니다.

  • 메모리 해제

    메모리 해제 역시 자바스크립트 엔진에 의해 자동으로 처리됩니다. 이때 메모리의 해제를 담당하는 것이 바로 가비지 컬렉션(가비지 컬렉터)입니다. 할당된 메모리가 해제되면 새로운 목적으로 그 공간을 사용할 수 있게됩니다.

🤔 메모리 관리가 제대로 되지 않으면?

“그렇다면 자바스크립트는 메모리 관리가 자동으로 되는 것 같은데, 메모리 관리가 제대로 되지 않는 경우가 존재할까요?”

메모리 할당과 메모리의 해제가 자동으로 이루어지지만 자바스크립트의 경우도 메모리의 관리가 완벽하게 되는 것은 아닙니다. 가바지 컬렉팅 언어에도 메모리 관리가 제대로 이루어지지 않을 수 있고, 이 경우 메모리 누수가 일어나게 됩니다.

메모리 누수(memory leak)현상은 컴퓨터 프로그램이 필요하지 않은 메모리를 계속 점유하고 있는 현상입니다. 할당된 메모리를 사용한 다음 반환하지 않는 것이 누적되면 메모리가 낭비됩니다. 즉, 더이상 불필요한 메모리가 해제되지 않으면서 메모리 할당을 잘못 관리할 때 발생합니다.

자바스크립트에서 메모리 누수가 일어나는 원인

  • 의도치 않은 전역 변수

    선언하지 않은 변수에 대해 자바스크립트는 글로벌 객체 내부에 새로운 변수를 생성합니다. (브라우저의 경우 글로벌 객체는 window 입니다.)

    아래 코드에서 bar에 대한 선언을 하지 않을 경우 전역객체에 참조하여 선언합니다.

    function foo(arg) {
      bar = "this is a hidden global variable"
    }
    
    function foo(arg) {
      window.bar = "this is an explicit global variable"
    }
    

    또, 다른 비슷한 실수로는 this의 사용입니다.

    function foo() {
      this.variable = "potential accidental global"
    }
    
    // Foo가 호출되면, this는 글로벌 객체인 윈도우를 가리키게 된다.
    foo()
    

    예상치 못한 전역 변수에 대해서만 이야기 하긴 했지만, 사실 많은 코드에 명시적인 전역 변수가 흩어져 있는 것이 사실입니다. ( null 처리 혹은 재할당이 되지 않는 경우)

    특히 전역변수를 방대한 양의 정보를 임시로 저장하고 처리하는데 사용한다면 모든 작업이 끝난 이후 null 처리 혹은 재할당을 해주는 것이 필요합니다.

    전역 변수와 관련하여 메모리 소비량이 증가하는 일반적인 원인은 주로 캐시입니다. 반복적으로 사용되는 저장데이터는 캐시로 처리하는데, 이것이 효율적으로 작동하기 위해서는 캐시크기에 대해 상한선이 있어야 합니다.

    한도가 없는 캐시는 메모리 해제가 되지 않으므로 메모리 소비 크기를 늘리는 원인이 됩니다.

  • 잊혀진 타이머 또는 콜백

    setInterval 은 자바스크립트에서 종종 사용되며, 각 호출 사이에 고정된 시간 지연으로 함수를 반복적으로 호출하거나 코드 스니펫을 실행합니다.

    자세히 설명하기 전 라이브러리에 대해 잠깐 언급하자면, 많은 라이브러리들은 자체적으로 메모리 관리 기능을 갖추고 있어, 사용하지 않는 인스턴스나 데이터가 메모리에서 자동으로 해제되도록 설계되어 있습니다. 즉, 라이브러리들은 보통 가비지 컬렉션에 좋은 관행을 따르도록 구현되어 있습니다.

    많은 라이브러리가 이벤트를 발생할 때 실행될 콜백 함수를 등록하게 하거나 객체의 변화를 관찰할 수 있도록 옵저버 패턴을 사용하는데, 이러한 콜백함수나 옵저버가 더이상 필요하지 않을 경우 이를 자동으로 해제할 수 있는 기능을 제공합니다.

    하지만 setInterval 같은 경우 아래와 같은 코드를 자주 볼 수 있습니다.

    var someResource = getData()
    
    setInterval(function () {
      var node = document.getElementById("Node")
      if (node) {
        // Do stuff with node and someResource.
        node.innerHTML = JSON.stringify(someResource)
      }
    }, 1000)
    

    위 예제는 타이머에서 더 이상 필요하지 않은 node 혹은 데이터를 참조하는 타이머의 예시입니다.

    node로 선언된 객체의 경우 제거될 수 있는 가능성이 있기 때문에 인터벌 핸들러 내부의 전체 블록을 불필요하게 만들 수 있습니다. 하지만 핸들러 즉, 인터벌은 여전히 활성화되어 있기 때문에 메모리를 해제할 수 없습니다.

    인터벌이 해제될 수 없다면 그 dependency 역시 해제가 될 수 없는데, 이는 someResource 를 인터벌 내부에서 의존하여 참조를 하고 있기 때문에 someResource 역시 해제될 수 없다는 뜻입니다.

    setInterval 외에도 DOM 이벤트 처리에서 널리 사용되는 Observer Pattern 역시도 메모리 누수의 위험성이 있었습니다. 옵저버패턴은 객체의 상태변화를 관찰하고 이를 구독하는 다른 객체들에게 알리는 디자인 패턴입니다. 이 패턴은 이벤트 리스너와 유사하게 작동하며 자바스크립트에서는 DOM 이벤트 처리나 데이터 바인딩 라이브러리에서 널리 사용됩니다.

    Observer는 어떤 객체를 참조할 때, 그 객체가 더 이상 필요하지 않다 하더라도 참조가 유지되어 메모리 누수를 발생시킬 수 있습니다. 특히 과거의 브라우저(IE)의 경우 DOM 노드와 자바스크립트 코스 다이의 순환 참조를 감지할 수 없었습니다.

    하지만 오늘날 모던 브라우저의 경우는 이러한 순환참조를 올바르게 감지하고 처리할 수 있는 현대적인 가비지 컬렉팅 알고리즘을 사용합니다. 즉 removeEventListener를 호출하여 이벤트 리스너를 제거할 필요가 없습니다. (그러나 실제 개발 환경에서는 여전히 명시적으로 리스너를 제거하는 것이 좋은 프랙티스로 간주됩니다.)

  • DOM 외부에서의 참조

    간혹 DOM 노드를 자료구조 안에 저장하는 것이 유용할 경우가 있습니다. 테이블의 여러 행을 빠르게 업데이트하고 싶을 경우, DOM을 딕셔너리나 배열에 저장해서 참조하여 사용하는 방법이 있을 수 있습니다.

    이렇게 될 경우, DOM 트리와 딕셔너리 안에 같은 DOM 참조가 두 벌로 존재하게 됩니다. 만약 나중에 이 두 행을 모두 제거해야할 경우, 두 참조 모두 제거해야 합니다.

    var elements = {
      button: document.getElementById("button"),
      image: document.getElementById("image"),
      text: document.getElementById("text"),
    }
    
    function doStuff() {
      image.src = "http://some.url/image"
      button.click()
      console.log(text.innerHTML)
    }
    
    function removeButton() {
      document.body.removeChild(document.getElementById("button"))
    
      // 이 시점에서도 여전히 elements에서 button의 참조를 가지고 있다.
      // 이 경우 button element는 여전히 메모리에 있으며, GC에 의해 해제 될 수 없다.
    }
    

    위 코드를 분석해보면,

    1. elements 객체를 생성

      여기서 elements 객체는 페이지 내의 세 DOM 요소 (button, image, text)에 대한 참조를 저장합니다.

    2. doStuff 함수

      이 함수는 elements 객체에 저장된 DOM 요소들을 사용하여 일련의 동작을 수행합니다. 함수 내에서는 elements 객체의 프로퍼티를 직접 참조하고 있지 않으므로, 이 함수 자체는 메모리 누수와는 관련이 없습니다.

    3. removeButton 함수

      이 함수는 button 요소를 DOM 트리에서 제거합니다. 그러나 elements 객체 내에는 여전히 button 요소에 대한 참조가 남아있습니다.

    removeButton 함수는 DOM에서 button 요소를 제거합니다. 이것은 브라우저의 렌더링 트리에서 button 요소를 제거해 해당 요소가 화면에 더 이상 표시되지 않도록 합니다.

    그러나 elements 객체는 여전히 이 button 요소를 참조하고 있으므로, 가비지 컬렉터는 button 요소가 여전히 "사용 중"으로 간주하고 메모리에서 해제하지 않습니다. 이것이 메모리 누수가 발생하는 과정입니다.

    따라서 아래와 같이 button 요소에 대한 모든 참조를 제거해야 합니다

    function removeButton() {
      document.body.removeChild(document.getElementById("button"))
      elements.button = null // button에 대한 참조를 제거
    }
    
  • 클로저

    자바스크립트에서 중요한 부분을 차지하는 것 중 하나가 클로저입니다. 클로저는 주변 상태에 대한 참조와 함께 묶인 함수의 조합입니다. 클로저는 내부 함수에서 외부 함수의 범위에 대한 접근을 제공하며 자바스크립트에서 클로저는 함수가 생성될 때마다 생성됩니다.

    또 다른 말로 설명하자면 함수가 속한 렉시컬 스코프(Lexical Environment)를 기억하여, 함수가 렉시컬 스코프 밖에서 실행될 때도 이 스코프에 접근할 수 있게 해주는 기능입니다.

    var theThing = null
    
    var replaceThing = function () {
      var originalThing = theThing
      // 상위 스코프인 originalThing을 참조하는 스코프를 갖게됨
      // 동시에 theThing 도 참조하게됨.
      var unused = function () {
        if (originalThing) console.log("hi")
      }
    
      theThing = {
        longStr: new Array(1000000).join("*"),
        someMethod: function () {
          console.log(someMessage)
        },
      }
    }
    
    setInterval(replaceThing, 1000)
    

    위 코드를 보면 replaceThing 이 호출될 때마다, 큰 사이즈 배열 longStr 과 someMethod 클로저를 생성합니다. 동시에 unused 변수는 originalThing 을 참조하는 클로저를 가지게 됩니다.

    중요한 것은 unused 와 같은 내부함수에서는 자신을 둘러싼 부모함수의 스코프를 공유하게 된다는 것입니다. (scope-chaining)

    만약 unused 내부 함수가 없다면 replaceThing은 매번 실행 될 때 마다 길이가 큰 문자열(theThing)을 생성하지만, 최신 자바스크립트 엔진 (v8과 같은) 에서는 이전에 호출된 originalThing이 사용 되지 않았음을 파악하고, 이전 값을 해제하여 메모리 사용량을 유지 시키게 됩니다.

    하지만 위 코드에서는 unused 내부 함수로 인해서 계속해서 originalThing 을 참조하게 되고 unused 가 사용되지 않더라도 이 코드가 실행될 때마다 메모리 사용량이 꾸준히 증가하게 됩니다. 따라서 GC가 작동하더라도 메모리 사용량은 크게 줄어들지 않게 됩니다.

    본질적으로 클로저의 참조 목록이 생성되면 (theThing으로 부터 생겨난 root), 이 클로저 내부에는 큰사이즈 배열에 대한 간접적인 참조도 동반하게 되므로 메모리 누수가 발생합니다.