클로저는 자바스크립트 고유개념이 아닌 여러 함수형 프로그래밍 언어에서 등장하는 보편적인 특성이다.
클로저는 어떤 함수 A 에서 선언한 변수 a 를 참조하는 내부 함수 B 를 외부로 전달할 경우 A의 실행 컨텍스트가 종료된 이후에도 a변수가 사라지지 않는 현상을 말한다.
A의 실행 컨텍스트가 종료되었음에도 불구하고 A의 렉시컬 환경(Lexical Environment) 를 참조하는 것이다.
const outer = function () {
let a = 1;
const inner = function() {
return ++a;
}
return inner;
}
const outer2 = outer(); // inner 함수의 실행 값을 리턴한 것이 아닌 inner 자체를 리턴했다.
outer2(); // 2
outer2(); // 3
inner 함수의 실행 시점에는 outer 함수의 실행 컨텍스트는 종료된 상태인데 어떻게 outer 함수의 렉시컬 환경에 접근할 수 있는 것일까? 바로 가비지 컬렉터의 동작 방식 때문이다. 외부 함수인 outer가 종료 되었더라도 inner 는 outer2에 의해 언젠가 실행될 가능성이 열려있어 수집 대상에서 제외 된다.
즉, Lexical Environment 가 가비지 컬렉터의 수거 대상에서 제외되는 경우는 지역 변수를 참조하는 내부 함수가 외부로 전달된 경우가 유일하다. “외부 함수의 렉시컬 환경이 가비지 컬렉팅 되지 않는 현상”
더 이상 참조되지 않음에도 불구하고 가비지 컬렉터의 수거대상에서 제외되는 것이다.
개발자의 의도와 달리 어떤 값의 참조 카운트가 0이 되지않아 GC의 수거 대상이 되지 않는 경우 메모리 누수라 할 수 있지만 개발자가 의도적으로 참조 카운트가 0 이 되지 않게 설계했다면 메모리 누수라 할 수 없다.
기본적으로 자바스크립트는 직접적으로 메모리 관리를 할 수 없다. 하지만 메모리 누수가 일어나지 않게 하려면 참조 카운트를 0 으로 만들면 된다.
참조 카운트를 0으로 만드는 방법은 식별자에 null 또는 undefined값을 할당하면 된다.
setInterval, return 으로 메모리 해제하는 것을 코드로 확인해보겠다.
const outer = (function(){
let a = 1;
let inner = function() {
return ++a;
};
return inner;
})();
console.log(outer());
console.log(outer());
outer = null; // 해제
(function () {
let a = 0;
let intervalId = null;
let inner = function() {
if(++a >= 10){
clearInterval(intervalId);
inner = null; // 해제
}
console.log(a);
}
intervalId = setInterval(inner, 1000);
})();
개발자가 의도하지 않은 곳에서 클로저가 일어나 메모리 누수가 발생한 경우에는 좋지 않은 상황이다. 하지만 개발자가 의도적으로 클로저를 만들고 활요한다면 메모리 누수라고 할 수 없어서 무조건적으로 좋지 않다고 할 수 없다.
메모리 누수는 클로저에서만 일어나는 것인가?