🏠 메모리 누수(Memory Leak) 쉽게 이해하기: 🖥 낭비되는 컴퓨터의 기억력 ❌

현대적인 프로그래밍 환경에서 메모리 누수(Memory Leak)는 여전히 애플리케이션의 성능 저하와 충돌을 일으키는 주요 원인 중 하나입니다. 메모리 누수를 한마디로 정의하면, “더 이상 필요하지 않은 데이터가 메모리에서 해제되지 않고 남아 있는 현상”입니다.


1️⃣ 메모리 누수란 무엇인가요? (쉽게 이해하기)

컴퓨터의 메모리(RAM)는 프로그램이 실행되는 동안 데이터를 임시로 저장하는 작업 공간입니다. 이 작업 공간은 한정되어 있습니다.

  • 정상적인 과정: 프로그램이 데이터를 사용하기 위해 메모리를 할당받고, 사용이 끝나면 메모리를 해제하여 다른 프로그램이 사용할 수 있도록 돌려줍니다.
  • 메모리 누수: 마치 식당에서 손님(데이터)이 식사를 마치고 퇴장했지만, 종업원(프로그램)이 테이블을 치우지 않아 다음 손님(다른 데이터)이 자리에 앉을 수 없게 되는 상황과 같습니다.
    • 메모리 누수가 발생하면 프로그램이 실행될수록 사용 가능한 메모리가 점점 줄어듭니다.
    • 결국, 컴퓨터는 느려지거나, 메모리 부족(Out of Memory) 오류와 함께 프로그램 또는 전체 시스템이 충돌하게 됩니다.

📝 딥하게: 메모리 누수는 개발자가 명시적으로 메모리를 해제해야 하는 언어(C, C++)에서 흔했지만, 가비지 컬렉터(Garbage Collector, GC)가 있는 언어(Java, JavaScript, Python 등)에서도 GC가 “이 데이터는 여전히 필요하다”고 오해하게 만들 때 발생합니다.


2️⃣ 💡 가비지 컬렉터와 메모리 누수

JavaScript 같은 고급 언어에서는 개발자가 메모리 해제에 대해 신경 쓸 필요가 없도록 가비지 컬렉터(GC)라는 자동 메모리 관리 도구가 작동합니다.

  • GC의 역할: GC는 주기적으로 메모리를 검사하여 “도달 불가능한 객체(unreachable objects)” (즉, 프로그램 코드 어디에서도 참조할 수 없어 더 이상 사용될 가능성이 없는 객체)를 찾아 메모리에서 자동으로 제거합니다.
  • 누수의 원인: 메모리 누수는 GC가 객체를 해제하지 못할 때 발생합니다. 이는 프로그램의 어딘가에서 해당 객체를 실수로 계속 참조(reference)하고 있기 때문입니다. GC는 그 객체가 “여전히 도달 가능하다”고 판단하여 삭제하지 않습니다.

3️⃣ 🖥 JavaScript에서 흔한 메모리 누수 예시 3가지

JavaScript 엔진(예: V8 엔진)은 Mark-and-Sweep 등의 알고리즘을 사용하여 GC를 수행합니다. 다음은 이 GC를 방해하는 대표적인 패턴입니다.

1. 전역 변수 (Global Variables)

의도치 않게 변수를 전역 스코프에 선언하거나, varlet/const 없이 변수를 사용할 때 발생합니다. 전역 객체(브라우저에서는 window 객체)에 한 번 등록된 변수는 프로그램이 끝날 때까지 GC의 대상이 되지 않으므로, 큰 객체를 여기에 저장하면 누수가 발생합니다.

JavaScript

function createGlobalLeak() {
    // let, const, var 키워드 없이 선언하면 전역 객체(window)의 속성이 됩니다.
    // 이는 의도치 않게 전역 객체를 오염시키고 메모리에서 해제되지 않게 합니다.
    leakyData = new Array(1000000).fill('leak'); 
}
// leakyData는 함수가 종료된 후에도 window.leakyData로 계속 참조됩니다.

2. 이벤트 리스너 (Event Listeners)

DOM 요소에 이벤트 리스너를 추가했지만, 나중에 그 DOM 요소 자체를 제거할 때 리스너를 해제하지 않아 발생합니다.

  • 문제: DOM 요소는 제거되었지만, 이벤트 핸들러(콜백 함수)는 여전히 DOM 요소를 참조하고 있습니다. 또한, 이 핸들러 자체도 window 객체와 같은 상위 객체에 의해 참조될 수 있습니다.
  • 해결: 요소를 제거하기 전에 반드시 removeEventListener를 사용하여 리스너를 명시적으로 제거해야 합니다.

JavaScript

const button = document.getElementById('myButton');
const dataStore = {}; // 큰 데이터가 있다고 가정

button.addEventListener('click', function onClick() {
    // 이 콜백 함수는 dataStore 객체를 참조하고 있습니다.
    console.log(dataStore.value); 
});

// 만약 나중에 button 요소를 DOM에서 제거해도, onClick 함수와 
// 그 함수가 참조하는 dataStore는 여전히 메모리에 남아 누수가 발생합니다.
// ❌ 해결책: button.removeEventListener('click', onClick);

3. 타이머 및 인터벌 (Timers and Intervals)

setInterval 또는 setTimeout이 설정되었지만, 나중에 명시적으로 해제되지 않은 경우에 발생합니다.

  • 문제: 콜백 함수가 실행을 기다리는 동안, 해당 함수가 참조하는 모든 외부 변수(클로저)는 GC 대상에서 제외됩니다. 인터벌이 무한정 반복되면 누수가 계속됩니다.
  • 해결: 작업이 끝나면 반드시 clearInterval이나 clearTimeout을 사용해야 합니다.

JavaScript

let count = 0;
// 큰 객체를 참조하는 클로저를 포함한 인터벌
const bigData = new Array(10000).fill('heavy'); 

const intervalId = setInterval(() => {
    count++;
    // 이 함수는 bigData를 참조하고 있어, bigData는 GC 대상이 될 수 없습니다.
    console.log(count, bigData.length);
}, 1000);

// 이 코드가 실행되지 않으면, bigData는 메모리에서 해제되지 않습니다.
// ❌ 해결책: clearInterval(intervalId);

4️⃣ 메모리 누수 방지 팁 및 결론

메모리 누수를 방지하는 가장 좋은 방법은 “필요 없는 참조는 즉시 끊어주는 것”입니다.

  1. 변수 스코프 관리: 항상 let 또는 const를 사용하고, 전역 변수 사용을 최소화합니다.
  2. 이벤트/타이머 정리: 이벤트 리스너, setInterval, setTimeout을 설정했다면, 해당 작업이 끝날 때 removeEventListener 또는 clearInterval/clearTimeout을 통해 반드시 해제합니다.
  3. WeakMap/WeakSet 사용: 객체 참조가 메모리 누수를 일으킬 때, WeakMap이나 WeakSet을 사용하면 키가 GC 대상이 될 수 있습니다. 이는 “약한 참조”를 생성하여 GC가 객체를 제거하는 것을 방해하지 않습니다.

메모리 누수는 당장 눈에 띄지 않지만, 장시간 실행되는 서비스나 트래픽이 많은 웹 애플리케이션에서는 치명적인 성능 문제를 일으킵니다. 주기적인 프로파일링 툴(예: Chrome 개발자 도구의 Performance, Memory 탭)을 사용하여 애플리케이션의 메모리 사용량을 모니터링하는 것이 중요합니다.