안녕하세요, 볼드나인 백엔드 개발팀 잭슨 입니다.
이번 글에서는 node.js 개발자들에게 날개를 달아 줄 수 있는 크롬 개발자 도구 활용법에 대해서 소개하겠습니다.
node.js는 기본적으로 GC(Garbage Collector)가 메모리 관리를 해줍니다.
GC가 지속적으로 사용하지 않는 객체를 찾아 메모리에서 제거해주죠.
하지만 전역변수이거나 코드 실행이 끝났지만 계속 참조되고 있는 객체가 있는 경우 처럼 실제로는 필요없는 객체이지만 GC의 메모리 해제 대상이 아니라서 계속해서 메모리를 차지하고 남아있는 경우가 발생합니다.
이런 현상이 프로그램 실행중 계속해서 쌓인다면 메모리가 가득차서 결국 프로그램은 강제로 종료되고 말겠죠.
계속해서 프로그램이 종료된다면 이제 어디서 메모리가 해제되지 않고 계속 쌓이는지 찾아야 합니다.
의심이 가는 코드를 열심히 들여다 보며 어디서 메모리 누수가 되는지 찾아야 겠죠. 이럴때 크롬 개발자 도구를 이용하면 메모리 관련 수치를 직접 눈으로 보면서 확인할 수 있습니다.
그리고 애초에 개발단계에서 메모리 누수가 발생할것으로 예상되는 기능을 개발한다면 개발단계에서 미리 확인할수도 있습니다.
그럼 크롬 개발자 도구에 많은 기능이 있지만 그중 실행중 메모리 확인을 위한 기능을 살펴보겠습니다.
크롬 개발자 도구를 사용할 수 있도록 서버 실행하기
먼저 메모리 증가를 확인할 수 있는 간단한 서버 코드를 작성합니다.
/app-memory 경로로 접속할때 마다 “X” 라는 문자를 10MB 만큼 메모리에 추가하는 코드입니다.
그리고 /clear-memory로 할당된 메모리를 해제합니다.
// server.js
import express from "express";
const app = express();
const port = 3000;
const bigArray = [];
function addStringToMemory(sizeInMB) {
// 입력받은 크기(MB)만큼 문자열 생성
return "X".repeat(sizeInMB * 1024 * 1024);
}
app.get("/add-memory", (req, res) => {
const largeString = addStringToMemory(10);
// 10MB 만큼 문자열 추가
bigArray.push(largeString);
res.send(`메모리 증가! Total: ${bigArray.length * 10}MB`);
});
app.get("/clear-memory", (req, res) => {
bigArray.length = 0;
res.send("메모리 해제!");
});
app.listen(port, () => {
console.log(`🚀 서버 실행 중: http://localhost:${port}`);
});
JavaScript
복사
그리고 서버를 실행하는데 이때 —inspect 옵션을 사용합니다.
이 옵션을 사용하면 크롬 개발자 도구로 실행중인 코드를 연결합니다.
node --inspect server.js
JavaScript
복사
서버를 실행 하였다면 터미널에 Debugger가 작동하고 있다는 로그를 확인할 수 있고 관련 문서 링크도 확인할 수 있습니다.
이제 개발자 도구로 접속하기 위해서 아래 주소를 크롬 브라우저에 입력해서 접속합니다.
chrome://inspect
접속에 성공했다면 아래와 같은 화면을 볼 수 있습니다.
만약 서버가 실행중이지 않다면 Remote Target 에는 아무내용도 나오지 않습니다.
여기서 inspect를 눌러주면 디버깅을 할 수 있는 개발자 도구가 열립니다.
아래와 같이 생겼습니다.
여기서 우리는 Memory 탭으로 가서 메모리 확인을 위한 기능들을 사용할겁니다.
Memory 탭으로 가면 몇가지 디버깅 기능들이 있는데 Heap snapshot을 이용해서 메모리를 확인해보겠습니다.
아래쪽에 Select JavaScript VM instance를 보면 현재 실행중인 node 프로세스를 확인할 수 있는데 표시되어있는 용량이 node.js에서 객체나 배열같은 데이터가 저장되는 heap 메모리의 용량 입니다.
현재는 7.8 MB가 할당되어 있습니다.
이제 /add-memory 경로로 요청하여 10MB 크기의 문자열을 전역 변수로 선언되어있는 배열에 추가하여 메모리를 증가시켜야 하는데 그 전에 증가된 후 메모리와 비교할 수 있게 현재 힙메모리의 크기를 저장합시다.
아래 이미지의 아래쪽에 표시된 Take snapshot을 눌러서 현재 node 프로세스가 사용중인 힙메모리를 그대로 저장할 수 있습니다.
그러면 왼쪽 Heap snapshots 목록에 현재 메모리 정보가 스냅샷으로 저장되어 있음을 확인할 수 있습니다.
저장된 스냅샷을 눌러서 보면 현재 node 프로세스가 사용중인 힙메모리에 할당된 다양한 정보들을 확인할 수 있습니다.
여기서 표시되는 정보에 대해서 간단히 알아보고 넘어가겠습니다.
•
Distance:
객체가 루트객체에서 얼마나 떨어져 있는지를 나타냅니다. 루트객체란 쉽게 생각하면 전역변수라고 생각하면 됩니다. 즉 전역변수에서 얼마나 떨어져있는지를 나타내고 이 값이 작을수록 루트객체에 가깝습니다. 루트 객체에 가깝다는 뜻은 GC에 의해서 제거될 가능성이 더 낮다는 것을 의미합니다. 아래 코드를 보면 이해하기 쉽습니다.
// Distance = 1 (전역 변수)
const obj = { name: "Alice" };
function foo() {
// Distance = 2 (함수 내부). 지역변수 이므로 함수가 종료되면 GC에 의해서 메모리 할당 해제됨
const temp = { value: 42 };
}
JavaScript
복사
•
Shallow Size: 객체 자체가 차지하는 크기입니다. 객체 내부에서 참조하는 다른 객체의 크기는 포함되지 않습니다. 아래 객체에서 id와 name 처럼 직접 값을 가지고 있는 데이터의 크기만 포함하고 data 처럼 참조하고 있는 다른 객체의 크기를 제외된 크기 입니다.
// shallow size: 16bytes
const obj = {
id: 1, // 8 bytes
name: "John", // 8 bytes
data: ["wallet", "backpack", "cellphone", ...], // 1000 bytes 라고 가정
};
JavaScript
복사
•
Retained Size: 객체 내부에서 참조하고 있는 객체의 크기까지 포함한 크기입니다. 위의 예시에서 obj의 Retained Sizes 1016 bytes가 됩니다.
이제 메모리 사용량을 증가 시키기 위해서 Source 탭으로 이동한다음 왼쪽의 Sciript 에서 소스코드를 선택한 다음 메모리 사용량을 증가시키는 곳에 중단점을 찍습니다.
이제 새로운 크롬탭을 하나 열고 add-memory 경로로 요청을 보냅니다.
그러면 전역 변수로 선언된 배열에 10MB 크기의 문자열이 추가되기 전에 실행이 멈춥니다. 오른쪽 공간에서 현재 변수에 할당된 데이터를 확인할 수 있습니다.
10MB크기의 문자열이 생성되어서 largeString 변수에 할당되어있고 bigArray 배열에 추가되기 전입니다.
현재 지역변수인 largeString에 이미 10MB 크기의 문자열이 할당되었기 때문에 사용중인 메모리는 10MB가 늘어났을 것입니다.
이제 코드 실행을 마무리 하고 늘어난 메모리를 확인해 봅시다.
오른쪽 상단에 보면 중단된 코드 실행을 재개해주는 버튼이 있습니다.
눌러서 실행을 끝내도록 합시다.
그리고 다시 Memory 탭으로 돌아와서 현재 실행중인 node 프로세스의 힙메모리 사용량을 보면 처음 7.5MB 에서 19.6MB로 증가한 것을 알 수 있습니다.
정확히 10MB아니지만 근사한 크기만큼 증가하였습니다.
이제 처음 했던것과 같이 스냅샷을 찍어 확인해봅시다.
그러면 코드 실행전에 찍었던 스냅샷 보다 약 10MB 만큼 증가한 것을 알 수 있습니다.
스냅샷을 눌러서 조금 더 자세한 정보를 봅시다.
자세한 메모리 사용량을 볼 수 있는데 여기서 앞서 말씀 드렸던 Shallow Size와 Retained Size를 정확히 확인할 수 있습니다.
현재 가장 많은 크기을 차지하고 있는 string은 Shallow/Retained Size모두 약 12MB를 차지하고 있고 그 아래 Array를 보면 Shallow Size는 약 200KB, Retained Size는 10MB 를 차지하고 있습니다.
즉, Array 자체의 크기는 200KB로 작지만 참조하고 있는 실제 데이터의 크기는 10MB로 큰것을 알 수 있습니다.
Array가 큰 문자열을 저장하고 있음을 유추할 수 있습니다.
직접 확인해 볼까요?
Array의 정보를 펼쳐서 확인해보니 앞서 코드에서 추가해주었던 10MB 크기의 문자열이 저장되어있음을 확인할 수 있습니다.
/add-memory 라는 요청이 모두 처리되고 끝났는데 메모리에 처리할때 사용한 데이터가 남아있으므로 메모리가 해제되지 않고 그대로 남아있다는 것을 알 수 있습니다.
물론 여기서는 메모리 누수인 상황을 만들기 위해서 일부러 전역변수에 큰 데이터를 저장하였습니다.
실제로는 더 많은 객체와 참조값들이 있겠지만 그렇더라도 중단점을 잘 설정하여 디버깅을 한다면 메모리 누수인 지점을 조금 더 정확하게 찾아낼 수 있을겁니다.
오늘 준비한 주제는 여기까지 입니다.
실제로 해보면 시간이 별로 안걸리는데 단계별로 그림과 설명을 섞어서 작성하니까 글이 꽤 길어진것 같습니다.
여기까지 따라오면서 보셨다면 아시겠지만 제가 설명한 기능과 수치 말고도 화면에 보이는 더많은 정보들이 있습니다.
제가 소개한 방법대로 사용해보면서 추가적인 정보들을 더 활용한다면 node.js 디버깅이 훨씬 더 수월해 질겁니다.
여기까지 제가 준비한 내용을 읽어주셔서 감사합니다.