티스토리 뷰

반응형

Spring Boot 성능 최적화의 핵심은 JPA 사용 방식, SQL 쿼리, 캐시 전략, 커넥션 풀, 관측 지표를 함께 점검하는 것입니다. 특히 응답 시간이 느린 API는 대부분 비즈니스 로직 자체보다 N+1 쿼리, 불필요한 즉시 로딩, 인덱스 누락, 반복 조회, 과도한 트랜잭션 범위에서 병목이 발생합니다. 이 글은 Spring Boot 기반 서비스를 운영하거나 배포 전 성능을 점검하려는 개발자를 위해, 바로 적용 가능한 JPA·쿼리·캐시 체크리스트를 정리합니다. 결론부터 말하면 측정 없이 튜닝하지 않고, 가장 먼저 DB 왕복 횟수와 실행 계획을 줄이는 것이 가장 효과적입니다.

Spring Boot 성능 최적화는 측정부터 시작해야 합니다

Spring Boot 성능 최적화에서 가장 흔한 실수는 “느릴 것 같은 코드”를 먼저 고치는 것입니다. 실제 병목은 예상과 다를 때가 많습니다. API 응답 시간이 1초 이상 걸린다고 해서 항상 JVM, 서버 스펙, 네트워크가 문제인 것은 아닙니다. 실무에서는 DB 쿼리 수가 수십 개로 늘어나거나, 인덱스를 타지 못한 쿼리가 전체 응답 시간을 대부분 차지하는 경우가 많습니다.

먼저 다음 지표를 확인해야 합니다.

점검 항목 확인할 내용 대표 도구
API 응답 시간 p50, p95, p99 지연 시간 Micrometer, Prometheus, Grafana
DB 쿼리 수 요청 1건당 실행 쿼리 개수 Hibernate SQL log, datasource-proxy
느린 쿼리 실행 시간이 긴 SQL MySQL slow query log, PostgreSQL pg_stat_statements
JVM 상태 GC, 힙 사용량, 스레드 수 Actuator, JFR, VisualVM
커넥션 풀 active, idle, timeout HikariCP metrics

Spring Boot Actuator와 Micrometer를 붙이면 애플리케이션 레벨의 지표를 빠르게 볼 수 있습니다. 운영 환경에서는 단순 평균보다 p95, p99 지연 시간이 더 중요합니다. 평균 100ms인 API라도 일부 요청이 3초 이상 걸리면 사용자는 느리다고 느낍니다.

개발 환경에서는 Hibernate SQL 로그를 켜고 한 번의 API 호출에서 실제 SQL이 몇 번 나가는지 확인하는 습관이 필요합니다. 단, 운영 환경에서 SQL 전체 로그를 무분별하게 켜면 로그 I/O가 병목이 될 수 있으므로 샘플링 또는 프록시 도구를 사용하는 편이 좋습니다.

성능 최적화의 첫 단계는 코드를 바꾸는 것이 아니라, 병목 위치를 숫자로 확인하는 것입니다.

Spring Boot JPA 성능 최적화: N+1과 Fetch 전략

Spring Boot JPA 성능 최적화에서 가장 먼저 봐야 할 문제는 N+1 쿼리입니다. 예를 들어 게시글 목록 20개를 조회한 뒤 각 게시글의 작성자 정보를 화면에 보여준다고 가정해 보겠습니다. 게시글 목록 조회 쿼리 1번에 이어 작성자 조회 쿼리가 게시글 수만큼 추가로 실행되면 총 21번의 쿼리가 발생합니다. 데이터가 20개일 때는 티가 덜 나지만 200개, 2,000개로 늘어나면 응답 시간은 급격히 악화됩니다.

대표적인 해결 방법은 다음과 같습니다.

  • fetch join으로 필요한 연관 엔티티를 한 번에 조회
  • @EntityGraph로 조회 시점의 로딩 전략을 명시
  • DTO projection으로 화면에 필요한 필드만 조회
  • 컬렉션 연관관계는 페이징과 함께 사용할 때 주의
  • 기본 연관관계는 가능하면 LAZY로 설정

예시는 다음과 같습니다.

@Query("select p from Post p join fetch p.author where p.id in :ids")
List<Post> findAllWithAuthor(@Param("ids") List<Long> ids);

하지만 fetch join을 모든 곳에 붙이는 것도 답은 아닙니다. 특히 @OneToMany 컬렉션을 fetch join하면서 페이징을 같이 쓰면 DB 페이징이 아니라 메모리 페이징으로 처리될 수 있습니다. 이 경우에는 먼저 루트 엔티티 ID만 페이징 조회한 뒤, 필요한 연관 데이터를 별도로 가져오는 2단계 조회가 더 안정적입니다.

JPA 성능 체크리스트는 다음과 같이 정리할 수 있습니다.

체크 항목 권장 방향
연관관계 기본 로딩 LAZY 우선
목록 API 엔티티 직접 반환보다 DTO projection 고려
상세 API 필요한 연관관계만 fetch join
컬렉션 페이징 fetch join 남용 금지
변경 감지 트랜잭션 범위를 작게 유지
대량 수정 엔티티 반복 수정 대신 bulk update 검토

또 하나 중요한 부분은 엔티티를 API 응답으로 직접 반환하지 않는 것입니다. 엔티티를 그대로 직렬화하면 의도하지 않은 lazy loading이 발생하거나, 순환 참조 문제가 생길 수 있습니다. 성능과 API 안정성을 모두 고려하면 요청 목적에 맞춘 DTO를 사용하는 편이 좋습니다.

Spring Boot 쿼리 최적화: 인덱스와 실행 계획 체크

Spring Boot 쿼리 최적화는 JPA 코드만 보는 것으로 끝나지 않습니다. 최종적으로 DB에 전달되는 SQL이 어떤 실행 계획으로 처리되는지 확인해야 합니다. 같은 JPA 메서드라도 조건절, 정렬, 조인, 데이터 분포에 따라 성능 차이가 크게 납니다.

가장 먼저 점검할 것은 인덱스입니다. 예를 들어 게시글 테이블에서 status, created_at 조건으로 최신 목록을 조회한다면 다음과 같은 복합 인덱스가 효과적일 수 있습니다.

CREATE INDEX idx_post_status_created_at
ON post (status, created_at DESC);

하지만 인덱스는 많을수록 좋은 것이 아닙니다. 조회는 빨라질 수 있지만 쓰기 성능과 저장 공간에는 비용이 생깁니다. 따라서 자주 사용되는 조회 패턴을 기준으로 설계해야 합니다.

쿼리 최적화 시 자주 보는 기준은 다음과 같습니다.

상황 확인 포인트
조건 검색이 느림 WHERE 절 컬럼에 적절한 인덱스가 있는가
정렬이 느림 ORDER BY 컬럼이 인덱스와 맞는가
조인이 느림 조인 키 양쪽 타입과 인덱스가 일치하는가
페이지 뒤로 갈수록 느림 offset pagination을 남용하고 있지 않은가
count 쿼리가 느림 전체 count가 꼭 필요한 화면인가

특히 offset 기반 페이징은 데이터가 많아질수록 뒤 페이지 조회가 느려집니다. 예를 들어 offset 100000 limit 20은 앞의 100,000건을 건너뛰는 비용이 발생할 수 있습니다. 무한 스크롤이나 최신순 목록이라면 keyset pagination을 고려할 수 있습니다.

SELECT *
FROM post
WHERE created_at < :lastCreatedAt
ORDER BY created_at DESC
LIMIT 20;

또한 검색 조건에 함수가 걸리면 인덱스를 제대로 사용하지 못할 수 있습니다. 예를 들어 where date(created_at) = '2026-05-23'보다 created_at >= '2026-05-23 00:00:00' and created_at < '2026-05-24 00:00:00' 형태가 더 유리한 경우가 많습니다.

Spring Boot에서 쿼리 최적화를 할 때는 “JPA 메서드가 깔끔한가”보다 “DB가 적은 비용으로 결과를 찾는가”를 기준으로 판단해야 합니다.

Spring Boot 캐시 최적화: Redis와 로컬 캐시 선택 기준

Spring Boot 캐시 최적화는 반복 조회가 많고 데이터 변경 빈도가 낮은 영역에서 효과가 큽니다. 예를 들어 메인 화면의 인기 게시글, 카테고리 목록, 공통 코드, 사용자 권한 정보, 외부 API 응답은 캐시 적용 후보가 될 수 있습니다. 반대로 매번 최신성이 중요한 결제 상태, 재고 수량, 실시간 잔액 같은 데이터는 캐시 정책을 매우 신중하게 설계해야 합니다.

Spring Boot에서는 @Cacheable, @CacheEvict, @CachePut을 통해 캐시를 쉽게 붙일 수 있습니다.

@Cacheable(value = "postSummary", key = "#postId")
public PostSummaryResponse getPostSummary(Long postId) {
    return postRepository.findSummaryById(postId)
        .orElseThrow(() -> new NotFoundException("post"));
}

캐시 저장소는 크게 로컬 캐시와 분산 캐시로 나눌 수 있습니다.

구분 장점 주의점 예시
로컬 캐시 빠르고 구조가 단순함 서버별 데이터 불일치 가능 Caffeine
분산 캐시 여러 서버가 같은 캐시 공유 네트워크 비용과 운영 부담 Redis
HTTP 캐시 클라이언트·CDN 활용 가능 인증·개인화 응답 주의 Cache-Control

단일 서버이거나 읽기 전용 성격이 강한 데이터는 Caffeine 같은 로컬 캐시가 단순하고 빠릅니다. 여러 대의 서버에서 같은 캐시 값을 공유해야 하거나 배포·스케일아웃 환경이라면 Redis가 더 적합합니다.

캐시에서 가장 중요한 것은 TTL과 무효화 전략입니다. 캐시를 붙였는데 데이터가 오래 남아 사용자가 잘못된 정보를 본다면 성능 개선보다 더 큰 문제가 됩니다. 따라서 다음 질문에 답할 수 있어야 합니다.

  • 이 데이터는 몇 초 또는 몇 분 동안 오래되어도 괜찮은가?
  • 데이터가 수정될 때 어떤 캐시 키를 삭제해야 하는가?
  • 캐시 미스가 한 번에 몰릴 때 DB가 버틸 수 있는가?
  • null 결과도 캐시할 것인가?
  • 개인별 데이터와 공용 데이터를 같은 키 구조로 섞고 있지 않은가?

또한 캐시 키 설계도 중요합니다. user:123:profile, post:456:summary처럼 도메인과 식별자를 명확히 포함하면 운영 중 삭제나 추적이 쉬워집니다. 캐시는 성능을 높이는 도구이지만, 잘못 설계하면 장애를 숨기거나 데이터 정합성 문제를 만들 수 있습니다.

Spring Boot 서버 설정 최적화: HikariCP, 트랜잭션, 스레드

Spring Boot 성능 최적화에서 애플리케이션 설정도 놓치면 안 됩니다. 특히 DB 커넥션 풀인 HikariCP 설정은 API 처리량에 직접적인 영향을 줍니다. 커넥션 풀 크기를 무작정 크게 잡으면 성능이 좋아질 것 같지만, 실제로는 DB에 과부하를 주고 컨텍스트 스위칭 비용만 늘릴 수 있습니다.

기본적으로 확인할 설정은 다음과 같습니다.

spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 10
      connection-timeout: 3000
      idle-timeout: 600000
      max-lifetime: 1800000

적절한 풀 크기는 서버 CPU, DB 성능, 쿼리 응답 시간, 동시 요청 수에 따라 달라집니다. 그래서 정답 숫자보다 중요한 것은 실제 지표입니다. active connections가 항상 최대치에 붙어 있고 connection timeout이 발생한다면 쿼리가 느리거나 풀 크기가 부족할 수 있습니다. 반대로 DB CPU가 이미 높은데 풀만 키우면 장애가 더 빨리 커질 수 있습니다.

트랜잭션 범위도 성능에 영향을 줍니다. 외부 API 호출, 파일 업로드, 복잡한 계산을 DB 트랜잭션 안에 오래 넣어두면 커넥션을 불필요하게 점유합니다. 읽기 전용 조회에는 @Transactional(readOnly = true)를 적용하면 JPA flush 비용을 줄이는 데 도움이 됩니다.

@Transactional(readOnly = true)
public List<PostListResponse> getPosts(PostSearchCondition condition) {
    return postRepository.search(condition);
}

스레드 설정도 함께 봐야 합니다. Tomcat의 요청 처리 스레드 수가 많아도 DB 커넥션 풀이 작으면 결국 DB 앞에서 대기합니다. 반대로 DB 커넥션은 충분하지만 외부 API가 느리면 스레드가 대기 상태로 묶입니다. 즉, 서버 설정 최적화는 하나의 값만 조정하는 작업이 아니라 요청 스레드, DB 커넥션, 외부 연동, CPU 사용률의 균형을 맞추는 작업입니다.

실무 체크리스트는 다음과 같습니다.

  • 조회 서비스에는 readOnly = true 적용 여부 확인
  • 트랜잭션 안에서 외부 API를 호출하지 않는지 점검
  • HikariCP timeout, active, idle 지표 모니터링
  • 느린 쿼리 때문에 커넥션이 오래 점유되는지 확인
  • 서버 스레드 수와 DB 커넥션 수의 병목 위치 비교

Spring Boot 성능 최적화 체크리스트: 배포 전 점검표

배포 전에는 코드 리뷰만으로 성능을 판단하기 어렵습니다. 최소한 핵심 API에 대해 요청당 SQL 수, 응답 시간, 캐시 적중률, 커넥션 풀 상태를 확인해야 합니다. 작은 서비스라도 이 과정을 거치면 운영 중 장애 가능성을 크게 줄일 수 있습니다.

아래 체크리스트를 기준으로 점검해 보시면 좋습니다.

영역 체크리스트 완료
JPA 목록 API에서 N+1 쿼리가 없는가  
JPA 엔티티 직접 반환 대신 DTO를 사용하는가  
JPA 컬렉션 fetch join과 페이징을 함께 쓰지 않는가  
쿼리 주요 검색 조건에 적절한 인덱스가 있는가  
쿼리 실행 계획에서 full scan이 과도하지 않은가  
쿼리 뒤 페이지 조회가 느리면 keyset pagination을 검토했는가  
캐시 반복 조회 데이터에 TTL이 명확한가  
캐시 수정 시 캐시 무효화 정책이 있는가  
서버 HikariCP active, idle, timeout 지표를 보는가  
서버 트랜잭션 범위가 과도하게 넓지 않은가  
관측 p95, p99 응답 시간을 모니터링하는가  

성능 테스트는 운영 데이터와 완전히 같을 수는 없지만, 최소한 데이터가 적을 때만 빠른 구조인지 확인해야 합니다. 개발 DB에 데이터가 100건밖에 없으면 인덱스 누락이나 N+1 문제가 잘 드러나지 않습니다. 가능하다면 운영과 유사한 규모의 샘플 데이터를 준비하고 주요 API에 부하를 걸어보는 것이 좋습니다.

또한 최적화는 한 번에 끝나는 작업이 아닙니다. 기능이 추가되고 데이터가 늘어나면 이전에는 괜찮았던 쿼리도 느려질 수 있습니다. 따라서 신규 기능 배포 시 “API 응답 시간과 쿼리 수가 기존 대비 얼마나 변했는가”를 리뷰 기준에 포함하는 것이 실용적입니다.

마지막으로, 성능 개선은 사용자 경험과 비용을 함께 봐야 합니다. 응답 시간을 500ms에서 100ms로 줄이는 것보다 5초 걸리는 API를 800ms로 줄이는 것이 훨씬 큰 효과를 냅니다. 우선순위는 항상 사용자 영향이 큰 병목부터 잡는 것이 좋습니다.

핵심 요약

  • Spring Boot 성능 최적화는 측정이 먼저입니다. 평균 응답 시간보다 p95, p99 지연 시간을 확인해야 합니다.
  • JPA에서는 N+1 쿼리, 불필요한 즉시 로딩, 엔티티 직접 반환이 대표적인 성능 저하 원인입니다.
  • 쿼리 최적화는 JPA 코드가 아니라 실제 SQL과 실행 계획을 기준으로 판단해야 합니다.
  • 인덱스는 조회 패턴에 맞게 설계해야 하며, 많이 만든다고 항상 좋은 것은 아닙니다.
  • 캐시는 반복 조회와 낮은 변경 빈도의 데이터에 효과적이며, TTL과 무효화 정책이 반드시 필요합니다.
  • HikariCP 풀 크기는 무작정 늘리기보다 active, idle, timeout 지표를 보고 조정해야 합니다.
  • 트랜잭션 범위가 넓으면 DB 커넥션을 오래 점유하므로, 외부 API 호출과 긴 작업은 분리하는 것이 좋습니다.
  • 결론적으로 Spring Boot 성능 최적화는 JPA 조회 전략, SQL 실행 계획, 캐시 설계, 운영 지표를 함께 보는 체크리스트 기반 접근이 가장 안정적입니다.

Spring Boot 성능 최적화의 한 줄 요약은 “측정하고, DB 왕복을 줄이고, 반복 조회를 캐시하라”입니다. 오늘 바로 적용한다면 먼저 핵심 목록 API 3개를 골라 요청당 SQL 수와 실행 계획을 확인해 보시기 바랍니다. 이후 JPA fetch 전략, 인덱스, 캐시 TTL을 순서대로 점검하면 작은 수정만으로도 체감 성능을 개선할 가능성이 높습니다.

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/05   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
글 보관함
반응형