프론트 시스템 아키텍처
프론트엔드는 Next.js App Router를 라우팅/렌더링의 중심으로 두고, UI 컴포넌트, Zustand 기반 클라이언트 상태, 서비스/API 계층, 인증 복구를 분리해 유지보수성과 운영 안정성을 확보한 구조입니다.
전체 구조

서비스별 역할
| 계층 | 핵심 책임 | |
|---|---|---|
| App Router | URL과 페이지 진입점 관리 | |
| Layout / Provider | 공통 레이아웃, 인증 초기화, 전역 환경 구성 | |
| Components | 재사용 가능한 UI와 도메인 화면 조각 구성 | |
| Stores | 로그인, 사용자, 온보딩 등 클라이언트 전역 상태 관리 | |
| Services / API | 백엔드 API 호출과 인증 요청 처리 | |
| Config / Security | 배포, 이미지, 보안 헤더, CDN 설정 관리 |
백엔드 시스템 아키텍처 (이너)
사용자 요청은 CDN → NLB → Istio를 통과한 뒤 API Gateway에 도달합니다. Gateway에서 JWT를 중앙 검증하고 X-User-Id 헤더를 주입하여 하위 서비스로 라우팅합니다. 하위 서비스는 JWT를 직접 파싱하지 않고 헤더만 신뢰합니다.
PlayBall 백엔드는 5개 마이크로서비스와 1개 공통 라이브러리로 구성되며, 서비스 간 직접 REST 호출 없이 Redis / Kafka를 통한 간접 데이터 공유만 존재합니다. 현재 DB는 단일 PostgreSQL 인스턴스에 테이블 소유권만 분리된 상태이며, 향후Payment 서비스 분리와 DB 스키마 분리를 대비해 EDA(Event-Driven Architecture)로 전환했습니다. 상세 내용은 MSA · EDA 전환 문서 참고.
전체 구조
flowchart TD
U[사용자] --> CDN[Vercel CDN DDOS 방어]
CDN --> NLB[NLB 접근 제한]
NLB --> ISTIO[Istio Gateway WAF + mTLS]
ISTIO --> G[API-Gateway :8085]
G --> AG[Auth-Guard :8080]
G --> Q[Queue :8081]
G --> S[Seat :8082]
G --> O[Order-Core :8083]
AG -.-> RC[공용 Redis :6379]
S -.-> RC
O -.-> RC
G -.-> RC
Q -.-> RQ[Queue 전용 Redis :6380]
S -.-> RQ
AG --> DB[(PostgreSQL :5432)]
S --> DB
O --> DB
AG -->|publish| KF[Kafka :9092]
O -->|publish| KF
S -->|consume| KF
O -->|consume| KF서비스별 역할
| 서비스 | 포트 | 프레임워크 | 핵심 책임 |
|---|---|---|---|
| API-Gateway | 8085 | Spring Cloud Gateway (WebFlux) | JWT 중앙 검증, 라우팅, CORS, Rate Limiting, 봇 차단 |
| Auth-Guard | 8080 | Spring Boot MVC | Kakao OAuth, JWT 발급/갱신(RTR), 로그아웃/블랙리스트, 유저 차단/해제 |
| Queue | 8081 | Spring Boot MVC | Redis ZSET 대기열, Admission Token 발급, Pre-Queue 검증 |
| Seat | 8082 | Spring Boot MVC | 좌석 추천/배정, Redisson 분산 락, Hold 관리 |
| Order-Core | 8083 | Spring Boot MVC | 주문 생성, 결제, 이메일 발송, 마이페이지 |
| common-core | — | 공유 라이브러리 | 도메인 엔티티, Kafka 설정, 암호화, 전역 예외 |
데이터 인프라
PostgreSQL
단일 PostgreSQL 16 인스턴스에서 테이블 소유권을 서비스별로 명확히 분리합니다. 각 서비스는 자신의 테이블만 쓰기하고, 다른 서비스 테이블은 읽기만 합니다. AES-256-GCM 필드 암호화가 email, nickname, phone에 적용됩니다.
| 서비스 | 소유 테이블 |
|---|---|
| Auth-Guard | users, user_sns, dev_users, withdrawal_requests |
| Seat | seats, match_seats, seat_holds, blocks, sections, areas, price_policies |
| Order-Core | orders, order_seats, payments, cash_receipts, cancellation_fee_policies, inquiries |
| 공유 (common-core) | matches, clubs, stadiums, onboarding_preferences, onboarding_preferred_blocks |
Redis 2대 분리
Redis는 Queue 전용(:6380)과 공용(:6379)으로 두 인스턴스를 분리합니다. 티켓 오픈 시 대기열 트래픽이 폭발하더라도 인증이나 좌석 분산 락에 영향을 주지 않도록 설계했습니다.
Kafka 이벤트 메시징 (Apache Kafka 3.7.1)
| 토픽 | Producer | Consumer | 용도 |
|---|---|---|---|
payment-completed | Order-Core | Seat | 결제 완료 시 좌석 BLOCKED → SOLD 전환 |
order-cancelled | Order-Core | Seat | 주문 취소 시 좌석 SOLD → AVAILABLE 복원 |
bank-transfer-expired | Order-Core | Seat | 무통장 입금 기한 만료 시 좌석 복원 |
user-blocked | Auth-Guard | Order-Core | 유저 차단 시 활성 주문 UNDER_REVIEW 처리 |
파티션 3 / Acks all / 3회 재시도 후 실패 시 {토픽}.DLT(Dead Letter Topic)로 전송됩니다.
캐싱 전략
Caffeine 로컬 캐시 (JVM 인-메모리)
W-TinyLFU 교체 알고리즘을 사용하는 고성능 로컬 캐시. 네트워크 홉 없이 sub-microsecond 접근이 가능하며, 거의 불변 데이터(Match, Section, Block)에 적용되어 DB 커넥션 부하를 크게 낮췄습니다.
| 서비스 | 캐시 | 최대 크기 | TTL | 대상 데이터 |
|---|---|---|---|---|
| Seat | match-exists | 1,000 | 10분 | Match 존재 검증 |
| Seat | match-detail | 1,000 | 10분 | Match 메타데이터 (JOIN FETCH) |
| Seat | section-all | 16 | 1시간 | 스타디움 섹션 구조 (영구 불변) |
| Seat | blocks-by-section-ids | 512 | 1시간 | 스타디움 블럭 매핑 |
| Queue | match-for-queue | 1,000 | 1분 | Match saleStatus 검증 |
| Order-Core | match-detail | 1,000 | 10분 | 주문서용 Match 메타데이터 |
Redis 분산 캐시
인스턴스 간 공유가 필요한 변동 데이터(User, 응답 캐시)는 Redis 분산 캐시를 사용합니다.
| 서비스 | 캐시 | TTL | 목적 |
|---|---|---|---|
| Auth-Guard | user-by-id | 10분 | User DTO 스냅샷 (Pod 간 즉시 전파) |
| Auth-Guard | auth-me | 30초 | /me 엔드포인트 응답 |
| Seat | seat-groups-response | 5초 | 좌석맵 API 응답 (유저 독립적) |
| Order-Core | matches-list-response | 30초 | 경기 목록 API 응답 |
스케줄러 (백그라운드 작업)
| 서비스 | 스케줄러 | 주기 | 역할 |
|---|---|---|---|
| Queue | QueuePromotionScheduler | 1초 | 대기열 → Ready 승격 (배치 100명) |
| Seat | MatchSeatScheduler | 매일 00:00 KST | 경기 7일 전 좌석 데이터 자동 생성 |
| Seat | SeatHoldCleanupScheduler | 60초 | 만료된 Hold 정리 + 좌석 AVAILABLE 복원 |
| Order-Core | MatchStatusScheduler | 10:59 / 00:00 KST | 경기 판매 개시(ON_SALE) / 종료(CLOSED) |
| Order-Core | BankTransferExpirationScheduler | 5분 | 입금 기한 초과 무통장 주문 자동 취소 |