목차 열기
Kafka 이벤트 메시징 / Caffeine 캐싱
PlayBall은 서비스 간 비동기 메시징으로 Apache Kafka 3.7.1을 사용하며, DB 부하를 흡수하기 위해 Caffeine 로컬 캐시와 Redis 분산 캐시를 계층적으로 활용합니다.
Kafka를 채택한 맥락(MSA 현황, EDA 전환 동기, Payment 분리/DB 스키마 분리 로드맵)은 별도 문서 MSA · EDA 전환 (Kafka 채택)에 정리되어 있습니다. 이 페이지는 토픽·설정·캐싱 구성 같은 운영 디테일에 집중합니다.
Kafka 이벤트 메시징
토픽 구성
| 토픽 | 파티션 | Producer | Consumer | 용도 |
|---|---|---|---|---|
payment-completed | 3 | Order-Core | Seat | 결제 완료 시 좌석 BLOCKED → SOLD 전환 |
order-cancelled | 3 | Order-Core | Seat | 주문 취소 시 좌석 SOLD → AVAILABLE 복원 |
bank-transfer-expired | 3 | Order-Core | Seat | 무통장 입금 기한 만료 시 좌석 복원 |
user-blocked | 3 | Auth-Guard | Order-Core | 유저 차단 시 활성 주문 UNDER_REVIEW 처리 |
신뢰성 정책
- Acks = all: 모든 replica 확인 후 응답
- Replication factor = 1, Log retention = 72h
- 재시도: 실패 시 3회 재시도 (1초 간격)
- DLT (Dead Letter Topic): 3회 재시도 후에도 실패하면
{토픽}.DLT로 전송 - @TransactionalEventListener(phase = AFTER_COMMIT): DB 커밋 이후 이벤트 발행으로 정합성 보장
이벤트 흐름 예시
1. 결제 완료
Order-Core
├─ Payment 저장 (PAID)
├─ Order 상태 전환 (PAYMENT_PENDING → PAID)
├─ [AFTER_COMMIT] Kafka publish
└─ payload: { orderId, matchSeatIds: [123, 124, 125], paymentMethod }
▼
Kafka topic: payment-completed
▼
Seat (Consumer: seat-service)
└─ @KafkaListener → MatchSeat.saleStatus: BLOCKED → SOLD
2. 유저 차단
AI 방어 서버
└─ POST /internal/users/{userId}/block
▼
Auth-Guard
├─ User.status = BLOCKED
├─ [AFTER_COMMIT] Kafka publish
└─ payload: { userId, occurredAt }
▼
Kafka topic: user-blocked
▼
Order-Core (Consumer: order-core-notification)
└─ 해당 userId의 PAID 주문 → UNDER_REVIEWCaffeine 캐싱
Caffeine이란?
Caffeine은 Java용 고성능 인-메모리 로컬 캐시 라이브러리입니다. Guava Cache의 개선 버전으로, Google 엔지니어 Ben Manes가 만들었으며 현재 Java 생태계에서 사실상 표준 로컬 캐시입니다.
핵심 개념: "로컬 캐시"
[요청] → Spring Application (JVM 메모리 안에 HashMap 같은 저장소)
↓ 있으면 바로 반환 (sub-ms)
↓ 없으면 DB/Redis 조회 후 저장
JVM 프로세스 자신의 힙 메모리에 데이터를 보관.
네트워크 홉이 없으니 접근 시간이 나노~마이크로초 단위.Redis vs Caffeine 비교
| 항목 | Caffeine (로컬) | Redis (원격) |
|---|---|---|
| 저장 위치 | JVM 힙 메모리 | 별도 서버 |
| 접근 시간 | < 1μs | 0.5~2ms (+네트워크) |
| 용량 | JVM 힙 한도 내 | GB~TB 가능 |
| 인스턴스 간 공유 | 각자 따로 | 공유됨 |
| 일관성 | 노드별 달라질 수 있음 | 단일 소스 |
| 장애 격리 | JVM과 운명공유 | Redis 다운 시 영향 |
Caffeine의 강점
- 매우 빠른 구현 — W-TinyLFU 교체 알고리즘으로 LRU보다 높은 hit rate, Lock-free에 가까운 동시성
- 풍부한 만료 정책 —
expireAfterWrite,expireAfterAccess,refreshAfterWrite,recordStats - Spring Cache와 자연스러운 통합 —
@Cacheable어노테이션만 붙이면 끝
PlayBall에서의 Caffeine 활용
@Cacheable(cacheNames = "match-exists", key = "#matchId", unless = "!#result")
public boolean exists(Long matchId) {
return matchRepository.existsById(matchId);
}
# application.yaml
spring:
cache:
type: caffeine
cache-names: match-exists
caffeine:
spec: maximumSize=1000,expireAfterWrite=10m,recordStats- maximumSize=1000: 최대 1000개 캐싱 (초과 시 LFU eviction)
- expireAfterWrite=10m: 쓰기 후 10분 만료
- recordStats:
/actuator/metrics/cache.gets로 hit/miss 통계
주의할 점
- 멀티 인스턴스 시 캐시 불일치 — 자주 바뀌는 데이터엔 부적합
- JVM 힙 사용 — 너무 큰 객체/많은 수를 담으면 GC 압박
- 재시작 시 휘발 — 프로세스 재시작하면 캐시는 비어서 시작 (cold start)
한 줄 요약: "Redis보다 수백 배 빠른, 프로세스 내부 메모리에 데이터를 보관하는 스마트한 HashMap"
PlayBall 캐싱 맵
| 서비스 | Caffeine (로컬) | Redis (분산) |
|---|---|---|
| Auth-Guard | — | user-by-id (10분), auth-me (30초) |
| Queue | match-for-queue (1분) | — |
| Seat | match-exists, match-detail (10분), section-all, blocks-by-section-ids (1시간) | seat-groups-response (5초) |
| Order-Core | match-detail (10분) | user-by-id (10분), matches-list-response (30초) |