도입
백엔드 운영에서 OOM은 단순 예외가 아니라 “프로세스 종료”, “재시작 루프”, “지연 폭발(P99)” 같은 형태로 장애를 만든다는 점에서 치명적입니다. 특히 JVM 기반 서버(Spring Boot 등)는 힙(Heap)과 네이티브 메모리(Direct Buffer, Metaspace, Thread Stack 등)를 함께 쓰기 때문에 “힙은 괜찮아 보이는데 OOM이 난다” 같은 상황도 실제로 자주 발생합니다.
이 글은 OOM을 원인 분류로 정리하고, 운영에서 바로 써먹는 진단 순서와 재발 방지까지 이어지도록 구성했습니다.
“OOM은 보통 메모리 부족이 아니라
메모리를 ‘회수하지 못하는 구조’에서 시작된다.”
정의
여기서 중요한 건 “메모리가 0이 됐다”가 아니라, 요청이 늘어나며 메모리 사용이 누적되는데 회수가 따라가지 못하는 구조가 생겼다는 점입니다. 즉 OOM은 대부분 “병”이고, “증상”은 GC 과다, 응답 지연, 스레드 고갈, 프로세스 종료 등으로 나타납니다.
종류(현상 기준)
💡 TIP / “힙만 보면 안 되는 이유”
JVM은 힙 외에도 Metaspace, Thread Stack, Direct Buffer 등 다양한 영역을 씁니다. 그래서 “힙 사용률은 60%인데 프로세스가 죽는다”면 네이티브 메모리나 컨테이너 제한을 의심해야 합니다.
흔한 원인
👍 자주 등장하는 원인
- 캐시(특히 TTL/최대 크기 제한 없는 in-memory cache)
- List/Map 같은 컬렉션에 데이터가 계속 쌓임(요청/세션/집계)
- 대용량 로깅/문자열 조합으로 객체 폭증
- 대용량 파일/응답을 메모리에 통째로 적재
- 스레드 생성 과다(스택 메모리 누적)
👎 운영에서 치명적인 패턴
- “예외 상황”에서만 누수가 발생(테스트에서 안 잡힘)
- 리트라이/큐 적체로 처리 지연 → 데이터가 메모리에 대기
- GC 튜닝만으로 해결하려고 시도(구조 문제를 숨김)
- 컨테이너 제한을 모르고 Xmx만 키움 → OOMKilled
진단 순서(운영 트리아지)
💡 TIP / OOM 직전의 전조 증상
OOM은 갑자기 터지는 것처럼 보여도 보통 전조가 있습니다: P99 지연 증가, GC 시간 증가, 스레드/큐 대기 증가, RSS 꾸준한 상승. “느려졌는데 원인이 안 보인다”면 메모리 압박을 먼저 의심해보는 게 좋습니다.
해결 전략
✅ 실무에서 바로 적용되는 조치
- ✔️ 캐시에는 TTL + 최대 크기를 반드시 둔다(무한 캐시 금지)
- ✔️ 대용량 데이터는 스트리밍 처리로 바꾸고, 메모리 적재를 피한다
- ✔️ 스레드/큐/리트라이를 제한해 “메모리 대기열”이 쌓이지 않게 한다
- ✔️ 컨테이너 환경에서는 메모리 제한을 기준으로 Xmx/네이티브 메모리를 설계한다
- ✔️ 재현되면 Heap dump/프로파일링으로 “누가 잡고 있나”를 확정한다
정리
✅ 핵심 요약
- ✔️ OOM은 추가 할당이 실패해 서비스가 마비되는 운영 사건입니다.
- ✔️ “Heap OOM / Metaspace / Direct / OS OOMKilled”처럼 영역으로 원인을 분류해야 합니다.
- ✔️ 평균 성능보다 P99/GC/대기열이 먼저 망가지는 경우가 많습니다.
- ✔️ 해결은 튜닝보다 무한 캐시/대기열/대용량 적재 같은 구조를 제거하는 방향이 우선입니다.