실무에서의 Spring Boot 성능 최적화 방법 - 빠르고 안정적인 서비스 운영을 위한 핵심 가이드

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, 네트워크, 애플리케이션 로직 등 여러 계층의 병목을 지속적으로 모니터링하고 개선하는 과정이 필요합니다.