👨💻 실무 자바 개발자라면 반드시 알아야 할 가비지 컬렉션 핵심 개념부터 Spring Boot 애플리케이션 성능 최적화까지!
메모리 누수와 성능 저하 문제를 해결하는 GC 튜닝 완벽 가이드
JVM 메모리 구조 이해하기
Java 애플리케이션의 성능 최적화와 GC 튜닝을 제대로 하려면 JVM의 메모리 구조부터 명확히 이해해야 합니다. JVM 메모리는 크게 힙(Heap) 영역과 논-힙(Non-Heap) 영역으로 나뉩니다.
힙(Heap) 영역
힙 영역은 객체 인스턴스와 배열이 저장되는 공간으로, GC의 주요 대상이 됩니다. 힙 영역은 다시 다음과 같이 세분화됩니다:
- Young Generation (젊은 세대)
- Eden 영역: 새로 생성된 객체가 처음 할당되는 공간
- Survivor 영역 (S0, S1): Minor GC에서 살아남은 객체가 이동하는 공간
- Old Generation (오래된 세대)
- Young Generation에서 일정 횟수 이상 GC 후에도 살아남은 객체들이 이동
논-힙(Non-Heap) 영역
- 메서드 영역(Method Area): 클래스 구조, 메서드 데이터, 상수, 정적 변수 등이 저장
- JVM 스택(Stack): 메서드 호출과 지역 변수가 저장
- PC 레지스터: 현재 실행 중인 JVM 명령의 주소를 저장
- 네이티브 메서드 스택: 네이티브 코드를 위한 스택 영역
가비지 컬렉션이란?
가비지 컬렉션(Garbage Collection, GC)은 더 이상 사용되지 않는 메모리를 자동으로 식별하고 해제하는 JVM의 핵심 기능입니다. 자바 개발자는 C/C++과 달리 명시적으로 메모리를 해제할 필요가 없어 개발 생산성이 크게 향상됩니다.
GC의 기본 원리
GC는 '도달 가능성(Reachability)'이라는 개념을 기반으로 작동합니다. 다음 조건 중 하나라도 해당되면 객체는 '도달 가능(Reachable)'으로 간주됩니다:
- GC 루트(Root)에서 참조 중인 객체
- 스택의 로컬 변수
- 활성 자바 스레드
- 정적 변수
- JNI 참조
- 도달 가능한 객체에 의해 참조되는 객체
도달할 수 없는(Unreachable) 객체는 '가비지'로 간주되어 GC의 대상이 됩니다.
public void createGarbage() {
// 객체 생성
Object obj = new Object();
// obj 변수를 null로 설정 - 이제 해당 객체는 도달 불가능
obj = null;
// 이 시점에서 원래 obj가 참조하던 객체는 가비지가 됨
// 다음 GC 사이클에서 수집될 예정
}
GC 알고리즘 종류와 작동 원리
JVM에는 여러 GC 알고리즘이 있으며, 각각 특징과 적합한 사용 사례가 다릅니다.
1. Serial GC
가장 단순한 GC로, 단일 스레드로 동작하며 Stop-the-World(STW) 현상이 발생합니다.
- 적합한 환경: 단일 프로세서 환경, 메모리가 제한적인 환경
- JVM 옵션:
-XX:+UseSerialGC
2. Parallel GC (Throughput Collector)
Java 8의 기본 GC로, 여러 스레드를 사용하여 병렬로 GC를 수행합니다.
- 적합한 환경: 멀티 프로세서 또는 멀티 코어 시스템, 배치 처리 같은 처리량 중심 애플리케이션
- JVM 옵션:
-XX:+UseParallelGC
3. CMS GC (Concurrent Mark Sweep)
애플리케이션 스레드와 동시에 실행되어 STW 시간을 최소화합니다.
- 적합한 환경: 응답 시간이 중요한 웹 애플리케이션
- JVM 옵션:
-XX:+UseConcMarkSweepGC
- 주의사항: Java 9부터 deprecated, Java 14에서 제거됨
4. G1 GC (Garbage First)
Java 9부터의 기본 GC로, 힙을 균등한 크기의 영역(region)으로 나누어 관리합니다.
- 적합한 환경: 8GB 이상의 대용량 힙, 예측 가능한 일시 정지 시간이 필요한 애플리케이션
- JVM 옵션:
-XX:+UseG1GC
5. ZGC (Z Garbage Collector)
Java 15부터 정식 버전으로 포함된 저지연 GC입니다.
- 적합한 환경: 초대형 힙(수 TB)과 매우 짧은 일시 정지 시간(10ms 미만)이 필요한 환경
- JVM 옵션:
-XX:+UseZGC
GC 작동 과정 상세 분석
Minor GC (Young Generation GC)
Eden 영역이 가득 차면 발생하며 다음과 같은 과정으로 진행됩니다:
- 새로운 객체는 주로 Eden 영역에 할당됩니다.
- Eden 영역이 가득 차면 Minor GC가 트리거됩니다.
- 살아남은 객체는 Survivor 영역(S0 또는 S1) 중 하나로 이동합니다.
- 이후 Minor GC에서는 Eden과 현재 사용 중인 Survivor 영역을 검사합니다.
- 살아남은 객체는 다른 Survivor 영역으로 이동하고, 객체의 나이(age)가 증가합니다.
- 특정 임계값(기본값 15) 이상 살아남은 객체는 Old Generation으로 승격(Promotion)됩니다.
Major GC (Full GC)
Old Generation이 가득 차면 발생하는 GC로, 일반적으로 Minor GC보다 시간이 훨씬 오래 걸립니다. Full GC 동안에는 대부분의 JVM GC에서 모든 애플리케이션 스레드가 중지됩니다(STW).
다음은 CMS GC의 단계를 예로 들어보겠습니다:
- Initial Mark: GC 루트에서 직접 참조하는 객체를 마킹 (STW 발생)
- Concurrent Mark: 애플리케이션 스레드와 동시에 마킹된 객체에서 참조하는 다른 객체를 추적
- Remark: 동시 마킹 단계에서 변경된 참조를 확인 (짧은 STW 발생)
- Concurrent Sweep: 애플리케이션 스레드와 동시에 가비지 객체를 제거
GC 튜닝이 필요한 상황들
다음과 같은 상황에서는 GC 튜닝을 고려해야 합니다:
- 높은 CPU 사용률: GC가 과도한 CPU 자원을 소모할 때
- 긴 GC 일시 중지 시간: 응답 시간에 민감한 애플리케이션에서 긴 STW 시간이 발생할 때
- Out of Memory 오류: 메모리 누수나 부적절한 메모리 할당으로 인한 OOM 오류 발생 시
- 처리량 저하: GC로 인해 전체 애플리케이션 처리량이 감소할 때
실무에서 자주 만나는 GC 관련 증상
[예시 로그]
2025-05-02T14:30:25.121+0900: [GC (Allocation Failure) [PSYoungGen: 76800K->7168K(89600K)] 253888K->190848K(294400K), 0.0397461 secs] [Times: user=0.11 sys=0.01, real=0.04 secs]
2025-05-02T14:30:25.581+0900: [Full GC (Ergonomics) [PSYoungGen: 7168K->0K(89600K)] [ParOldGen: 183680K->176539K(204800K)] 190848K->176539K(294400K), [Metaspace: 33291K->33291K(1079296K)], 0.5481907 secs] [Times: user=1.94 sys=0.02, real=0.55 secs]
위 로그에서 알 수 있는 주요 정보:
- Minor GC는 0.04초 소요되었지만 Full GC는 0.55초로 약 14배 더 오래 걸림
- Old Generation의 대부분(약 86%)이 사용 중으로, 메모리 누수 가능성이 있음
- 최적화가 필요한 구간: Full GC 빈도 줄이기, Old Generation 크기 조정
Spring Boot 애플리케이션 GC 모니터링 방법
Spring Boot 애플리케이션에서 GC 활동을 모니터링하는 방법들을 알아보겠습니다.
1. JVM 명령줄 옵션으로 GC 로깅 활성화
application.properties
또는 application.yml
에서 다음과 같이 설정할 수 있습니다:
# application.yml
spring:
jvm:
args: >
-Xms512m
-Xmx1024m
-XX:+UseG1GC
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/path/to/gc.log
또는 직접 명령줄에서:
java -jar -Xms512m -Xmx1024m -XX:+UseG1GC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/to/gc.log myapp.jar
2. Spring Boot Actuator로 메모리 사용량 모니터링
Spring Boot Actuator를 사용하면 REST API를 통해 애플리케이션의 메모리 상태를 확인할 수 있습니다.
먼저 의존성을 추가합니다:
<!-- Maven -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
그리고 application.properties
에서 엔드포인트를 활성화합니다:
# 모든 액추에이터 엔드포인트 활성화
management.endpoints.web.exposure.include=*
# 특정 엔드포인트만 활성화하려면
# management.endpoints.web.exposure.include=health,metrics,prometheus
이제 다음 엔드포인트로 메모리 상태를 확인할 수 있습니다:
/actuator/metrics/jvm.memory.used
/actuator/metrics/jvm.memory.max
/actuator/metrics/jvm.gc.pause
3. 전문 모니터링 도구 활용
실무에서는 다음 도구들이 많이 사용됩니다:
- JVisualVM: 무료 모니터링 도구, JDK에 포함됨
- Java Mission Control (JMC): Oracle에서 제공하는 고급 모니터링 도구
- Prometheus + Grafana: 메트릭 수집 및 시각화 조합
- Datadog, New Relic, Dynatrace: 상용 APM 도구
Prometheus + Grafana 설정 예시
Micrometer 의존성 추가:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
이제 /actuator/prometheus
엔드포인트에서 Prometheus 형식의 메트릭을 확인할 수 있습니다.
GC 튜닝 실전 파라미터 가이드
기본 힙 크기 설정
-Xms<초기 힙 크기>
-Xmx<최대 힙 크기>
예시: -Xms4g -Xmx4g
(초기값과 최대값을 동일하게 설정하면 리사이징으로 인한 오버헤드 방지)
Young Generation 크기 설정
-XX:NewRatio=<비율> # Old/Young 비율 (기본값 2, Old가 Young의 2배)
-XX:NewSize=<크기> # Young 영역 초기 크기
-XX:MaxNewSize=<크기> # Young 영역 최대 크기
예시: -XX:NewRatio=1
(Old와 Young을 1:1로 설정)
G1 GC 튜닝 (Java 9 이상 권장)
-XX:+UseG1GC # G1 GC 사용
-XX:MaxGCPauseMillis=<시간> # 목표 최대 일시 중지 시간(ms)
-XX:G1HeapRegionSize=<크기> # G1 리전 크기 (1MB~32MB, 2의 제곱)
-XX:InitiatingHeapOccupancyPercent=<비율> # Old GC 시작 힙 점유율 (기본값 45%)
예시: -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:InitiatingHeapOccupancyPercent=70
Spring Boot 애플리케이션 실전 튜닝 예시
REST API 서버 튜닝:
java -jar \
-Xms2g -Xmx2g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=100 \
-XX:+ParallelRefProcEnabled \
-XX:+UseStringDeduplication \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/logs/heapdump.hprof \
-XX:+PrintGCDetails \
-XX:+PrintGCDateStamps \
-Xloggc:/logs/gc.log \
myapp.jar
배치 처리 애플리케이션 튜닝:
java -jar \
-Xms4g -Xmx4g \
-XX:+UseParallelGC \
-XX:ParallelGCThreads=8 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/logs/heapdump.hprof \
-XX:+PrintGCDetails \
-XX:+PrintGCDateStamps \
-Xloggc:/logs/gc.log \
mybatch.jar
Spring Boot에서 메모리 누수 방지하기
메모리 누수는 GC가 수거할 수 없는 객체가 계속 쌓이는 현상입니다. Spring Boot 애플리케이션에서 자주 발생하는 메모리 누수와 해결 방법을 알아보겠습니다.
1. 정적 컬렉션 사용 주의
정적(static) 컬렉션은 애플리케이션 수명 주기 동안 GC 대상이 되지 않으므로 주의해야 합니다.
// 잘못된 예
public class CacheManager {
// 계속 증가하는 정적 맵
private static final Map<String, Object> CACHE = new HashMap<>();
public static void add(String key, Object value) {
CACHE.put(key, value); // 제거 메커니즘 없음!
}
}
// 개선된 예
public class CacheManager {
// 크기 제한 있는 LRU 캐시
private static final Map<String, Object> CACHE =
Collections.synchronizedMap(new LinkedHashMap<String, Object>(1000, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, Object> eldest) {
return size() > 1000; // 최대 1000개 항목으로 제한
}
});
}
2. ThreadLocal 사용 후 정리
ThreadLocal 변수를 사용한 후 제거하지 않으면 웹 애플리케이션에서 메모리 누수가 발생할 수 있습니다.
// 잘못된 예
public class UserContext {
private static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
public static void setUser(User user) {
userThreadLocal.set(user);
}
public static User getUser() {
return userThreadLocal.get();
}
// remove() 메서드가 없음!
}
// 개선된 예
public class UserContext {
private static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
public static void setUser(User user) {
userThreadLocal.set(user);
}
public static User getUser() {
return userThreadLocal.get();
}
// 반드시 사용 후 제거
public static void clear() {
userThreadLocal.remove();
}
}
// 스프링 인터셉터에서 자동으로 정리
public class UserContextInterceptor implements HandlerInterceptor {
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
UserContext.clear(); // 요청 처리 후 ThreadLocal 정리
}
}
3. 캐시에 약한 참조(Weak Reference) 사용
캐시에 약한 참조를 사용하면 메모리 부족 시 GC가 수거할 수 있습니다.
// 개선된 캐시 구현
public class BetterCache {
// WeakHashMap 사용 - 키가 더 이상 강하게 참조되지 않으면 항목이 제거됨
private Map<String, Object> cache = new WeakHashMap<>();
// 또는 Guava 캐시 사용
LoadingCache<Key, Value> cache = CacheBuilder.newBuilder()
.maximumSize(1000) // 최대 항목 수 제한
.expireAfterWrite(10, TimeUnit.MINUTES) // 만료 시간 설정
.weakKeys() // 약한 키 참조
.removalListener(notification -> {
// 제거 로직
})
.build(new CacheLoader<Key, Value>() {
@Override
public Value load(Key key) {
// 값 로딩 로직
}
});
}
4. 순환 참조 제거
객체 간 순환 참조는 GC를 방해할 수 있습니다. 특히 Spring Bean 간의 양방향 참조에 주의해야 합니다.
// 잘못된 예 - 순환 참조
public class Parent {
private List<Child> children = new ArrayList<>();
public void addChild(Child child) {
children.add(child);
child.setParent(this); // 순환 참조 발생
}
}
public class Child {
private Parent parent;
public void setParent(Parent parent) {
this.parent = parent;
}
}
// 개선된 예 - 약한 참조 사용
public class Child {
private WeakReference<Parent> parentRef;
public void setParent(Parent parent) {
this.parentRef = new WeakReference<>(parent);
}
public Parent getParent() {
return parentRef.get();
}
}
실전 성능 문제 해결 사례
사례 1: REST API 서버의 응답 지연
증상: 평소에는 빠른 응답 시간을 보이지만, 간헐적으로 모든 API 응답이 수 초간 지연됨
원인 분석:
- GC 로그 확인 결과 Full GC가 빈번하게 발생
- Old Generation이 빠르게 채워짐
- 캐시 데이터의 무한 증가 발견
해결 방법:
- 캐시에 만료 정책 적용 (TTL 설정)
-XX:+UseG1GC
적용하여 GC 알고리즘 변경- 힙 크기 증가 및 Old:Young 비율 조정
결과: Full GC 빈도가 하루 수십 번에서 주 1회 미만으로 감소, 응답 지연 문제 해결
사례 2: 배치 작업 중 OutOfMemoryError
증상: 대용량 데이터 처리 배치 작업 중 java.lang.OutOfMemoryError: Java heap space
오류 발생
원인 분석:
- 힙 덤프 분석 결과 대량의 중복 문자열 객체 발견
- 한 번에 너무 많은 데이터를 메모리에 로드하는 구조적 문제
해결 방법:
-XX:+UseStringDeduplication
옵션 적용 (G1 GC 사용 시)- 데이터 처리를 청크(chunk) 단위로 분할하여 배치 처리
- 불필요한 객체 조기 해제를 위한 명시적 null 처리
// 개선된 배치 처리 예제
@Configuration
public class BatchConfig {
@Bean
public Step processDataStep(JobRepository jobRepository,
PlatformTransactionManager transactionManager,
ItemReader<Data> reader,
ItemProcessor<Data, Result> processor,
ItemWriter<Result> writer) {
return new StepBuilder("processDataStep", jobRepository)
.<Data, Result>chunk(500) // 500개 단위로 처리
.reader(reader)
.processor(processor)
.writer(writer)
.transactionManager(transactionManager)
.build();
}
}
결과: 메모리 사용량 40% 감소, OOM 오류 없이 안정적으로 배치 작업 수행
자주 묻는 질문
Q: Spring Boot 애플리케이션의 기본 GC는 무엇인가요?
A: Spring Boot는 JVM 위에서 실행되므로, 사용하는 Java 버전의 기본 GC를 따릅니다. Java 8에서는 Parallel GC, Java 9 이상에서는 G1 GC가 기본값입니다.
Q: GC 튜닝의 최우선 목표는 무엇인가요?
A: 애플리케이션의 특성에 따라 다르지만, 일반적으로 다음 중 하나가 목표입니다:
- 응답 시간이 중요한 애플리케이션: GC 일시 중지 시간 최소화
- 처리량이 중요한 애플리케이션: 전체 처리량 최대화
- 균형 있는 접근: 적절한 일시 중지 시간을 유지하면서 처리량 최적화
Q: 컨테이너(Docker) 환경에서 JVM 메모리 설정 시 주의할 점은?
A: Docker 컨테이너에서는 다음 사항을 고려해야 합니다:
- Java 8u131 이전 버전은 컨테이너 메모리 제한을 인식하지 못함
-XX:+UseContainerSupport
옵션 확인 (Java 10부터 기본 활성화)- 컨테이너 메모리 제한의 약 75%를 JVM 힙 최대 크기(-Xmx)로 설정하는 것이 안전
Q: 어떤 GC 알고리즘이 가장 좋은가요?
A: "최고의" GC는 애플리케이션 특성에 따라 다릅니다:
- 응답 시간이 중요한 웹 애플리케이션: G1 GC 또는 ZGC
- 높은 처리량이 필요한 배치 작업: Parallel GC
- 저사양 환경이나 작은 힙: Serial GC
- 대규모 서버 환경: G1 GC 또는 ZGC
Q: VisualVM에서 메모리 누수를 어떻게 탐지하나요?
A: VisualVM으로 다음과 같이 메모리 누수를 탐지할 수 있습니다:
- '모니터' 탭에서 힙 사용량 그래프가 GC 후에도 지속적으로 증가하는지 관찰
- 'Sampler' 탭에서 메모리 샘플링으로 많은 메모리를 차지하는 클래스 식별
- 힙 덤프를 생성하고 '클래스', '인스턴스 수', '크기' 기준으로 정렬하여 분석
- 두 개의 힙 덤프를 비교하여 객체 수가 비정상적으로 증가한 클래스 찾기
Q: 일반적인 Spring Boot 애플리케이션에 권장되는 GC 옵션은 무엇인가요?
A: 대부분의 일반적인 Spring Boot 애플리케이션에는 다음 옵션을 권장합니다:
-Xms2g -Xmx2g # 힙 크기를 충분히, 초기값과 최대값 동일하게
-XX:+UseG1GC # G1 GC 사용 (Java 9 이상에서는 기본값)
-XX:MaxGCPauseMillis=200 # 최대 일시 중지 시간 목표
-XX:+HeapDumpOnOutOfMemoryError # OOM 발생 시 힙 덤프 생성
-XX:HeapDumpPath=/logs/heapdump.hprof # 힙 덤프 저장 경로
컨테이너 환경에서는 다음 옵션도 고려하세요:
-XX:+UseContainerSupport # 컨테이너 메모리 제한 인식 (Java 10 이상은 기본 활성화)
-XX:MaxRAMPercentage=75.0 # 컨테이너 가용 메모리의 75%를 최대 힙으로 사용
Spring Boot의 GC 최적화 실전 사례
Spring Boot 애플리케이션에서 GC를 최적화하기 위한 실전 사례를 살펴보겠습니다.
1. REST API 서버 최적화 사례
문제 상황: API 응답 시간이 간헐적으로 증가하고, 로그에 빈번한 Full GC가 기록됨
접근 방법:
- GC 로그 분석으로 문제 원인 파악
- JVM 옵션 최적화
- 애플리케이션 코드 레벨 개선
코드 개선 사례:
@Service
public class ProductService {
private final ProductRepository productRepository;
// 캐시 제한과 만료 정책 적용
@Cacheable(cacheNames = "products", key = "#id", unless = "#result == null")
public Product getProduct(Long id) {
return productRepository.findById(id).orElse(null);
}
// 페이징 적용으로 메모리 사용량 제한
public Page<Product> getAllProducts(Pageable pageable) {
return productRepository.findAll(pageable);
}
// 스트림 API 대신 페이징 처리로 대량 데이터 처리
public void processAllProducts(Consumer<Product> processor) {
int pageSize = 100;
int pageNum = 0;
Page<Product> page;
do {
page = productRepository.findAll(PageRequest.of(pageNum++, pageSize));
page.getContent().forEach(processor);
} while(page.hasNext());
}
}
JVM 설정 최적화:
# application.yml
spring:
cache:
caffeine:
spec: maximumSize=1000,expireAfterAccess=1h
jpa:
properties:
hibernate:
jdbc:
batch_size: 50 # 배치 크기 설정으로 메모리 효율성 향상
open-in-view: false # 세션 영속성 컨텍스트 제한으로 메모리 사용 감소
결과:
- API 응답 시간 평균 40% 개선
- Full GC 빈도 일 10회 -> 주 1회 미만으로 감소
- CPU 사용률 15% 감소
2. 대용량 배치 처리 최적화 사례
문제 상황: 대용량 데이터 처리 중 OutOfMemoryError 발생
접근 방법:
- 메모리 사용량 프로파일링
- 청크 기반 처리 구현
- 불필요한 객체 참조 제거
코드 개선 예시:
@Component
@EnableBatchProcessing
public class OptimizedBatchProcessor {
@Autowired
private JobBuilderFactory jobBuilderFactory;
@Autowired
private StepBuilderFactory stepBuilderFactory;
@Bean
public ItemReader<Transaction> reader() {
// JdbcPagingItemReader로 교체하여 메모리 사용량 최적화
JdbcPagingItemReader<Transaction> reader = new JdbcPagingItemReader<>();
reader.setDataSource(dataSource);
reader.setPageSize(500); // 한 번에 로드할 레코드 수 제한
reader.setQueryProvider(createQueryProvider());
reader.setRowMapper(new TransactionRowMapper());
return reader;
}
@Bean
public ItemProcessor<Transaction, ProcessedData> processor() {
return transaction -> {
ProcessedData result = processTransaction(transaction);
transaction = null; // 명시적으로 참조 해제하여 GC 도움
return result;
};
}
@Bean
public ItemWriter<ProcessedData> writer() {
return new JdbcBatchItemWriterBuilder<ProcessedData>()
.dataSource(dataSource)
.sql("INSERT INTO processed_data (...) VALUES (...)")
.itemPreparedStatementSetter(new ProcessedDataPreparedStatementSetter())
.build();
}
@Bean
public Job importJob(Step step1) {
return jobBuilderFactory.get("importJob")
.incrementer(new RunIdIncrementer())
.start(step1)
.build();
}
@Bean
public Step step1(ItemReader<Transaction> reader,
ItemProcessor<Transaction, ProcessedData> processor,
ItemWriter<ProcessedData> writer) {
return stepBuilderFactory.get("step1")
.<Transaction, ProcessedData>chunk(100) // 청크 크기 최적화
.reader(reader)
.processor(processor)
.writer(writer)
.taskExecutor(taskExecutor()) // 병렬 처리
.throttleLimit(4) // 동시 처리 스레드 제한
.build();
}
@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(4);
executor.setQueueCapacity(25);
return executor;
}
}
JVM 최적화 설정:
java -jar \
-Xms4g -Xmx4g \
-XX:+UseG1GC \
-XX:+UseStringDeduplication \
-XX:+AlwaysPreTouch \
-XX:+ScavengeBeforeFullGC \
-XX:+DisableExplicitGC \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/logs/heapdump.hprof \
batch-processor.jar
결과:
- 메모리 사용량 60% 감소
- 배치 작업 성공적 완료 (이전에는 OOM으로 실패)
- 처리 속도 15% 향상 (청크 처리 및 병렬화로 인한 효과)
결론: Spring Boot 애플리케이션의 GC 튜닝 핵심 원칙
JVM GC 튜닝은 단순히 JVM 옵션을 조정하는 것을 넘어, 애플리케이션 설계와 개발 단계에서부터 메모리 사용을 최적화하는 총체적인 접근이 필요합니다. 다음 원칙을 기억하세요:
- 측정 먼저, 최적화는 그 다음: GC 로그와 프로파일링을 통해 실제 문제를 정확히 파악한 후 최적화
- 싼 것이 비싼 것: 객체 생성은 비교적 저렴하지만, 불필요한 객체를 오래 유지하는 것은 비용이 큼
- 캐시는 양날의 검: 적절한 캐시 전략으로 성능은 높이되, 메모리 누수는 방지
- 대규모 데이터는 청크로 처리: 한 번에 모든 데이터를 메모리에 로드하지 말고 청크 단위로 처리
- GC 튜닝은 장기전: 운영 환경에서 지속적인 모니터링과 미세 조정 필요
JVM GC에 대한 이해와 적절한 튜닝은 Java 백엔드 개발자의 필수 역량입니다. 이 가이드가 여러분의 Spring Boot 애플리케이션 성능 최적화에 도움이 되었기를 바랍니다.
추가 자료
'자바(Java) 실무와 이론' 카테고리의 다른 글
Java 21부터 달라진 주요 기능 요약: 실무 개발자가 알아야 할 핵심 변화점 (0) | 2025.05.23 |
---|---|
자바로 만드는 자동 메일링 시스템 – JavaMailSender 완벽 정복 (0) | 2025.05.11 |
[자바] Java로 파일 압축/해제하기: Zip API 예제 (0) | 2025.01.24 |
[자바] Java에서 Enum 클래스의 활용법 (1) | 2025.01.24 |
[자바] JVM OutOfMemoryError 해결 가이드: 실무에서의 사례 분석 (1) | 2025.01.20 |