Nodejs의 메모리 누수를 이해하기 위해서는, nodejs의 메모리 관리에 대해서 이해하고 있어야 한다. nodejs는 자바스크립트의 V8엔진을 사용하고 있다. 이에 대해 이해하기 위해서는, 아래 포스트를 먼저 참고할 필요가 있다.
메모리는 크게 스택과 힙메모리로 구별할 수 있다.
V8은 가비지 콜렉션을 이용해서 힙 메모리를 관리한다. 간단히 얘기해, 스택에서 더이상 참조하지 않는 객체의 메모리를 해제하여 다른 객체가 메모리를 할당하여 쓸 수 있도록 한다. V8의 가비지 컬렉터는 더이상 사용하지 않는 메모리를 해제하여 공간을 확보하는 책임이 있다. V8 가비지 컬렉터는 객체를 생성시점으로 묶어서 각각 다른 스테이지별로 별도로 관리한다. V8 가비지 컬렉터는 2개의 다른 스테이지와 세개의 다른 알고리즘을 사용한다.
간단히 말해 메모리 누수랑, 애플리케이션에서 더이상 사용하지 않는 메모리가 힙에서 계속 남아 있고, 그래서 이를 가비지 컬렉터가 OS로 메모리로 반환하지 못하는 상황을 의미한다. 이는 메모리에서 쓸모없는 블록으로 존재하게 된다. 이러한 블록이 계속해서 생기게 되면 애플리케이션에서는 더이상 사용할 메모리가 존재하지 않게 되고, 나아가 OS 또한 할당할 메모리가 남아나지 않아서 애플리케이션이 느려지고 크래쉬되거나, 혹은 OS 단에서 문제가 발생할 수 있다.
V8의 가비지 콜렉터와 같은 자동 메모리 관리는 메모리 누수를 피하는데 초점이 맞춰져있다. 예를 들어 순환 참조는 가비지 콜렉터의 고려대상이 아니지만, 힙의 원치 않는 참조로 인해 문제가 발생할 수 있다. 일반적인 메모리 누수의 상황은 아래와 같다.
window
, global
) 애플리케이션의 생명주기 동안 절대로 가비지 콜렉팅이 되지 않아 계속해서 메모리를 점유하고 있게 된다. 따라서 글로벌 변수를 참조 하고 있는 객체 또한 가비지 콜렉팅의 대상이 되지 않는다는 것을 의미한다. 루트로부터 커다란 객체 참조 그래프를 가지고 있다는 것은 결국 메모리 누수로 이어지게 된다.setTimeout
setInterval
Observer
이벤트 리스너 등의 콜백이 적절한 조치 없이 무거운 객체의 참조를 가지고 있을 경우 메모리 누수가 발생할 수 있다.전역 변수는 절대로 가비지 컬렉팅 되지 않으므로, 전역변수를 남용하지 않는 것이 제일 좋다.
만약 undeclare한 변수를 선언하게 되면, 자동으로 자바스크립트는 이를 호이스팅해서 전역 변수로 만들어 버린다. 이는 곧 메모리 누수로 이어지게 된다.
function hello() {
// 전역변수로 호이스팅 된다.
foo = 'Message'
}
function hello() {
// 여기서 this는 global 이기 때문에 마찬가지로 호이스팅되어 전역변수가 된다.
this.foo = 'Message'
}
이러한 원치 안흔ㄴ 사고를 방지 하기 위해서는, 자바스크립트 파일 상단에 'use strict';
를 선언해 두면된다. 엄격한 모드에서는, 위의 코드는 에러를 발생시킨다. 만약 ES 모듈이나 타입스크립트 또는 바벨과 같은 프랜스파일러를 사용한다면, 굳이 하지 않아도 된다. 최근 버전의 Nodejs에서는, --use_strict
옵션으로 nodejs 환경 전역에 이 모드를 활성화 시킬 수 있다.
'use strict'
// This will not be hoisted as global variable
function hello() {
foo = 'Message' // will throw runtime error
}
// This will not become global variable as global functions
// have their own `this` in strict mode
function hello() {
this.foo = 'Message'
}
화살표 함수를 사용하면, 마찬가지로 전역변수를 생성할수도 있다는 사실을 조심해야 한다. 이러한 경우에는 엄격모드로는 해결할 수가 없고, eslint의 no-invalid-this
로 해결하면 된다.
// 전역변수로 할당된다.
const hello = () => {
this.foo = 'Message";
}
마지막으로, bind
와 call
을 사용하는 함수에 전역 this
를 바인딩하지 않도록 주의한다.
글로벌 스코프의 사용은 가능한 줄이는 것이 좋다.
null
을 넣어주면 된다.스택 접근은 힙 접근 보다 성능적으로도 우월하고, 메모리의 효율성도 높기 때문에 가능한 스택 변수를 많이 활용해야 한다. 이는 또한 실수로 일어나는 메모리 누수도 방지해준다. 물론, 실무상으로 오로지 스태틱 데이터만 쓸 수 있는 일은 없다. 실제 에플리케이션은, 다양한 객체와 다이나믹 데이터를 사용해야 한다. 하지만 몇가지 트릭을 사용하여 스택을 조금 더 효율적으로 쓸 수 있다.
function outer() {
const obj = {
foo: 1,
bar: "hello",
};
const closure = () {
// 구조분해 할당을 써서 필요한 foo만 꺼내왔다.
const { foo } = obj;
myFunc(foo);
}
}
function myFunc(foo) {}
실제 애플리케이션에서 힙메모리의 사용을 피할수는 없지만, 아래 팁들을 이용하면 좀더 효율적으로 사용할 수 있다.
Object.assign
으로 복사하는 것이 좋다.앞서 언급했던 것처럼 클로져, 타이버, 그리고 이벤트 핸들러는 메모리 누수가 일어날 수 있는 영역이다. 아래 코드를 살펴보자.longStr
은 절대 가비지 콜렉팅이 되지 않고, 또한 점점 커지기 때문에 메모리 누수의 원인이 된다.
참고: https://blog.meteor.com/an-interesting-kind-of-javascript-memory-leak-8b47d2e7f156?gi=275d4bdd446b
var theThing = null
var replaceThing = function () {
var 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
함수 끝에서 originalThing
을 null
로 선언해주어야 한다. 이러한 경우도 객체의 복사본을 만들거나, 앞서 언급한 null
을 하는 전략으로 메모리 누수를 피할 수 있다.
이벤트 리스너와 observer도 마찬가지다. 작업이 끝나면 이들을 클리어해주어야 한다. 이들이 영원히 참조하고 있게 해서는 안된다. 특히 부모 스코프의 객체를 참조하고 있다면 더욱 위험하다.
자바스크립트 엔진의 진화와 언어의 성장으로 인하여, 자바스크립트의 메모리 누수는 우리가 생각하는 것 만큼 잦은 이슈는 아니다. 그러나 주의를 기울이지않으면, 성능 문제를 야기하거나 애플리케이션과 OS의 크래쉬를 야기할 수 있다. 메모리 누수가 일어나지 않기 위해 첫번째로 우리가 할일은 V8이 어떻게 메모리를 관리하는 지다. 그 다음에는 무엇이 메모리 누수를 일으키는지 알아야 한다. 이에 대해 이해하고 있고, 그리고 만약 메모리 누수 문제가 발생한다면, 우리는 무엇을 살펴보아야 하는지 알 수 있게 된다. 만약 Nodejs에서 메모리 누수 문제가 발생한다면, 아래 두 개의 링크를 확인해보자.
출처
참고