OOM은 처음이라

sujin·2025년 5월 4일
0
post-thumbnail

최근 회사에서 운영하는 서비스에서 OOM 문제가 발생했습니다. 처음 겪는 상황이라 원인을 파악하는데 시간이 걸렸고 현재도 문제를 완전히 해결하기 위해 여러 방법을 시도하고 있습니다. 이 경험을 바탕으로 OOM이란 무엇인지, 어떤 원인으로 발생하는지, 그리고 어떻게 해결할 수 있을지에 대해 정리해보려고 합니다.



OOM(Out of Memory) 이란?

OOM 에러는 애플리케이션이 사용할 수 있는 메모리의 한계를 초과하려고 할 때 발생하는 에러입니다. 쉽게 말해, 더 이상 필요하지 않은 값들이 여전히 메모리 공간을 차지하고 있어 새로운 데이터를 저장할 공간이 부족해지는 상황이라고 이해할 수 있습니다.

이러한 문제가 발생하게 되면, 애플리케이션이 강제로 종료되거나, 전체 시스템 성능이 심각하게 저하되는 등의 문제가 발생할 수 있습니다. 특히 서버 환경에서는 OOM이 발생하는 순간 요청을 처리하지 못해 서비스 중단으로 이어질 수 있기 때문에 주의해야합니다.

원시 값과 참조 값의 메모리 관리 방식

자바스크립트에서 값은 크게 원시 값과 참고 값으로 나뉩니다. 이 두 종류의 값은 메모리에 저장되는 방식이 서로 다르며, 이로 인해 메모리 관리에도 차이가 생깁니다.

원시 값문자열,숫자,불리언,null,undefined 와 같은 값들입니다. 이들은 실행 컨텍스트가 생성되는 시점에 스택(stack) 이라는 메모리 공간에 저장되고, 함수가 종료되면 자동으로 메모리에서 제거 됩니다. 이처럼 원시 값은 사용이 끝나면 자연스럽게 정리되기 때문에 별도로 메모리를 수거할 필요가 없습니다.

반면, 참조 값객체,배열,함수 등을 말합니다. 참조 값은 힙(Heap) 이라는 공간에 저장되며, 변수에는 해당 값의 실제 값이 아니라 그 값을 가리키는 메모리 주소가 저장됩니다. 힙은 스택과 달리 다양한 실행 컨텍스트에서 공유되기 때문에 참조 값이 여전히 다른 곳에서 사용되고 있는지 아닌지를 판단해 메모리를 해제 해야합니다. 이러한 역할을 해주는 것이 바로 가비지 컬렉션 입니다.

🤔 실행 컨텍스트 란?

  • 실행 컨텍스트는 자바스크립트 코드가 실행될 때 생성되는 실행 환경을 의미합니다. 즉, 자바스크립트 엔진이 코드를 해석하고 실행하는 동안의 환경을 말하며, 함수 호출이나 코드 블록이 실행될 때 새로운 실행 컨텍스트를 생성합니다.
  • 실행 컨텍스트의 구성 요소는 크게 렉시컬 환경, 변수 객체, this 값이 있습니다.
    • 렉시컬 환경 : 변수나 함수가 어디서 선언 되었는지를 기억하고 저장합니다.
    • 변수 객체 : 변수와 함수 선언이 포함된 객체로 함수 내에서 사용되는 변수들이 저장됩니다.
    • this : 함수가 어디서 실행되었는지에 따라서 동적으로 결정됩니다.

가비지 컬렉션

가비지 컬렉션은 자바스크립트 엔진이 불필요해진 메모리를 자동으로 해제해주는 기능입니다. 자바스크립트는 메모리를 자동으로 관리하기 때문에 가비지 컬렉션은 메모리 누수를 방지하는데 중요한 역할을 합니다.

가비지 컬렉션은 기본적으로 도달할 수 없는 값(unreachable value) 을 찾아서 메모리에서 제거합니다. 예를 들어 어떤 객체를 참조하던 변수가 null로 설정되거나, 함수 실행이 끝나면서 클로저 내부에서 참조하던 객체들이 더 이상 사용되지 않을 경우, 해당 객체는 더 이상 도달할 수 없는 상태가 됩니다.

대표적인 방식은 Mark-and-Sweep 방식 입니다. 이 방식은 먼저 전역 객체나 실행 중인 함수에서 시작해서 연결된 객체들을 모두 찾아 ‘도달 가능하다’고 표시한 뒤, 도달할 수 없는 객체들을 찾아내어 제거합니다. 하지만 모든 상황에서 완벽하게 작동하는 것은 아니며, 잘못된 참조나 불필요한 클로저 사용 등으로 인해 참조가 끊기지 않는 객체가 남아 있는 경우 , 메모리 누수가 발생할 수 있습니다.

OOM이 발생하는 원인

OOM은 여러 가지 이유로 발생할 수 있지만 일반적으로 아래와 같은 원인이 있습니다.

  • 사용하지 않지만 참조가 해제되지 않은 객체 가 계속해서 메모리를 차지하고 있을 때 발생할 수 있습니다. 예를 들어 클로저 내부에서 참조되고 있거나, 전역 객체에 등록된 값이 제거되지 않은 경우 등이 이에 해당합니다.
  • 대용량 데이터를 한꺼번에 메모리에 로드 하는 경우에 문제가 발생할 수 있습니다. 예를 들어 JSON데이터 수만 건을 메모리에 올려놓고 처리하거나, 이미지 파일 수백 개를 동시에 로딩하는 경우 OOM 위험이 높아집니다.
  • 무한 루프나 재귀 호출 이 발생할 경우 메모리 사용량이 급격히 증가합니다.
  • 서버 환경에서는 로그나 응답 데이터를 메모리에 계속 유지 하거나, 캐시 데이터를 수동으로 정리하지 않아 누적될 경우 문제가 발생할 수 있습니다.

Next.js 와 OOM

Next.js는 서버 기능을 포함하고 있기 때문에 Node.js 런타임에서 돌아가는 서버 측 코드가 존재합니다. getServerSideProps, API Routes, Server Actions 와 같은 기능이 그 예입니다. 따라서, Next.js에서도 서버 측에서 OOM이 발생할 수 있으며 서비스 중단으로 이어질 수 있습니다.

저는 최근 회사에서 Next.js 기반의 서비스에서 OOM 문제를 경험하게 되었습니다. 메모리가 초기화되지 않아 메모리 사용량이 점점 증가하다가 결국 한계치를 초과해 버리는 현상이었습니다. 실무에서 OOM 문제를 마주친건 처음이라 당황스럽기도 했지만, 우선 Next.js에서 OOM이 발생할 수 있는 원인 을 찾아봤습니다.

  • 불필요하거나 과도한 데이터 캐싱
    서버에서 응답 결과를 캐시에 저장하되, 이를 해제하지 않으면 메모리가 계속 누적될 수 있습니다.
  • 대용량 데이터의 반복 패칭 또는 유지
    한 번에 많은 양의 데이터를 패칭하거나, 이를 서버 메모리에 오래 유지할 경우 문제가 됩니다.
  • 해제되지 않는 참조
    클로저나 전역 변수 등으로 인해 객체 참조가 끊기지 않으면 가비지 컬렉션이 이를 수거하지 못합니다.
  • 의도치 않은 무한 루프 또는 재귀 호출
    반복되는 로직이 메모리를 점유하면서 해제되지 않아 문제가 될 수 있습니다.

Next.js의 캐시가 문제?

이 중에서도 가장 핵심적인 원인으로 판단한 것은 데이터 패칭 후 캐시에 저장된 데이터가 적절히 초기화되지 않는 문제 였습니다.

실제로 저희 서비스는 대부분의 페이지가 서버 컴포넌트 로 구성되어 있었고, 데이터를 Server Actions를 통해 데이터를 패칭 하고 있었습니다. 커머스 서비스의 특성상 무한스크롤 페이지가 많았고, 이미지나 상품 정보처럼 한 페이지에서 불러오는 데이터 양이 많다 보니 메모리 사용량이 빠르게 증가한 것으로 추측했습니다.

문제를 해결하기 위해 문제 해결을 위해 먼저 일부 페이지의 데이터 패칭 방식을 서버 → 클라이언트 로 전환했습니다. 클라이언트에서 데이터를 가져올 경우 서버 메모리에 직접 데이터를 유지하지 않기 때문에 일시적으로 메모리 사용량을 줄일 수 있었습니다.

💭 캐싱도 살리고 메모리도 지킬 수는 없을까?

클라이언트 패칭으로 변경하면서 들었던 생각은 "Next.js의 장점 중 하나가 데이터 캐싱인데 이 캐싱이 오히려 OOM의 원인이 되어 버린다면 과연 Next.js의 이점을 제대로 활용하고 있는 것일까?" 라는 의문이 들었습니다.
그렇다면, "Next.js의 데이터 캐싱 기능을 유지하면서도 OOM 문제를 해결할 수 있는 방법은 없을까?" 라는 생각이 들었고 아직 실무에 적용해보진 않았지만 다음과 같은 방법이 있다고 생각했습니다.

  • 캐시의 만료 시간 설정
  • 클라이언트 패칭과 서버 패칭 구분
    • 동적인 데이터와 정적인 데이터에 따라 적절한 패칭 방식을 선택한다.

번외) OOM 디버깅 방법

크롬의 개발자 도구를 통해서 메모리 디버깅을 진행할 수 있습니다.

1. Heap snapshot
스크린샷 2025-04-29 오후 10 45 58

  • 현재 페이지의 힙 상태를 기록하고 분석할 수 있으며 성능 개선 전후로 스냅샷을 비교할 수 있습니다.

2. Allocation instrumentation timeline
스크린샷 2025-04-29 오후 10 37 28

  • 메모리 누수가 의심되는 시나리오를 수행하여 메모리 상태를 기록할 수 있다. 기록이 진행하는 동안 타임라인에 메모리 할당과 해제가 그래프로 표시되어 이를 분석하여 메모리 누수를 디버깅할 수 있습니다.

3. Allocation sampling

  • 메모리 할당을 함수 단위로 간단하게 기록할 수 있습니다.

자세한 디버깅 방법은 FEConf 2023 [B2] SSR 환경(Node.js) 메모리 누수 디버깅 가이드 를 참고해주세요!

크롬 개발자 도구의 Memory 탭을 활용해 메모리 누수의 원인을 찾아보려 했지만, 개인적으로는 직관적이지 않고 어려웠다고 느꼈습니다. 관련 블로그나 문서를 참고했지만 어떤 항목을 중점적으로 봐야 하는지 명확하지 않았고 각 지표가 무엇을 의미하는지도 쉽게 와닿지 않았던 것 같습니다. (익숙하지 않아서 더 그랬던 것일 수도...😅)

해당 탭에서 제가 파악할 수 있었던 건, 현재 페이지에서 메모리가 얼마나 사용되고 있는지, 메모리가 적절히 해제되고 있는지 여부 정도였던 것 같습니다. 실제 누수의 원인을 정확히 짚어내기에는 한계가 있었고, 프론트엔드 개발자가 실무에서 활용할 수 있는 메모리 디버깅 툴이나 라이브러리를 더 찾아봐야겠다는 생각이 들었습니다.


📚 참고
Visualizing memory management in V8 Engine
Tìm hiểu về NextJS và build ứng dụng đơn giản kết hợp Firebase
Next.js에서 메모리 누수 디버깅하기

profile
개발댕발

0개의 댓글