Memory Leak
메모리 누수
사용하지 않는 메모리가 해제되지 않는 버그. 시간이 지나면 시스템 느려짐.
메모리 누수
사용하지 않는 메모리가 해제되지 않는 버그. 시간이 지나면 시스템 느려짐.
메모리 누수(Memory Leak)는 프로그램이 동적으로 할당한 메모리를 더 이상 사용하지 않음에도 불구하고 해제하지 않아 발생하는 버그입니다. 이로 인해 프로그램이 실행되는 동안 사용 가능한 메모리가 지속적으로 감소하게 되며, 심한 경우 시스템 전체의 성능 저하나 크래시를 유발할 수 있습니다.
메모리 누수의 주요 발생 원인으로는 해제되지 않은 참조가 있습니다. 객체에 대한 참조가 남아있어 가비지 컬렉터가 회수하지 못하는 경우, 이벤트 리스너 미해제로 DOM 요소가 제거되었으나 이벤트 핸들러가 남아있는 경우, 클로저 내 변수 참조로 외부 스코프의 큰 객체를 불필요하게 참조하는 경우 등이 대표적입니다.
프로그래밍 언어에 따라 메모리 관리 방식이 다릅니다. JavaScript, Java, Python 같은 언어는 가비지 컬렉션(GC)을 통해 자동으로 메모리를 관리하지만, 참조가 남아있으면 GC도 메모리를 회수할 수 없습니다. 반면 C, C++ 같은 언어는 수동 메모리 관리를 요구하므로 malloc/free, new/delete를 명시적으로 호출해야 하며, 잘못된 관리 시 메모리 누수나 dangling pointer 문제가 발생합니다.
메모리 누수를 탐지하기 위한 다양한 도구들이 있습니다. Valgrind는 C/C++ 프로그램의 메모리 오류를 검출하는 강력한 도구이며, Chrome DevTools의 Memory 탭은 JavaScript 힙 스냅샷을 분석하여 누수를 찾을 수 있게 해줍니다. Node.js에서는 --inspect 플래그와 함께 Chrome DevTools를 사용하거나, clinic.js 같은 도구를 활용할 수 있습니다.
JavaScript에서 흔히 발생하는 메모리 누수 패턴과 해결 방법입니다:
// 문제 1: 이벤트 리스너 미해제 class LeakyComponent { constructor() { this.data = new Array(10000).fill('large data'); // 이벤트 리스너 등록 - 해제하지 않으면 누수 발생! window.addEventListener('resize', this.handleResize); } handleResize = () => { console.log('Resized!', this.data.length); } } // 문제 2: 클로저에서의 참조 유지 function createClosure() { const hugeArray = new Array(1000000).fill('data'); // 클로저가 hugeArray를 참조하여 GC 불가 return function() { return hugeArray[0]; }; } // 문제 3: setInterval 미정리 function startPolling() { const cache = []; setInterval(() => { // 계속 데이터가 쌓이며 메모리 증가 cache.push(new Array(1000).fill(Math.random())); }, 1000); }
// 해결 1: 컴포넌트 정리(cleanup) 패턴 class SafeComponent { constructor() { this.data = new Array(10000).fill('large data'); this.handleResize = this.handleResize.bind(this); window.addEventListener('resize', this.handleResize); } handleResize() { console.log('Resized!', this.data.length); } // 반드시 cleanup 메서드를 호출하여 리스너 해제 destroy() { window.removeEventListener('resize', this.handleResize); this.data = null; // 명시적 참조 해제 } } // 해결 2: WeakRef 또는 필요한 데이터만 참조 function createSafeClosure() { const hugeArray = new Array(1000000).fill('data'); const firstItem = hugeArray[0]; // 필요한 값만 저장 return function() { return firstItem; // hugeArray 참조 없음 }; } // 해결 3: interval ID 저장 및 정리 function startSafePolling() { const cache = []; const MAX_CACHE_SIZE = 100; const intervalId = setInterval(() => { if (cache.length >= MAX_CACHE_SIZE) { cache.shift(); // 오래된 데이터 제거 } cache.push(new Array(1000).fill(Math.random())); }, 1000); // cleanup 함수 반환 return () => clearInterval(intervalId); }