Spring Boot는 개발 생산성이 높지만, 트래픽이 많거나 대규모 데이터를 처리하는 환경에서는 성능 최적화가 필수적입니다.
1. JVM 및 애플리케이션 레벨 최적화
1-1. JVM 메모리 설정 조정
Spring Boot는 JVM 위에서 동작하므로, 올바른 메모리 설정이 가장 기본적인 튜닝 포인트입니다.
# 예시 (4GB 메모리 환경)
JAVA_OPTS="-Xms2g -Xmx2g -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError"
- -Xms / -Xmx: 힙 메모리 최소/최대 크기 설정
- G1GC: 대규모 힙에서 효율적인 GC 알고리즘
- HeapDumpOnOutOfMemoryError: OOM 발생 시 원인 분석을 위한 덤프 생성
운영 환경에서는 G1GC 또는 ZGC를 권장하며, GC 로그를 통해 Full GC 빈도를 모니터링하는 것이 좋습니다.
1-2. Spring Boot DevTools 제거
개발 편의를 위해 사용하는 spring-boot-devtools는 운영 환경에서 절대 사용하지 않아야 합니다. 해당 모듈은 ClassLoader를 반복적으로 재로딩하여 불필요한 CPU와 메모리 사용을 유발합니다.
runtimeOnly 'org.springframework.boot:spring-boot-devtools' // 개발 환경에서만
운영 시에는 gradle profile 또는 Spring profile로 devtools 의존성을 제외하세요.
2. 데이터베이스 성능 최적화
2-1. 커넥션 풀 튜닝 (HikariCP)
Spring Boot 2.x 이상에서는 기본적으로 HikariCP를 커넥션 풀로 사용합니다. 적절한 풀 크기 설정은 응답 속도에 직접적인 영향을 줍니다.
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
idle-timeout: 30000
max-lifetime: 1800000
DB CPU 코어 수 × 2 정도의 maximum-pool-size가 일반적인 기준입니다. DB 트래픽 패턴에 따라 모니터링 후 조정해야 합니다.
2-2. N+1 쿼리 문제 제거
JPA를 사용할 때 가장 자주 발생하는 성능 문제는 N+1 문제입니다. 즉, 한 번의 조회로 여러 엔티티를 가져올 때 연관된 데이터가 각각 추가 쿼리로 발생하는 경우입니다.
@EntityGraph(attributePaths = {"orders", "addresses"})
List<User> findAll();
또는 fetch join을 적극적으로 활용하여 불필요한 쿼리 호출을 줄입니다.
SELECT u FROM User u JOIN FETCH u.orders
2-3. 캐싱 도입 (Redis, Caffeine)
자주 조회되는 데이터는 캐싱을 통해 DB 부하를 줄입니다.
@Cacheable(value = "userCache", key = "#userId")
public User getUser(Long userId) {
return userRepository.findById(userId).orElseThrow();
}
Spring Cache와 Redis를 연동하면 다중 서버 환경에서도 캐시 일관성을 유지할 수 있습니다.
3. API 및 네트워크 성능 최적화
3-1. Controller 응답 압축
HTTP 응답에 GZIP 압축을 적용하면 네트워크 전송량을 크게 줄일 수 있습니다.
server:
compression:
enabled: true
mime-types: text/html,text/xml,text/plain,application/json
min-response-size: 1024
특히 JSON API에서 평균 30~70%의 트래픽 절감 효과를 얻을 수 있습니다.
3-2. HTTP Keep-Alive 및 Connection Pool
외부 API를 자주 호출한다면, RestTemplate 또는 WebClient에서 커넥션 풀을 설정해야 합니다.
PoolingHttpClientConnectionManager manager = new PoolingHttpClientConnectionManager();
manager.setMaxTotal(200);
manager.setDefaultMaxPerRoute(20);
Spring WebFlux(WebClient)에서는 ConnectionProvider를 통해 커넥션 재사용을 설정할 수 있습니다.
3-3. API 캐싱 및 ETag
정적 API나 변경 주기가 긴 데이터는 ETag를 이용한 클라이언트 캐싱을 적용합니다.
@GetMapping("/data")
public ResponseEntity<DataResponse> getData(@RequestHeader(value="If-None-Match", required=false) String eTag) {
String currentETag = generateETag();
if (currentETag.equals(eTag)) {
return ResponseEntity.status(HttpStatus.NOT_MODIFIED).eTag(currentETag).build();
}
return ResponseEntity.ok().eTag(currentETag).body(service.getData());
}
이렇게 하면 데이터가 변경되지 않은 경우 304 응답으로 빠르게 처리됩니다.
4. 비즈니스 로직 성능 개선
4-1. 비동기 처리 (@Async, CompletableFuture)
CPU와 I/O 부하가 높은 작업은 비동기로 처리하여 응답 시간을 줄입니다.
@Async
public CompletableFuture<Result> processData() {
return CompletableFuture.supplyAsync(() -> heavyTask());
}
단, 트랜잭션 전파 범위를 벗어나는 작업에는 주의가 필요합니다.
4-2. 배치 처리 및 큐 활용
대량의 데이터를 한 번에 처리해야 한다면, Spring Batch 또는 메시지 큐(Kafka, RabbitMQ)를 이용해 비동기 분산 처리 구조로 전환합니다.
즉각적인 결과가 필요하지 않은 작업은 큐에 적재하고, 별도의 워커에서 처리하도록 설계하는 것이 일반적입니다.
5. 인프라 및 운영 단계 최적화
5-1. 프로파일 기반 설정 분리
Spring Boot는 환경별로 서로 다른 설정을 가질 수 있습니다.
spring:
profiles:
active: prod
운영 환경에서는 로깅 수준, 캐시 TTL, DB 커넥션 풀 등 프로파일별로 최적화된 구성을 적용하세요.
5-2. Actuator 및 APM 도입
Spring Boot Actuator를 활성화하면 성능 메트릭을 실시간으로 확인할 수 있습니다.
management:
endpoints:
web:
exposure:
include: health, metrics, prometheus
Elastic APM, NewRelic, Datadog과 같은 APM 도구를 연동하면, SQL 쿼리 지연, GC 시간, 메소드 실행 시간 등을 시각화할 수 있습니다.
5-3. Docker 및 JVM 이미지 최적화
컨테이너 기반 배포에서는 JVM 튜닝 + 경량 이미지 구성이 필수입니다.
FROM eclipse-temurin:17-jre-alpine
COPY build/libs/app.jar app.jar
ENTRYPOINT ["java","-XX:+UseContainerSupport","-jar","/app.jar"]
Alpine 이미지를 사용하면 기본 이미지 대비 약 60% 용량 절감 효과를 얻을 수 있습니다.
6. 주요 성능 병목 지점 점검 체크리스트
- DB 연결 풀에서 Connection Leak 발생 여부 확인
- 대량 루프 내 save() 반복 호출 → Batch Insert로 전환
- 빈번한 JSON 직렬화 → ObjectMapper 재사용
- REST API 호출 과다 → Feign + Circuit Breaker 적용
- 스레드 풀 부족 → ThreadPoolTaskExecutor 크기 조정
주의할 점
- 무분별한 캐싱은 데이터 불일치 문제를 유발할 수 있음
- JPA Fetch 전략 변경 시 Lazy 로딩 순환 참조 주의
- GC 튜닝은 트래픽 패턴에 따라 다르게 적용해야 함
- “성능 최적화”보다 “병목 제거”가 더 중요함
Spring Boot 성능 최적화는 단일 설정 변경으로 끝나는 작업이 아닙니다. JVM, DB, 네트워크, 애플리케이션 로직 등 여러 계층의 병목을 지속적으로 모니터링하고 개선하는 과정이 필요합니다.
'개발 > JAVA' 카테고리의 다른 글
| DDD(Domain Driven Design)와 Spring 적용 — 복잡한 비즈니스를 코드로 명확하게 표현하기 (0) | 2025.11.16 |
|---|---|
| 대규모 트래픽을 위한 캐싱 전략 — 성능과 안정성을 동시에 잡는 방법 (0) | 2025.11.15 |
| Spring Boot와 Elasticsearch 연동하기 — 대용량 검색 서비스의 핵심 구현 (0) | 2025.11.14 |
| Spring Boot와 AWS S3 파일 저장하기 - 안전하고 확장 가능한 파일 스토리지 구축 (0) | 2025.11.13 |
| Spring Boot와 AWS RDS 연동하기 - 안정적인 클라우드 데이터베이스 환경 구축 (0) | 2025.11.12 |
