Playball Logo

Command Palette

Search for a command to run...

목차 열기

성능 최적화 / 부하테스트 트러블슈팅

1차부터 5차(Phase 4)에 걸쳐 단계별로 진행한 성능 개선 이력입니다.AS-IS P99 6,887ms / 503 에러 다수에서 캐싱과 인프라 튜닝으로 5,000 VU 수용을 목표로 개선했습니다.


AS-IS: 최적화 전 상태

Seat 서비스에 Tomcat 스레드 200개 vs HikariCP 커넥션 20개의 설정 불균형이 있었습니다. 180개 스레드가 findByIdOrThrow에서 커넥션 대기로 블로킹되면서 P99가 6.8초까지 치솟았습니다.

[모든 요청] → [Tomcat 스레드 200개]
     │
     ├─ matchRepository.findByIdOrThrow()     ← 매 요청 DB 조회
     ├─ sectionRepository.findAll...()        ← 매 요청 DB 조회
     ├─ blockRepository.findBySectionIdIn()   ← 매 요청 DB 조회
     ├─ userRepository.findByIdOrThrow()      ← 매 요청 DB 조회
     └─ onboardingPreference 조회             ← 매 요청 DB 조회

→ HikariCP 20개 커넥션 풀에 200개 스레드가 경합
→ 커넥션 대기 → 스레드 블로킹 → P99 6,887ms
→ DB 커넥션 270개 한계 도달 → 503 에러 다수 발생

핵심 진단: Redis 캐싱이 문제가 아니라 DB 커넥션 풀 + Tomcat 스레드 블로킹이 병목. "코드 레벨에서 할 수 있는 건 DB 커넥션을 쓰는 쿼리 자체를 줄이는 것"


DB 부하 유발 Top 7 쿼리 전수조사

순위위치쿼리요청당 호출불변성제안
1Seat/SeatCommonServiceMatchRepository.findDetailByIdOrThrow (JOIN FETCH)2회거의 불변Caffeine
2Seat/SeatCommonServiceSectionRepository.findAllWithAreaOrderBy좌석 진입 1회영구 불변Caffeine
3Seat/SeatCommonServiceBlockRepository.findBySectionIdIn좌석 진입 1회영구 불변Caffeine
4Order-Core/OrderServiceMatchRepository.findDetailByIdOrThrow2회거의 불변Caffeine
5Queue/QueueServiceMatchRepository.findByIdOrThrow매 요청거의 불변Caffeine
6Auth-Guard, Order-CoreUserRepository.findByIdOrThrow토큰/주문 매회변동 있음Redis
7Seat/recommendationOnboardingPreference/Block추천 매회변동 있음Redis

1차: Seat BookingOptions Match Caffeine 캐싱

BookingOptionsService.saveBookingOptions()matchRepository.findByIdOrThrow()를 Caffeine 로컬 캐시(match-exists, TTL 10분)로 교체. DB 커넥션 점유 시간 제거 + Tomcat 스레드 회전율 향상.

@Cacheable(cacheNames = "match-exists", key = "#matchId", unless = "!#result")
public boolean exists(Long matchId) {
    return matchRepository.existsById(matchId);
}

# Caffeine 설정
spec: maximumSize=1000,expireAfterWrite=10m,recordStats

2차 (Phase 1 확대): Multi-Service Caffeine 전면 확대

Seat/Queue/Order-Core × 6개 캐시로 확대. DB 쿼리 50~60% 감소, DB 커넥션 peak 250 → 150 (-39%).

서비스캐시TTL대상
Seatmatch-exists, match-detail10분Match 조회
Seatsection-all, blocks-by-section-ids1시간스타디움 구조 (영구 불변)
Queuematch-for-queue1분saleStatus 검증 (짧은 TTL)
Order-Corematch-detail10분주문서용 Match

3차 (Phase 2): Redis 분산 캐시 — User 데이터

User, OnboardingPreference는 변경 가능한 데이터라 로컬 캐시 대신 Redis 분산 캐시 사용. Pod A에서 닉네임 갱신 → Pod B에서도 즉시 반영.

  • Auth-Guard user-by-id (10분), auth-me (30초)
  • Order-Core user-by-id (10분, Auth-Guard 캐시 공유)
  • @CacheEvict로 프로필 변경 시 즉시 무효화
  • DefaultTyping.EVERYTHING으로 다형성 역직렬화 지원

결과: /auth/me P99 400ms → 50ms

4차 (Phase 3): API 응답 Redis 캐시 + 인프라 튜닝

파라미터변경 전변경 후이유
Tomcat max-threads200400스레드 점유 시간 감소 → 동시 처리 증가
HikariCP max-pool-size2030DB 의존 쿼리 감소로 여유 확보
HikariCP min-idle205유연한 커넥션 관리
총 DB 커넥션~80≤250max_connections=270 한계 내
  • Seat seat-groups-response 5초 캐시 → P99 2000ms → 200ms
  • Order-Core matches-list-response 30초 캐시

5차 (Phase 4): 커넥션 회전율 · 스레드 점유 핫픽스

(1) Queue OSIV OFF

spring.jpa.open-in-view=false로 변경. HTTP 요청 전체가 아닌 트랜잭션 경계에서만 DB 커넥션 점유.커넥션 회전율 2배 향상.

(2) BookingOptions Resilience4j @Retry

Thread.sleep(50~150ms) 블로킹 재시도 → Resilience4j 비동기 재시도로 교체. 평균 대기: 300ms → 60ms.

(3) Queue Redis Lua 스크립트 통합

대기열 재진입 시 ZREM + ZADD + ZRANK + ZCARD를 각각 호출 → Lua 스크립트 1회 호출로 통합.3 RTT → 1 RTT.


전체 최적화 타임라인

AS-IS (최적화 전)
│  P99: 6,887ms / DB 커넥션 270 한계 / 503 에러 다수
│
├─ 1차: Seat BookingOptions Match Caffeine 캐싱
│  └─ match-exists 캐시 도입 → 요청당 DB 조회 1회 제거
│
├─ 2차 (Phase 1 확대): Multi-Service Caffeine 전면 확대
│  └─ Seat/Queue/Order-Core × 6개 캐시
│  └─ DB 쿼리 50~60% 감소, 커넥션 peak 250→150
│
├─ 3차 (Phase 2): Redis 분산 캐시 — User 데이터
│  └─ user-by-id, auth-me Redis 캐싱
│  └─ /auth/me P99: 400ms → 50ms
│
├─ 4차 (Phase 3): API 응답 Redis 캐시 + 인프라 튜닝
│  └─ seat-groups-response 5초 캐시, matches-list 30초 캐시
│  └─ Tomcat 200→400, HikariCP 20→30
│  └─ seat-groups P99: 2000ms → 200ms
│
├─ 5차 (Phase 4): 커넥션 회전율 · 스레드 점유 핫픽스
│  └─ Queue OSIV OFF (커넥션 회전율 2배)
│  └─ BookingOptions Resilience4j @Retry
│  └─ Queue Redis Lua 통합 (3 RTT → 1 RTT)
│
TO-BE
   캐싱이 정적 데이터 쿼리를 흡수하고,
   DB 커넥션은 좌석 조회/추천 등 정말 필요한 곳에만 사용.

HikariCP 커넥션 풀이란

HikariCP는 Java에서 가장 빠른 JDBC 커넥션 풀 라이브러리로, Spring Boot의 기본 커넥션 풀입니다. DB 커넥션은 TCP 연결이라 생성 비용이 비싸므로(수십~수백 ms), 미리 커넥션을 생성해놓고 재사용합니다.

설정의미
maximum-pool-size20~30풀에서 최대 몇 개 커넥션을 유지할지
minimum-idle5~20최소 유지 커넥션 수
connection-timeout30초커넥션을 못 빌리면 이 시간 후 예외
leak-detection-threshold10초이 시간 이상 커넥션 반환 안 하면 로그 경고

기타 트러블슈팅

  • 대기열 오픈 11시 정각 입장 지연: DB saleStatus 의존 → 시간 기반 Lazy 판정으로 변경
  • Seat Hold Detached Entity: OSIV OFF 후 JPA dirty checking 실패 → Bulk UPDATE로 교체
  • AES-GCM Hibernate 불필요 UPDATE: 랜덤 IV로 매번 다른 암호문 → IV 캐싱으로 해결
  • Redis 메모리 초과: Refresh Token TTL 7일 → 4시간 단축, 부하테스트용 15분
  • seat-groups N+1 쿼리: JOIN FETCH 추가 + 인덱스 최적화