angular universal 메모리 누수 개선하기 - 001
SSR 동작 시 발생하는 heap out of memory 수정하기
앵귤러 기반 웹사이트를 개발하면서 발생한 메모리 누수의 다양한 원인들과 그 해결법을 기록합니다.
들어가며
부끄럽지만 앵귤러 기반 웹 프로젝트를 진행하며 메모리 누수같은 문제는 사실 전혀 신경을 안(못)쓰고 있었습니다. 어차피 사용자 브라우저에서 돌아가니깐 새로고침만 하면 해당 페이지의 메모리는 초기화될테니 속도만 느리지 않다면 된다는 생각이었지요. 당연히 이는 크게 잘못된 생각이었고 서버사이드 렌더링(이하 SSR)를 적용하면서 문제는 상당히 심각하게 흘러갑니다. 지금부터 제가 개발하면서 만나본 다양한 메모리 누수(memory leak) 문제를 살펴보고 그 해결법을 기록하고자 합니다.
문제가 뭔가요?
증상
node를 기반으로 돌아가고 있던 앵귤러 서버가 일정시간마다 죽는 문제가 발생하였습니다.
- 로컬 테스트 및 개발 서버에선 발생하지 않고 실제 프로덕션 환경(개발서버 환경은 GAE)에서만 발생하며,
- 트래픽 양과 비례하여 발생주기가 짧아지는 특징이 있었습니다.
다만 이 이상의 특징은 찾을 수 없었으며, 재현도 쉽지 않고 정확한 발생 조건도 파악이 안되는 답도 없는 상황이 되어버렸습니다.
로그.. 로그를 보자!
일단 서버가 죽은 시각의 로그를 까보면 다음과 비슷한 내용의 메세지가 출력됩니다(이미지 출처).
메모리가 부족해서 node 서버가 종료됐다는 메세지인데 프로덕션 환경에는 코드 경량화(minify)가 적용되어 서버가 동작하기 때문에 정확히 코드의 어느 부분이 문제인지는 로그만 가지고는 확인할 수 없었습니다. 그래서 다양한 가설을 세워두고 이것저것 테스트를 해보았습니다.
문제 해결하기
증상 재현하기
메모리를 제한하고 서버를 띄우면 어떻게 될까?
로컬, 개발 서버 환경에서는 해당 문제가 발생하지 않았기에 우선 문제 재현을 위해서 메모리를 제한하여 서버를 띄워 테스트 해보기로 했습니다. node 서버는 node --max-old-space-size=128 dist/APP_NAME/server/main.js
처럼 max-old-space
option을 이용해 메모리 크기를 직접 지정할 수 있습니다. 크기를 64~2048 사이에서 여러번 테스트를 진행해보니 128정도로 설정해놓고 동일한 웹페이지 로드를 여러번 하니 로컬 환경에서도 서버가 죽은 현상이 재현이 됐습니다.
이를 토대로 다음과 같은 단서를 얻었습니다.
- 동일한 페이지를 꽤 긴 텀(2~3초)을 두고 호출만 반복했을 뿐인데 서버가 죽었으니 이건 높은 확률로 메모리 누수 문제일 것이다.
- 메모리 누수가 문제라면 SSR 처리를 하지 않는다면 서버는 죽지 않을 것이다(CSR은 단순히 파일서버의 역할만 하므로).
- 문제가 되는 코드는 해당 페이지 해당 페이지 구현에 필요한 모듈 내부의 코드일 확률이 높을 것이다.
2번 가설을 확인하기 위해서 SSR을 제공하지 않는 버전으로 새로 빌드를 하여 동일한 테스트를 해 본 결과, 서버는 죽지 않고 잘 버티는 모습을 보여주었습니다.
왜 SSR 환경에서만 서버가 죽을까?
SSR은 웹페이지를 서버에서 조립하여 생성된 html 문서를 브라우저에 전송합니다. 서버에서 조립을 한다는 말은 화면 구성에 필요한 각종 컴포넌트, 서비스같은 스크립트 코드가 서버에서 실행된다는 의미입니다. 이 때 브라우저에 전송이 완료된 후에 사용했던(메모리에 올려뒀던) 요소들은 메모리에서 해제를 해줘야 합니다. 대부분의 요소들은 똑똑한 가비지 컬렉터에 의해 정상적으로 메모리에서 해제되지만, 간혹 그러지 못하는 요소들이 있습니다. 메모리에서 해제되지 못하는 원인은 매우 다양하며 구글에 javascript memory leak
키워드를 검색하면 다양한 케이스들을 확인할 수 있습니다.
CSR에서는 왜 서버가 안죽을까?
CSR은 기본적으로 단순한 파일서버와 유사한 형태로 동작합니다. 개발자가 작성한 소스코드를 빌드하여 생성된 html, js, css 등등을 단순한 파일로 취급하여 요청한 브라우저에 전송'만' 해주는 역할을 합니다. 그렇다면 CSR 방식에서는 메모리 누수를 신경쓰지 않아도 되냐고 묻는다면 그건 아니라고 대답할 수 있습니다. CSR 방식은 메모리 누수로 인해 서버가 죽진 않겠지만, 사용자가 브라우저를 새로고침 없이 장시간 사이트를 이용한다면 브라우저가 점점 느려지는 현상을 겪게될 것입니다. 이는 SPA 방식의 웹사이트에서 더 빈번하게 문제를 발생시킬 수 있습니다.
누수 코드 찾기
메모리 누수 문제가 거의 확실해보이니, 이제 문제의 코드를 찾아야 합니다. 코드 여기저기를 뒤지던 중, 갑자기 제가 개인적으로 만들어 공개해놓은 앵귤러 컴포넌트가 생각났습니다.
static?
이 문서에서 말하고자 하는 핵심 문제입니다.
모바일 디바이스에서 당겨서 새로고침을 구현하기 위해서 만들었던 ngx-pull-to-refresh
모듈이 문제였습니다. 이 모듈에선 컴포넌트가 여러번 선언되어 모듈 내부로 넘겨받은 이벤트들을 한꺼번에 관리하기 위해 입력받은 refresh
이벤트를 static
변수에 담아서 관리하고 있었습니다. array 타입의 변수를 static으로 선언하여 이벤트를 담아 관리하는 패턴이 좋은지 여부는 둘째치고(사실 좋은 패턴은 아닌거 같아요..), 추가한 이벤트를 component가 Destroy될 때 제거해주는 코드가 없는 상태였습니다. 해당 소스코드를 보면, static으로 선언되어 있다지만 어쨌든 컴포넌트 내부에 선언되어 있는 변수였기에 컴포넌트가 소멸하면 같이 소멸할 것으로 기대했지만 이는 잘못된 생각이었습니다. static에 관한 정보는 여기 저기서 확인하실 수 있으며, 문제가 된 코드를 어떻게 수정하였는지는 이 commit의 변경내역을 보시면 확인하실 수 있습니다.
재도전
문제의 코드를 수정했으니 다시 테스트를 해보았습니다만..
여전히 동일한 문제가 계속 발생하고 있었습니다. 다만, 서버가 죽는 주기가 좀 더 길어졌습니다. 위에서 수정한 static 변수 관련 문제도 문제였지만 메모리 누수의 원인이 하나가 아니었단 뜻이겠죠. 나머지 원인도 찾아내서 제거해야 합니다..
결론
- SSR 기능을 제공하는 angular(그 외 다른 node 기반 서버) 서버 구동 시 메모리 누수는 치명적인 문제를 일으킬 수 있습니다.
- static 변수를 사용할 땐 상당히 주의를 하며 사용해야 합니다. 자칫하면 메모리 누수의 원인이 될 수 있습니다.