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 미해제
서블릿 컨테이너는 스레드를 재사용합니다. ThreadLocal을 remove() 하지 않으면 이전 요청 데이터가 남습니다.
// 위험
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 메모리 모니터링 중인가?