[Javascript] 메모리 누수

[원본 링크]

Javascript는 GC가 달려있는 언어지만, 좀 어지러운 사용 형태를 갖고 있어서 누수가 많이 발생하는 편이다.
장기실행형 프로그램이 아니다보니 티가 잘 안 날 뿐이다.

물론 언어 자체의 문제는 아니고, 사용 형태에 따른 문제긴 하다. GC 자체는 제대로 달려있다.
GC 내부 구조에 대한 내용은 별도 포스트를 참조한다.
https://blog.naver.com/sssang97/223584806455




메모리 누수의 원인

Javascript는 다른 언어에 비해서 누수가 발생할 수 있는 코드 패턴을 자주 사용하는 편이다.
그 중 첫번째가 전역변수 기반의 값 관리고, 두번째가 남발되는 클로저와 이벤트다.



A. 전역변수

일단 뭐 이런 식으로 전역변수를 마구 뿌리는 경우도 적지 않게 있다.

window.foo = 'bar' // 전역변수
const boom = 1; // 이것도 함수 밖에 있다면 전역변수

전역변수는 당연히 프로그램이 죽을때까지 GC 대상으로 잡히지 않기 때문에, 생각없이 할당하다가는 메모리가 줄줄 새는 문제가 발생할 수 있다.

또, Javascript의 느슨한 문법 탓에 전역변수가 만들어지는 경우도 있다.
JS는 정의한적 없는 변수에 값을 할당하려 시도하면 그걸 그냥 전역변수로 만든다.

foo = 'nar'


B. 클로저와 캡쳐

두번째는 클로저다.
그런데 알다시피, GC에 의한 메모리 해제는 "참조하는 대상이 없는" 객체의 경우에 대해서만 이루어진다.

다음과 같은 코드의 경우에는

    function fn1() {
        let largeObj = new Array(100000) // 언제 해제될까?

        // Do Something
        return true;
    }

largeObj가 저 함수가 거의 종료되자마자 쓰레기로 탐지되고 정리될 것이라 가정할 수 있다.
largeObj를 다른 곳에 참조로 넘기지만 않는다면 말이다.

하지만 다음과 같은 코드 패턴에서는 해제될 도리가 없다.

    function fn1() {
        let largeObj = new Array(100000) // 언제 해제될까?

        setInterval(() => {
            let myObj = largeObj
            // do something
        }, 1000)
    }

setInterval에 클로저를 넘겨서 1초마다 실행되게 했다.
이러면 클로저가 largeObj의 참조를 캡쳐해서 가져가는데, 클로저 객체가 생존한 채로 계속해서 실행되니 저 largeObj는 절대로 해제되지 않는다.

그래서 setInterval를 계속해서 돌릴 필요가 없다면 clearInterval 등으로 처리가 끝나면 종료되게끔 하는 것이 꽤 중요하다.

이건 비단 setInterval에만 해당되는 문제는 아니다. 클로저를 넘기고, 클로저에서 외부 변수를 캡쳐해서 쓴다면 그 지점을 유의하면서 작성해야 한다.
클로저에서의 캡쳐는 함수에 직접 파라미터로 넘기는 것에 비해 암시적으로 처리되고, 가볍게 넘어가는 경우가 많아서 누수 유발 가능성이 더 높다.



C. console.log

디버거나 프로파일러나 뭐 이것저것 있어도 디버깅의 기본은 항상 로깅이고, JS 환경에서도 당연히 로그를 찍으면서 쓴다.
그런데, console.log는 잠재적으로 메모리 누수의 가능성이 존재한다.

다음과 같은 함수가 있다면

    function foo() {
        let largeObj = new Array(100000) // 언제 해제될까?

        // Do Something

        console.log(largeObj); // 로깅

        return true;
    }

우리는 foo 함수가 종료되면 largeObj도 정리될 것이라 기대할 것이다.
그런데 실상은 그렇지 않다.

놀랍게도 console.log는 콘솔에 로그만 찍고 끝나는게 아니라, 참조를 들고서 버리지 않을 수 있다.
개발자 도구에 지속적으로 객체의 정보를 보여줄 필요가 있기 때문이다.
console.log로 한번이라도 찍은 객체는 영원히 지워지지 않는 우주 쓰레기가 될 가능성이 다분하다.

그래서 가급적 console.log는 디버깅 환경에서만 사용하는 것이 바람직하고, 프로덕션 환경에서는 사용을 최소화해야 한다.




메모리 프로파일링

다행스럽게도 대부분의 브라우저들은 개발자도구를 통해 메모리 프로파일링 기능을 제공한다.

예를 들어, Chrome와 Node.js의 경우에는 아래 방법을 사용할 수 있다.

메모리 탭으로 이동해서 "타임라인" 어쩌고를 시작한다.


그러면 시작을 누른 시점부터 종료할때까지의 메모리 할당 정보를 기록해준다.
저기서 좌측 상단의 중지 버튼을 누르면 기록을 중단한다.


그럼 프로파일러가 도는동안 할당됐던 값들을 쭉 볼 수 있다.

할당 수집을 켜놓은 상태에서 의심되는 기능을 하나씩 돌려보고, 수집을 끄고 비교해보면 노가다를 하면 어지간한건 아마 찾을 수 있을 것이다.

이걸로 잘 찾으려면 경험적인 노하우가 많이 필요할 것이다.





메모리 누수 탐지 도구: memlab

memlab은 페북에서 만든 메모리 누수 탐지용 보조도구다.
puppeteer 기반이고, 브라우저 환경에서의 누수를 감지하기 위한 목적으로 만들어졌다.

근데 이게 코드에서 누수지점을 딱 잡아주는 그런건 아니고, 그냥 메모리 할당 프로파일러인데 조금 더 보기 좋게 모아서 뿌려주고 스냅샷 좀 편하게 관리해주는 그런 정도다.

npm으로 설치할 수 있다.

사용법은 그렇게 어렵진 않다.

이런 지도 페이지에 대해서 프로파일러를 돌려보자.


// initial page load url: Google Maps
function url() {
    return 'https://www.google.com/maps/@37.386427,-122.0428214,11z';
}

// action where we want to detect memory leaks: click the Hotels button
async function action(page) {
    // puppeteer page API
    await page.click('button[aria-label="Hotels"]');
}

// action where we want to go back to the step before: click clear search
async function back(page) {
    // puppeteer page API
    await page.click('[aria-label="Close"]');
}

module.exports = { action, back, url };

이런 형태로 검사 스크립트를 규격화된 형태로 작성해야 한다.
url이 대상 페이지 url. action은 메모리 패턴을 감지할 액션, back은 종료 액션이다.

저렇게 하면 memlab은 퍼펫티어 켜서 action으로 버튼 누르고, back으로 행동을 종료하면서 그 시점에 할당되고 해제되는 메모리들을 추적한다.

실행은 이렇게 할 수 있다.

테스트가 완료되어도 결과를 바로 보여주진 않고


결과물 모아두는 경로가 따로 있다.

시나리오 파일 골라서 view-heap을 때리면

메모리 프로파일링 결과를 볼 수 있다.

방향키 위아래로 item 고르면서 그것과 연관된 메모리 할당 정보를 함께 볼 수 있고, 1,2,3,4,5,6 버튼을 통해 섹션을 전환할 수 있다.
앞서 설명했듯 누수 지점을 찾아주는게 아니라 찾을 수 있게 모아주는 거라서, 눈이 빠지도록 봐야 한다.

자세한 내용은 문서를 참조하길 바란다.
https://facebook.github.io/memlab/docs/intro




기타

구글도 leak-finder-for-javascript란걸 만들었는데, 현재는 deprecated된것같다.
https://code.google.com/archive/p/leak-finder-for-javascript/



참조
https://owenjeon.medium.com/javascript-memory-leak-dom-tree-76e855d281c8
https://github.com/facebook/memlab
https://facebook.github.io/memlab/docs/api/interfaces/core_src.IHeapSnapshot/
https://ui.toast.com/posts/ko_20210611
https://stackoverflow.com/questions/12996129/memory-leak-when-logging-complex-objects