프로젝트명: YuGiOhDeck (유희왕 덱 빌더)
YuGiOhDeck은 유희왕 카드를 검색·선택하여 나만의 덱을 구성하고 공유할 수 있는 웹 애플리케이션입니다. URL에 덱 구성을 인코딩해 친구에게 간편하게 덱을 전달할 수 있도록 설계되었습니다.
Backend:
Frontend:
목적: 동시에 많은 사용자가 카드 검색/덱 구성 페이지를 사용 중일 때, 과도한 로드를 방지하고 안정적인 실시간 알림(broadcast)을 제공하기 위함.
구현:
/queue 엔드포인트 설정enter 메시지를 서버로 전송하고, 서버는 Redis의 ZSet에 사용자 세션 ID와 타임스탬프를 저장MAX_RUNNING) 체크 후, 초과 시 대기열로 이동broadcast() 호출로 클라이언트에 실시간 전송// Redis에 사용자 추가 예시
String runKey = RUNNING_PREFIX + qid;
if (redis.opsForZSet().score(runKey, user) == null && totalRunningSize() < MAX_RUNNING) {
redis.opsForZSet().add(runKey, user, Instant.now().toEpochMilli());
}
우선순위 대기열 관리:
vip, main)에 저장// 대기열 우선순위 계산 및 다음 사용자 프로모션
TypedTuple<String> vipTuple = firstWithScore(vipKey);
TypedTuple<String> mainTuple = firstWithScore(mainKey);
// VIP 우선 처리
if (vipScore <= mainScore) {
uid = vipTuple.getValue();
} else {
uid = mainTuple.getValue();
}
대기열 Vacancy 발생 시 자동 프로모션:
ENTER 메시지 전송promoteVip.forEach(uid -> notifier.sendToUser(uid, "{\"type\":\"ENTER\"}"));
promoteMain.forEach(uid -> notifier.sendToUser(uid, "{\"type\":\"ENTER\"}"));
TTL 설정: Redis Sorted Set의 각 사용자 엔트리마다 score로 타임스탬프를 저장하고, EXPIRE를 걸어 세션 만료 시 자동 삭제
비활성 사용자 제어 로직:
@Scheduled 어노테이션을 사용하여 10초 주기로 모든 RUNNING ZSet 점검TIMEOUT 메시지 전송 후 RUNNING ZSet에서 제거long cutoff = System.currentTimeMillis() - sessionTtlMillis();
Set<String> expired = redis.opsForZSet().rangeByScore(runKey, 0, cutoff);
expired.forEach(uid -> notifier.sendToUser(uid, "{\"type\":\"TIMEOUT\"}"));
Client 측 heartbeat 처리:
useCallback으로 ping 메시지를 주기적으로 서버에 전송하여 세션 유지const sendPing = useCallback(() => {
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.send('PING');
}
}, []);
기존:
@Query("""
SELECT c FROM CardModel c
WHERE (:frameType = '' OR c.frameType = :frameType)
AND (LOWER(REPLACE(c.korName,' ','')) LIKE CONCAT(:norm,'%')
OR LOWER(REPLACE(c.name,' ','')) LIKE CONCAT(:norm,'%'))
""")
Page<CardModel> searchByNameContaining(...);
| 지표 | 기존 검색 (Fuzzy) | Fulltext 검색 | 변화량 |
|---|---|---|---|
| 샘플 수 | 10 000 | 10 000 | – |
| 평균 응답 시간 | 1 635 ms | 1 112 ms | –523 ms (–32 %) |
| 중앙값 (Median) | 1 743 ms | 1 083 ms | –660 ms (–38 %) |
| 최소 응답 시간 | 53 ms | 26 ms | –27 ms |
| 최대 응답 시간 | 10 365 ms | 3 857 ms | –6 508 ms |
| 표준 편차 (Std.Dev) | 939 ms | 420 ms | –519 ms (–55 %) |
| 처리량 (Throughput) | 44 req/sec | 63 req/sec | +19 req/sec (+43 %) |
| 에러율 | 0 % | 0 % | – |
검색 응답 속도가 평균 32% 개선되었으며, 처리량도 43% 향상됨.
| 워커 수 | Throughput (RPS) | 평균 (ms) | 중앙값 P50 (ms) | P90 (ms) | P95 (ms) | P99 (ms) | Max (ms) | Std.Dev (ms) |
|---|---|---|---|---|---|---|---|---|
| 50 | 51.1 | 1 485 | 1 548 | 2 049 | 2 214 | 3 054 | 5 607 | 531 |
| 40 | 59.6 | 1 108 | 1 134 | 1 725 | 1 893 | 2 104 | 3 634 | 476 |
| 30 | 63.7 | 989 | 1 107 | 1 410 | 1 475 | 1 613 | 2 927 | 398 |
최종적으로 **2초 SLA (P95 ≤ 2 000 ms)**를 만족하면서도 평균 응답속도 1초 미만(989ms), 높은 처리량(63.7 RPS)을 기록한 30명 제한이 최적임을 확인.
| 역할 | 라이브러리/도구 |
|---|---|
| WebSocket 서버 | Spring WebSocket |
| 세션 관리 | Redis (TTL, ZSet) |
| 3D 렌더링 | react-three-fiber, drei |
| 스타일링 | styled-components |
| CI/CD | GitHub Actions, Maven |
| 검색 엔진 | MySQL Full-Text (Ngram) |
결론: YuGiOhDeck 프로젝트를 통해 실시간 대기열 시스템 설계, Redis 세션 및 우선순위 관리, MySQL Full-Text 기반 검색 개선 등 고급 백엔드 아키텍처 역량을 강화하였으며, 프론트엔드 3D 인터랙션과 서버 부하 제어를 모두 만족시키는 풀스택 서비스를 완성하였습니다.