어제 오늘 내일

[Spring Boot] "로그도 없이 서버가 죽었어요..." OOM(Out Of Memory) 분석과 힙 덤프(Heap Dump) 본문

IT/SpringBoot

[Spring Boot] "로그도 없이 서버가 죽었어요..." OOM(Out Of Memory) 분석과 힙 덤프(Heap Dump)

hi.anna 2026. 3. 26. 00:50

 
열심히 만든 서버를 배포하고 퇴근했는데, 새벽에 알람이 울립니다. "서버 다운".
부랴부랴 접속해서 로그를 보지만, 아무것도 없습니다.

java.lang.OutOfMemoryError: Java heap space

이 에러는 JVM의 힙(Heap) 메모리가 꽉 차서 더 이상 객체를 만들 수 없을 때 발생합니다. 문제는 JVM 자체가 멈추기 때문에 로그를 남길 힘조차 없다는 것입니다.
이때 필요한 것이 바로 힙 덤프(Heap Dump)입니다.


1. 힙 덤프(Heap Dump)가 뭔가요?

쉽게 말해 "서버가 죽기 직전의 메모리 스냅샷"입니다.
어떤 객체가 메모리를 몇 GB나 차지하고 있었는지, 누가 그 객체를 참조하고 있었는지를 낱낱이 기록한 파일(hprof)입니다.

이 파일만 있으면 범인을 100% 잡을 수 있습니다.


2. "죽을 때 사진 찍어!" JVM 옵션 설정

서버가 언제 죽을지 알고 24시간 지켜보고 있을까요? 불가능합니다.
그래서 우리는 JVM에게 "너 죽을 때(OOM 발생 시) 자동으로 덤프 파일 남기고 죽어!"라고 명령해야 합니다.

실행 명령어에 옵션 추가 ()

java -jar \
  -XX:+HeapDumpOnOutOfMemoryError \        # 1. OOM 터지면 덤프 떠라!
  -XX:HeapDumpPath=./dumps/oom-dump.hprof \ # 2. 여기다 저장해라!
  my-application.jar

이제 서버가 OOM으로 죽으면 ./dumps 폴더에 oom-dump.hprof라는 파일이 덩그러니 남아있을 겁니다. 이게 바로 결정적인 증거입니다.


3. 범인 잡는 도구: VisualVM (무료)

덤프 파일은 바이너리라서 메모장으로 못 엽니다. 분석 도구가 필요합니다.
가장 접근하기 쉬운 건 JDK에 포함되어 있거나 무료로 받을 수 있는 VisualVM입니다. (IntelliJ Ultimate 버전 사용자라면 내장 프로파일러가 더 좋습니다.)

  1. VisualVM 설치 및 실행 (https://visualvm.github.io/)
  2. File -> Load 메뉴에서 아까 생성된 .hprof 파일 선택
  3. Classes 탭 클릭 -> Size 기준으로 정렬

분석 시나리오

리스트를 쭉 내리는데, 뜬금없이 ArrayListHashMap이 혼자서 메모리의 80% (예: 2GB)를 차지하고 있다면?
범인입니다.


4. 메모리 누수(Memory Leak)의 주범들

도대체 왜 메모리가 꽉 찼을까요? 실무에서 가장 많이 실수하는 패턴 3가지입니다.

① static 변수에 데이터 무한 저장

public class CacheService {
    // static은 GC 대상이 아님! (서버 꺼질 때까지 살아있음)
    private static final List<String> userHistory = new ArrayList<>();

    public void addHistory(String history) {
        userHistory.add(history); // 계속 넣기만 하고 지우질 않음 -> OOM 발생
    }
}

해결책: static 사용을 자제하거나, EhCache, Redis 같은 전문 캐시 솔루션을 사용하세요. (TTL 설정 가능)

② 닫지 않은 리소스 (InputStream, DB Connection)

public void readFile() {
    InputStream is = new FileInputStream("large-file.txt");
    // 다 쓰고 is.close() 안 함 -> 메모리에 계속 남아있음
}

해결책: try-with-resources 구문을 써서 자동으로 닫히게 하세요.

③ 롬복 @ToString의 배신 (무한 참조)

JPA 양방향 연관관계에서 @ToString을 무심코 쓰면, UserTeam을 부르고, TeamUser를 부르며 문자열을 무한 생성하다가 OOM이 터집니다.
해결책: @ToString(exclude = "team")으로 끊어주세요.


5. 예방이 최선이다 (모니터링)

OOM이 터지고 나서 분석하는 건 '소 잃고 외양간 고치기'입니다.
지난 시리즈에서 다뤘던 Prometheus + Grafana를 기억하시나요?

  • JVM Heap Usage 그래프가 계단식으로 계속 올라가기만 하고 떨어지지 않는다면(GC가 안 먹힘), 메모리 누수가 진행 중이라는 신호입니다. 이때 미리 코드를 점검해야 합니다.

마치며

오늘의 결론입니다.

  1. OOM은 로그 없이 죽기 때문에 힙 덤프가 유일한 해결책이다.
  2. -XX:+HeapDumpOnOutOfMemoryError 옵션은 선택이 아닌 필수다. (운영 스크립트에 당장 추가하세요!)
  3. VisualVM으로 덤프 파일을 열면 메모리를 잡아먹는 범인을 바로 찾을 수 있다.

이제 서버가 죽어도 당황하지 말고, 침착하게 덤프 파일을 열어보세요. 그 안에 답이 있습니다.
다음 포스팅에서는 "DB 연결이 안 돼서 서버가 멈췄어요..." DB 커넥션 풀(HikariCP)의 적정 사이즈를 계산하고 데드락을 방지하는 튜닝 방법에 대해 알아보겠습니다.
도움이 되셨다면 좋아요와 댓글 부탁드립니다! 😊

반응형
Comments