TestForge Blog

Spring Boot 메모리 누수 원인과 진단 방법

Spring Boot 애플리케이션에서 흔히 발생하는 메모리 누수 패턴 5가지와 Heap Dump 분석으로 빠르게 잡는 방법.

TestForge Team ·

메모리 누수 증상

  • GC 빈도가 점점 증가
  • Heap 사용량이 GC 후에도 줄지 않음
  • 운영 며칠 후 OutOfMemoryError 발생
java.lang.OutOfMemoryError: Java heap space

원인 1: 정적 컬렉션에 데이터 무한 축적

// 위험 패턴
public class CacheService {
    private static final List<String> cache = new ArrayList<>();

    public void add(String data) {
        cache.add(data);  // 지우는 코드 없음 → 무한 증가
    }
}

해결: Caffeine이나 Guava Cache 같은 만료 정책이 있는 캐시 사용.

@Bean
public Cache<String, String> localCache() {
    return Caffeine.newBuilder()
        .maximumSize(1000)
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .build();
}

원인 2: ThreadLocal 미해제

서블릿 컨테이너는 스레드를 재사용합니다. ThreadLocalremove() 하지 않으면 이전 요청 데이터가 남습니다.

// 위험
public class UserContext {
    private static final ThreadLocal<User> currentUser = new ThreadLocal<>();
    public static void set(User user) { currentUser.set(user); }
    // remove() 없음!
}

// 안전
try {
    UserContext.set(user);
    // ... 비즈니스 로직
} finally {
    UserContext.remove();  // 반드시 해제
}

원인 3: JPA N+1 + 대량 데이터 로딩

// List<Order> 전체를 메모리에 올림
List<Order> orders = orderRepository.findAll();
orders.forEach(o -> process(o.getItems()));  // N+1 쿼리

해결: 페이징 + Fetch Join

@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.status = :status")
Page<Order> findByStatus(@Param("status") String status, Pageable pageable);

원인 4: 이벤트 리스너 미해제

@Component
public class MyListener implements ApplicationListener<MyEvent> {
    // Spring이 관리하므로 괜찮지만,
    // 직접 등록한 리스너는 반드시 해제해야 함
}

원인 5: 외부 라이브러리 리소스 미반환

// 위험: close() 누락
InputStream is = url.openStream();
// ... 사용 후 is.close() 없음

// 안전
try (InputStream is = url.openStream()) {
    // 자동 close
}

Heap Dump로 원인 찾기

# 1. 실행 중인 PID 확인
jps -l

# 2. Heap Dump 생성
jmap -dump:format=b,file=heap.hprof <PID>

# 또는 OOM 시 자동 생성 JVM 옵션
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/app/dumps/

생성된 .hprof 파일을 Eclipse MAT 또는 IntelliJ Profiler로 열면 어떤 객체가 메모리를 잡고 있는지 시각적으로 확인할 수 있습니다.

운영 환경 JVM 권장 옵션

java -jar app.jar \
  -Xms512m -Xmx1g \
  -XX:+UseG1GC \
  -XX:MaxGCPauseMillis=200 \
  -XX:+HeapDumpOnOutOfMemoryError \
  -XX:HeapDumpPath=/var/log/heap/ \
  -Xlog:gc*:file=/var/log/gc.log:time,uptime:filecount=5,filesize=20m

빠른 점검 체크리스트

  • static 컬렉션에 만료 정책 있는가?
  • ThreadLocal 사용 시 finally에서 remove() 하는가?
  • JPA 조회 시 페이징 적용했는가?
  • try-with-resources 사용하는가?
  • Actuator /actuator/metrics 로 JVM 메모리 모니터링 중인가?