Skip to main content

SSR 환경(Node.js) 메모리 누수 디버깅 가이드 (1)

About 2 minJavaScriptTypeScriptArticle(s)blogyozm.wishket.comjsjavascripttstypescript

SSR 환경(Node.js) 메모리 누수 디버깅 가이드 (1) 관련

Node.js > Article(s)

Article(s)

SSR 환경(Node.js) 메모리 누수 디버깅 가이드 (1) | 요즘IT
SSR 환경(Node.js) 메모리 누수 디버깅 가이드 (1)

[FEConf2023에서 발표한 SSR 환경(Node.js) 메모리 누수 디버깅 가이드open in new window][1]를 정리한 글입니다. 발표 내용을 2회로 나누어 발행합니다. 1회에서는 메모리 누수에 대해 알아보고, 메모리 누수를 모니터링 도구를 통해 확인해보겠습니다. 2회에서는 메모리 누수 현상을 직접 디버깅해보고 해결하는 방법을 알아봅니다. 본문에 삽입된 이미지의 출처는 모두 이 콘텐츠와 같은 제목의 발표 자료로, 따로 출처를 표기하지 않았습니다. 발표 자료는 FEConf2023 홈페이지open in new window에서 다운로드할 수 있습니다.

안녕하세요. 저는 토스 플레이스의 박지혜라고 합니다. 이번 글에서는 Node.js로 실행한 SSR 환경에서 메모리 누수가 있을 때 디버깅하는 방법에 대해서 소개하겠습니다.

여러분은 “SSR 환경(Node.js) 메모리 누수 디버깅 가이드"라는 제목에서 어떤 키워드가 가장 중요하다고 생각하시나요? 저는 'Node.js'와 '메모리 누수'라는 키워드가 제일 중요하다고 생각합니다. 이 중에서도 메모리 누수에 대해 제 경험을 바탕으로 소개하겠습니다.

어느 날 동료 데브옵스 엔지니어가 저에게 “특정 서비스가 OOM(out of memory)이 발생하는데 확인해 주세요.” 라고 얘기를 했습니다. 이 말을 듣고 저는 간단하게 메모리 누수에 대해 고민하고 해당 부분을 수정하려고 했습니다. 먼저 코드를 열어보니 큰 문제가 없어 보였는데 메모리 누수가 계속 발생했습니다. 그때 저는 조금 더 공부를 해서 디버깅을 통해 이 문제를 해결해야겠다고 마음먹었습니다.

이번 글을 통해 두 가지 내용을 전달하고 싶습니다.

  1. 메모리 누수를 디버깅할 수 있는 자신감
  2. 브라우저의 Memory 탭을 통해 다양한 환경에서 메모리 누수의 범인을 찾는 법

이번 글을 통해 그때 당시의 저와 비슷한 문제를 겪고 있는 분들에게 도움이 되었으면 좋겠습니다.


메모리 누수가 무엇이고, 무엇이 문제인가?

메모리 누수

메모리 누수란 실제로 필요하지 않는 메모리를 계속 차지하고 있는 현상을 뜻합니다. 아래와 같이 엘리베이터에 비유하여 메모리 누수에 대해 알아보겠습니다.

메모리 누수란?
메모리 누수란?

정원이 10명인 엘리베이터가 있고 사람이 4명 타고 있습니다. 이 4명은 내리지 않고 계속 타고 있다고 가정하겠습니다. 그리고 다른 사람이 타기도 하고 내리기도 할 겁니다. 즉, 이 엘리베이터는 사실상 6명만 이용할 수 있는 상황이기 때문에 금방 정원이 초과될 것입니다. 단순하게 표현하면 엘리베이터가 힘들게 되겠죠. 정원도 자주 초과하고 항상 4명이 공간을 차지하고 있기 때문입니다. 즉, 엘리베이터가 효율적으로 운행되지 못하고 있습니다. 이런 상황을 메모리 누수 현상이라고 생각할 수 있습니다.

메모리 누수의 문제점

그럼 엘리베이터가 효율적으로 운행되지 않는다면 무엇이 문제일까요? 자바스크립트는 어플리케이션으로 동작하기 위해 기본적으로 메모리가 필요합니다. 따라서 메모리가 부족해지면 성능이 저하됩니다.

보통 GC라고 부르는 가비지 컬렉터가 메모리 누수를 막기 위해 많은 활동을 하면 CPU 사용량도 필연적으로 늘어납니다. 또한 CPU를 활발하게 사용하는 작업이 많아지면 이벤트 루프가 블로킹됩니다. 이벤트 루프는 자바스크립트 연산에서 매우 중요한 부분을 차지하기 때문에 이 경우 연산이 느려지고 성능이 저하 되는 것입니다. 이로 인해 실행 중이던 서버가 종료되는 문제를 겪을 수도 있습니다.

서버가 비정상적으로 종료되었을 때 다시 실행되도록 해두었을지라도, 이렇게 서버가 죽게 된다면 서버가 종료된 그 순간에는 정상적인 서버의 역할을 하지 못하는 이슈가 발생합니다. 즉, 가용성에 문제가 생깁니다. 간단하게 정리하면 성능이 안 좋아지고, 어플리케이션이 자꾸 죽게 됩니다.

메모리 누수가 있으면 뭐가 문제죠?
메모리 누수가 있으면 뭐가 문제죠?

해결 방법

앞선 엘리베이터 비유를 통해 해결 방법을 알아보겠습니다. 정원이 10명인 엘리베이터에 많은 사람이 타고 있어서 엘리베이터가 힘든 상황이었기 때문에 정원을 늘릴 수 있도록 더 큰 엘리베이터로 바꿔주거나, 자리를 항상 차지하는 4명의 범인을 내보내면 될 것 같습니다.

즉, 힙 메모리를 늘려주거나 메모리 누수의 범인을 디버깅을 통해 찾아 해결할 수 있습니다.

메모리 누수 해결방법
메모리 누수 해결방법

메모리 누수를 확인하는 방법

Node.js 환경에서 메모리 누수를 확인하는 방법은 무엇일까요? 아래와 같이 Node.js를 실행한 터미널에 heap out of memory 라는 문구가 출력되고 이 에러 문구를 통해 확인할 수 있습니다.

메모리 누수가 있는지 어떻게 알 수 있어요?
메모리 누수가 있는지 어떻게 알 수 있어요?

그러나 개발자들이 항상 터미널을 보고 있을 수는 없습니다. 보통 본인이 실행한 서버에 모니터링 도구를 붙여 이 도구를 통해 서버를 관찰합니다. 서버 모니터링의 경우, 모니터링 도구에 그래프로 표현된 CPU 사용률이나 메모리 상태와 같은 지표들을 확인할 수 있습니다. 그러나 클라이언트 환경의 경우 모니터링 툴을 붙이기는 쉽지 않습니다. 사용하는 유저의 브라우저 종류나 하드웨어 성능에 따라 달라질 수 있기 때문입니다. 하지만 디버깅하는 방법 자체는 두 경우 모두 동일하기 때문에 같은 방법으로 설명하겠습니다.


모니터링 도구에서 메모리 누수 확인하기

이번 단락에서는 실제 소스 코드를 통해 모니터링 도구에서 메모리 누수를 확인해 보겠습니다. 아래 코드를 기반으로 메모리 누수를 일부러 발생시키고 이를 디버깅하며 해결해 볼 것이기 때문에 잘 기억해 주셨으면 좋겠습니다.

const server = http.createServer((req,res) => {
  res.writeHead(200, { 'Content-Type': 'text/html' });
  res.write(`
    <!DOCTYPE html>
    <html lang="en">
    <head>
	  <title>Hello World</title>
    </head>
    <body>
	  <h1>Content</h1>
    </body>
    </html>
  `);
  res.end();
});

위 코드는 Node.js로 작성한 간단한 예제 코드입니다. http 요청을 받으면 200이라는 상태 코드와 함께 HTML을 리턴하는 간단한 코드입니다. 이제 이 코드를 활용해 메모리 누수가 있는 코드와 없는 코드를 비교해 보겠습니다.

메모리 누수가 없는 코드
const server = http.createServer((req,res) => {
  if (req.url === '/normal') {
    nonMemoryLeakFunction();
  }
  res.writeHead(200, { 'Content-Type': 'text/html' });
  res.write(`
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <title>Hello World</title>
    </head>
    <body>
      <h1>Content</h1>
    </body>
    </html>
  `);
  res.end();
});

 
 
 














If 조건문을 활용하여 두 가지 코드를 작성했습니다. 표시한 부분 외에는 차이가 없는 코드입니다. 그리고 유저의 요청을 통해 약간의 트래픽을 주는 환경을 아래와 같이 쉘 스크립트로 간단하게 만들었습니다.

#!/bin/bash

# 반복함수 설정
total_requests=30

# 대상 URL
url="http://127.0.0.1:3000/normal"

# 반복해서 요청 보내기
for ((i=1; i<=$total_requests; i++))
do
  curl -s "$url" > /dev/null &
  sleep 1 # 1초 대기
done

wait

echo "모든 요청이 완료되었습니다."

# 스크립트 종료
exit 0

1초에 한 번씩 curl명령어를 실행할 거에요. SSR + 트래픽이 생기는 환경을 작은 규모로 재현했어요.

메모리 누수가 없는 코드에서 호출하는 nonMemoryLeakFunction을 보겠습니다. 함수안에서 listItems 배열을 선언하고, 반복문이 100만 번 반복되면서 배열에 아이템을 넣습니다. 그리고 이 함수가 사용 중인 힙 메모리 용량을 출력하도록 했습니다. 여기서 listItems 배열이 선언된 위치를 주목해 주세요.

메모리 누수가 없는 코드
const server = http.createServer((req,res) => {
  if (req.url === '/normal') {
    nonMemoryLeakFunction();
  }
  res.writeHead(200, { 'Content-Type': 'text/html' });
  res.write(`
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <title>Hello World</title>
    </head>
    <body>
      <h1>Content</h1>
    </body>
    </html>
  `);
  res.end();
});

function nonMemoryLeakFunction() {
  const listItems = [];

  for (let i=0; i<1_000_000; ++i) {
    listItem.push(i);
  }
  console.log(`${process.memoryUsage().heapUsed / 1024 / 1024} MB`);
}


 
















 
 
 
 
 
 
 
 
이 함수를 실행시키면 큰 변화 없이 1초마다 25MB 정도의 메모리 사용량을 나타내고 있습니다.
이 함수를 실행시키면 큰 변화 없이 1초마다 25MB 정도의 메모리 사용량을 나타내고 있습니다.

이러한 숫자들을 모니터링 도구를 활용해 확인하면 아래와 같은 그래프로 표현될 것입니다. 큰 변동 없이 비슷한 수치의 메모리 사용량을 보일 것이고, 중간에 배포를 했다면 잠깐 메모리 사용량이 떨어지기도 할 것입니다. 본인의 서비스에 붙여둔 모니터링 툴이 아래와 같은 그래프를 나타내고 있다면 서비스에 별다른 문제가 없다고 생각해도 될 것입니다.

메모리 누수가 없는 코드를 모니터링하면 이렇게 보여요!
메모리 누수가 없는 코드를 모니터링하면 이렇게 보여요!

이번에는 누수가 있는 코드를 살펴보겠습니다. 앞서 설명드린 listItems 배열의 위치가 함수 밖에 선언되어 있습니다. 즉, 전역변수로 선언되었습니다. 눈치를 채셨겠지만 의도적으로 메모리 누수를 일으키겠다는 의미입니다. 그리고 동일하게 100만 번의 반복문을 실행시키고 함수의 메모리 사용량을 출력하도록 했습니다.

다음 글에서는 앞서 확인한 메모리 누수 현상을 직접 디버깅하고 해결하는 방법을 알아보겠습니다.


이찬희 (MarkiiimarK)
Never Stop Learning.

  1. FEConf2023에서 발표된 'SSR 환경(Node.js) 메모리 누수 디버깅 가이드'/박지혜 토스 플레이스 프론트엔드 엔지니어 ↩︎