← Back to Projects

02 · Backend Portfolio

tixy

공연 예매 티켓팅 플랫폼 · 목록 조회 API 성능 최적화. 5만 건의 공연 데이터, 4개 테이블 JOIN 쿼리 — 인덱스와 캐시를 단계적으로 측정해 평균 응답 −84% · 처리량 5.5배까지 끌어올린 작업 기록.

TIXY 로고
기간8주 · 2026.03 — 2026.04
백엔드 4명
역할
  • 인덱스 + 캐싱으로 목록 조회 쿼리 성능 개선 (k6 부하 테스트 검증)
  • 복잡한 동적 쿼리를 위한 QueryDSL · jOOQ 비교 후 jOOQ 도입
  • RedisTemplate 기반 Ranking 조회 시스템 구현
Links GitHub ↗
01 · 서비스 소개

같은 문제를 여러 방식으로 풀어보고
차이를 데이터로 검증한 티켓팅 플랫폼.

공연 예매 티켓팅 플랫폼. 검색 API에 인덱스와 캐시를 단계적으로 적용해 응답 시간과 처리량이 어떻게 달라지는지 측정한 작업 기록.

모듈 구성멀티 모듈 — tixy(메인, Java 17 · Spring Boot 3.5) / tixy-pt(지원·AI, Java 21 · Spring Boot 4.0) / tixy-watcher(결제 와처, Node.js) / tixy-batch(배치, Java 17 · Spring Boot 4.0)
인프라5-Server 구성 — Nginx(프록시) + HAProxy(LB) + App × 2 + MySQL + Redis · Prometheus / Grafana / Micrometer 모니터링

기술 스택

LanguageJava 17
FrameworkSpring Boot 3.5.13
ORM / QuerySpring Data JPA · jOOQ 3.19.31
DatabaseMySQL 8 · Flyway 10.21
Cache / LockRedis 8 · Redisson 3.45 · Caffeine
AuthSpring Security · JJWT 0.12.7
MonitoringActuator · Micrometer · Prometheus · Grafana
TestJUnit 5 · k6 부하 테스트
InfraDocker · Docker Compose · Nginx · HAProxy
02 · 시스템 아키텍처

5-Server 구성 —
Nginx · HAProxy · App × 2 · DB · Cache.

tixy 5-Server 아키텍처 — Nginx 리버스 프록시 → HAProxy 로드 밸런서 → App × 2 (Rest/WS) → MySQL · Redis, Prometheus/Grafana 모니터링

클라이언트 요청은 Nginx(정적 서빙 · 프록시) → HAProxy(L7 로드밸런서) → App × 2로 흐르고, MySQL·Redis는 별도 서버로 분리. Prometheus가 앱 지표를 스크레이프해 Grafana로 시각화합니다.

03 · 담당 영역

검색 API 성능 최적화.

공연 목록 조회 API에 인덱스캐시를 각각 붙여 보고, 마지막에 둘을 합쳐 k6 부하 테스트로 검증했습니다.

인덱스 + 캐싱으로 목록 조회 쿼리 성능 개선

4-Table JOIN 쿼리에 인덱스 조합(EXPLAIN 분석)과 캐시(v1 DB / v2 Caffeine / v3 Redis)를 각각·함께 적용해 k6로 비교 검증.

동적 쿼리 도구 선택

QueryDSL · jOOQ 비교 후, 조건 조합이 많고 SQL 근접 표현이 필요해 jOOQ 채택.

Ranking 조회 시스템

RedisTemplate 기반 Sorted Set으로 조회수·판매량 랭킹 조회 구현.

04.1 · 기술적 도전

인덱스 설계.

대상
공연 검색 목록 조회 API. 4개 테이블 JOIN(events · event_sessions · ticket_types · venues), 5만 건 이벤트 데이터. 옵티마이저가 venues를 드라이빙 테이블로 잡으며 풀스캔에 가까운 동작.
접근
이론(Equality 좌측 · 고카디널리티 우선 · Range 후방)으로 후보 조합을 만든 뒤 실제 EXPLAIN 결과로 검증해 선택.
결과
최종 선정: events(category, event_status, open_date) + ticket_types(event_session_id, ticket_type_status, price).
총 실행 시간 52.5ms → 0.906ms (약 58배), 드라이빙 스캔 27,500건 → 50건 (약 550배).
배운 것
단일 시나리오로 결론 내지 않기. 시나리오를 12개로 확장하니 평균은 개선(-40%)됐지만 p95는 +5% 악화되는 조건 조합도 있었음. 이후로는 여러 조건 조합을 함께 측정하는 걸 기본 절차로 삼았습니다.
04.2 · 기술적 도전

캐싱 전략 비교 — v1 / v2 / v3.

같은 jOOQ 쿼리를 호출하는 엔드포인트를 세 버전으로 만들어 캐시 레이어만 다르게 두고 비교.

v1캐시 없음 (DB 직접 조회)
v2Caffeine 로컬 캐시 (maximumSize=100, expireAfterWrite=5m)
v3Redis 분산 캐시 (GenericJackson2JsonRedisSerializer)

개별 응답 속도 (avg · p90 · p95)

v1(DB) / v2(Local) / v3(Redis) 개별 응답 속도 — 평균/p90/p95 비교

시스템 부하 감당 (처리량 · 실패율)

v1(DB) / v2(Local) / v3(Redis) 시스템 부하 감당 — 처리량 16.9 → 91.0 → 93.1 req/s, 실패율 95.3% → 8.8% → 8.2%
핵심 관찰
개별 응답에서는 Local(v2)이 Redis(v3)보다 약 1ms 빠름. 그러나 시스템 부하 관점에서는 두 캐시가 처리량·실패율 모두 거의 동등하고, 캐시 유무의 격차(v1 대비)가 훨씬 컸음.
설계 결정
1ms 지연은 멀티 인스턴스 환경의 일관성과 바꿀 만한 차이가 아님 → 운영 관점에서 Redis(v3) 채택.
04.3 · 기술적 도전

Index + Cache 조합의
부하 테스트 검증.

k6로 Nothing / Index / Cache / Index+Cache 4가지 조합의 평균 · p95 응답 시간 비교.

4가지 조합 평균 응답(ms)과 p95 응답(ms) 비교 — Nothing 72.1/143.1, Index 43.5/150.5(+5%), Cache 48.9/61.5(−57%), Index+Cache 11.5/46.0(−84%/−68%)
관찰
  • Index 단독 — 평균 −40%이지만 p95 +5% 악화 (옵티마이저 계획 편차)
  • Cache 단독 — 평균 −32%, p95 −57% (안정성 우위)
  • Index + Cache — 평균 −84%, p95 −68% (두 지표 모두 압도)
설계 결정
인덱스는 DB 접근 효율을, 캐시는 DB 접근 자체를 줄인다. 두 기법을 같이 적용했을 때 처음 체감한 보완 관계.

v3 Redis Cache 기준 포화 지점

즉시 투입 부하 테스트 — VU 65에서 TPS 피크, 70부터 실패율 급등
점진 투입(Ramp-up) 부하 테스트 — VU 100에서 TPS 피크, 이후 실패율 급등
즉시 투입 포화
65 VU
점진 투입 포화
100 VU
처리량 (req/s)
93.1
16.9 → 93.1 (5.5×)
05 · 회고

측정 가능한 구조가
결론의 토대다.

같은 쿼리를 v1/v2/v3 세 엔드포인트로 분리하고, JVM warm-up 10회 · 시나리오마다 10회 평균으로 측정한 뒤에야 "어느 쪽이 더 빠르냐"를 숫자로 답할 수 있는 상태가 됐습니다. Index만 봤을 때 91% 개선처럼 보였지만, 시나리오를 넓히고 나서야 조건 조합에 따라 p95가 오히려 나빠질 수 있다는 걸 발견했고 — 측정을 신뢰하려면 측정 자체를 먼저 의심해야 한다는 걸 체득한 프로젝트였습니다.