ABOUT

성능과 운영 안정성을 함께 끌어올리는 개발자입니다.

92% Positional Error Reduction
79% p95 Latency Improvement
90%+ Long Tasks Reduction

2022.02 · 한국장학재단

우수 멘티

한국장학재단 사회 리더 대학생 멘토링 IT

2022.10 · 동작구청

우수 인재상

동작구청 우수 SW 인재

2025.05 · (주) 그랩

프로그래밍 우수상

(주) 그랩 우수 프로그램 개발

2025.05 · AWSKRUG

AWS한국사용자모임 발표

AI agent 스크립트 튜닝 관련 발표

ComputerScience

Development

Engineering

Trouble Shooting

GUESTBOOK

첫 마음부터
함께 나누는 온기

방명록 작성하러 가기

SUBSCRIBE

최신소식을
편하게 만나보세요.

OOM (Out of Memory)

도입

OOM(Out Of Memory)은 “메모리가 부족하다”가 아니라, 프로세스가 더 이상 메모리를 확보하지 못해 시스템이 기능적으로 멈추는 사건입니다.

백엔드 운영에서 OOM은 단순 예외가 아니라 “프로세스 종료”, “재시작 루프”, “지연 폭발(P99)” 같은 형태로 장애를 만든다는 점에서 치명적입니다. 특히 JVM 기반 서버(Spring Boot 등)는 힙(Heap)과 네이티브 메모리(Direct Buffer, Metaspace, Thread Stack 등)를 함께 쓰기 때문에 “힙은 괜찮아 보이는데 OOM이 난다” 같은 상황도 실제로 자주 발생합니다.

이 글은 OOM을 원인 분류로 정리하고, 운영에서 바로 써먹는 진단 순서재발 방지까지 이어지도록 구성했습니다.

핵심 메시지

“OOM은 보통 메모리 부족이 아니라
메모리를 ‘회수하지 못하는 구조’에서 시작된다.”

- 장애는 ‘양’보다 ‘흐름’에서 터진다 -

정의

운영체제 또는 런타임(JVM 등)이 추가 메모리 할당을 실패하면서 발생하는 상태이며, 결과적으로 프로세스가 비정상 종료하거나 기능이 마비됩니다.

여기서 중요한 건 “메모리가 0이 됐다”가 아니라, 요청이 늘어나며 메모리 사용이 누적되는데 회수가 따라가지 못하는 구조가 생겼다는 점입니다. 즉 OOM은 대부분 “병”이고, “증상”은 GC 과다, 응답 지연, 스레드 고갈, 프로세스 종료 등으로 나타납니다.

종류(현상 기준)

“어떤 영역이 부족했는지”로 분류하면 원인 추적이 쉬워집니다.
구분 대표 메시지 실무 해석
Heap OOM Java heap space 객체가 계속 쌓이는데 GC로 회수되지 않음(캐시/컬렉션/참조 유지)
Metaspace OOM Metaspace 클래스 로딩/언로딩 문제(동적 프록시, 리로드, 클래스 로더 누수)
Direct/Native OOM Direct buffer memory Netty/IO에서 DirectByteBuffer 과다, 네이티브 메모리 누적
OS OOM Killer Killed / OOMKilled 프로세스가 OS/컨테이너 메모리 한도를 넘어 강제 종료됨

💡 TIP / “힙만 보면 안 되는 이유”

JVM은 힙 외에도 Metaspace, Thread Stack, Direct Buffer 등 다양한 영역을 씁니다. 그래서 “힙 사용률은 60%인데 프로세스가 죽는다”면 네이티브 메모리컨테이너 제한을 의심해야 합니다.

 

흔한 원인

대체로 “메모리를 잡아두는 참조” 또는 “회수 속도보다 빠른 생성”에서 발생합니다.

👍 자주 등장하는 원인

  • 캐시(특히 TTL/최대 크기 제한 없는 in-memory cache)
  • List/Map 같은 컬렉션에 데이터가 계속 쌓임(요청/세션/집계)
  • 대용량 로깅/문자열 조합으로 객체 폭증
  • 대용량 파일/응답을 메모리에 통째로 적재
  • 스레드 생성 과다(스택 메모리 누적)

👎 운영에서 치명적인 패턴

  • “예외 상황”에서만 누수가 발생(테스트에서 안 잡힘)
  • 리트라이/큐 적체로 처리 지연 → 데이터가 메모리에 대기
  • GC 튜닝만으로 해결하려고 시도(구조 문제를 숨김)
  • 컨테이너 제한을 모르고 Xmx만 키움 → OOMKilled

진단 순서(운영 트리아지)

먼저 “누가 OOM을 냈는지”를 구분합니다: JVM인가, OS/컨테이너인가.
1
로그에서 OOM 메시지 유형 확인 Java heap space / Metaspace / Direct buffer memory / Killed(OOMKilled)
2
메모리 추세(증가 패턴) 확인 계단형 증가(누수) vs 톱니형(정상 GC) vs 스파이크(버퍼/배치)
3
GC/스레드/버퍼 지표 확인 GC 시간↑, 스레드 수↑, Direct buffer↑가 단서
4
덤프/프로파일로 “잡고 있는 객체” 추적 Heap dump(최후) 또는 sampling profiler로 대상을 좁힘

💡 TIP / OOM 직전의 전조 증상

OOM은 갑자기 터지는 것처럼 보여도 보통 전조가 있습니다: P99 지연 증가, GC 시간 증가, 스레드/큐 대기 증가, RSS 꾸준한 상승. “느려졌는데 원인이 안 보인다”면 메모리 압박을 먼저 의심해보는 게 좋습니다.

 

해결 전략

“메모리를 늘리는 것”보다, 메모리를 잡아두는 구조를 끊는 것이 우선입니다.

✅ 실무에서 바로 적용되는 조치

  • ✔️ 캐시에는 TTL + 최대 크기를 반드시 둔다(무한 캐시 금지)
  • ✔️ 대용량 데이터는 스트리밍 처리로 바꾸고, 메모리 적재를 피한다
  • ✔️ 스레드/큐/리트라이를 제한해 “메모리 대기열”이 쌓이지 않게 한다
  • ✔️ 컨테이너 환경에서는 메모리 제한을 기준으로 Xmx/네이티브 메모리를 설계한다
  • ✔️ 재현되면 Heap dump/프로파일링으로 “누가 잡고 있나”를 확정한다

 

정리

“메모리 크기” 문제가 아니라, 메모리 흐름(생성/보관/회수)운영 한도의 문제입니다.

✅ 핵심 요약

  • ✔️ OOM은 추가 할당이 실패해 서비스가 마비되는 운영 사건입니다.
  • ✔️ “Heap OOM / Metaspace / Direct / OS OOMKilled”처럼 영역으로 원인을 분류해야 합니다.
  • ✔️ 평균 성능보다 P99/GC/대기열이 먼저 망가지는 경우가 많습니다.
  • ✔️ 해결은 튜닝보다 무한 캐시/대기열/대용량 적재 같은 구조를 제거하는 방향이 우선입니다.
728x90