스레드는 프로그램이 동시에 여러 일을 처리할 수 있게 해주는 작업 단위입니다.
스레드풀은 미리 준비해둔 스레드들의 집합입니다.
음식점 비유:
┌─────────────────────────────────────┐
│ 주방 (스레드풀) │
│ │
│ 👨🍳 👨🍳 👨🍳 (요리사 = 스레드) │
│ │
│ 📋 📋 📋 (대기 주문 = 큐) │
└─────────────────────────────────────┘
- 기본 요리사 2명 (corePoolSize = 2)
- 바쁠 때 최대 10명까지 (maxPoolSize = 10)
- 주문 50개까지 대기 가능 (queueCapacity = 50)
DungeonTalk 백엔드는 도메인별 분리된 스레드풀을 사용하여 비동기 작업을 처리합니다.
🎮 게임 매칭 상황:
유저A: "게임 매칭해주세요!"
유저B: "저도 매칭해주세요!"
AI게임: "AI 턴 처리해주세요!"
👨🍳 매칭 전용 주방(스레드풀):
- 요리사 2~10명이 이런 요청들만 처리
- 게임 관련 작업에만 집중
💓 WebSocket 연결 유지:
"클라이언트야, 살아있니?" (매 30초마다)
"네, 살아있어요!"
👨🍳 Heartbeat 전용 직원:
- 요리사 2명이 연결 확인만 담당
- 게임 처리와 완전 분리
❌ 만약 스레드풀을 분리하지 않았다면:
매칭 처리가 너무 느림 → 모든 스레드 점유 → WebSocket 연결 끊김 😱
✅ 스레드풀 분리 후:
매칭 처리가 느림 → 매칭용 스레드만 느림 → WebSocket은 정상 작동 😊
┌─────────────────────────────────────────────────────────────────┐
│ DungeonTalk Backend │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Web Layer │ │ WebSocket │ │
│ │ (Controllers) │ │ Layer │ │
│ └─────────┬────────┘ └─────────┬────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Service Layer │ │
│ │ │ │
│ │ ┌─────────────────┐ ┌────────────────────────────┐ │ │
│ │ │ MatchingService │ │ AiGameFlowService │ │ │
│ │ │ │ │ │ │ │
│ │ │ @Async( │ │ @Async( │ │ │
│ │ │ "matchingTask │ │ "matchingTaskExecutor") │ │ │
│ │ │ Executor") │ │ │ │ │
│ │ └─────────┬───────┘ └────────────┬───────────────┘ │ │
│ └────────────┼──────────────────────────────┼─────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Thread Pool Layer │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ matchingTaskExecutor │ │ │
│ │ │ ┌─────────────────────────────────────────────────┐ │ │ │
│ │ │ │ ThreadPoolTaskExecutor Configuration │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ • Core Threads: 2 │ │ │ │
│ │ │ │ • Max Threads: 10 │ │ │ │
│ │ │ │ • Queue: 50 │ │ │ │
│ │ │ │ • KeepAlive: 60s │ │ │ │
│ │ │ │ • Name: "Matching-Async-" │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ RejectedExecutionHandler: │ │ │ │
│ │ │ │ └─► Fallback to Main Thread │ │ │ │
│ │ │ └─────────────────────────────────────────────────┘ │ │ │
│ │ └──────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ stompTaskScheduler │ │ │
│ │ │ ┌─────────────────────────────────────────────────┐ │ │ │
│ │ │ │ ThreadPoolTaskScheduler Configuration │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ • Pool Size: 2 │ │ │ │
│ │ │ │ • Name: "stomp-heartbeat-" │ │ │ │
│ │ │ │ • RemoveOnCancel: true │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ Purpose: STOMP Heartbeat Only │ │ │ │
│ │ │ └─────────────────────────────────────────────────┘ │ │ │
│ │ └──────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
매칭과 AI 턴 처리는 같은 스레드풀을 공유하여 리소스를 효율적으로 사용합니다.
WebSocket Heartbeat는 별도 스케줄러로 실시간 연결을 안정적으로 유지합니다.
🎮 사용자: "매칭해주세요!" 클릭
│
▼ HTTP 요청 전송
┌─────────────┐
│ Controller │ ──── "어? 매칭 요청이 왔네!"
└─────────────┘
│
▼ MatchingService 호출
┌──────────────────┐
│ MatchingService │ ──── "@Async로 비동기 처리해야지!"
└─────────┬────────┘
│ @Async("matchingTaskExecutor")
▼ 스레드풀로 작업 위임
┌─────────────────────────┐
│ matchingTaskExecutor │ ── "매칭 전용 주방이야!"
│ │
│ 👨🍳 👨🍳 (T1)(T2) │ ── "지금 2명 대기중"
│ │
│ 📋📋📋 Queue(50개 대기가능) │ ── "주문 대기열"
└─────────────────────────┘
│
▼ 실제 매칭 작업 수행
┌─────────────────────────┐
│ 매칭 처리 로직 │
│ • Redis에서 대기자 찾기 │ ── "다른 플레이어 있나?"
│ • 유저 상태 업데이트 │ ── "매칭중 상태로 변경"
│ • 게임방 생성 │ ── "방 만들어서 입장!"
└─────────────────────────┘
💡 핵심 포인트: Controller는 바로 응답하고, 실제 매칭은 백그라운드에서 처리!
Game Event
│
▼
┌──────────────┐ Event ┌───────────────────┐
│ EventListener│ ────────► │ AiGameFlowService │
└──────────────┘ └─────────┬─────────┘
│ @Async("matchingTaskExecutor")
▼
┌─────────────────────────┐
│ matchingTaskExecutor │ (Same pool)
│ │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ T1 │ │ T2 │ │ ... │ │
│ └─────┘ └─────┘ └─────┘ │
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ AI Turn Processing │
│ • AI API Call │
│ • Response Processing │
│ • WebSocket Message │
└─────────────────────────┘
WebSocket Connection
│
▼
┌─────────────────┐ ┌────────────────────┐
│ WebSocketConfig │────►│ SimpleBroker │
└─────────────────┘ └─────────┬──────────┘
│ setTaskScheduler(stompTaskScheduler)
▼
┌─────────────────────────┐
│ stompTaskScheduler │
│ │
│ ┌─────┐ ┌─────┐ │
│ │ HB1 │ │ HB2 │ │
│ └─────┘ └─────┘ │
│ │
│ Heartbeat: 30s interval │
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ Client ←→ Server │
│ Heartbeat Messages │
└─────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Thread Pool 분리 전략 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
│ │ Matching │ │ WebSocket │ │ Future │ │
│ │ Domain │ │ Infrastructure│ │ Extension │ │
│ │ │ │ │ │ │ │
│ │ matchingTask │ │ stompTask │ │ aiChatTask │ │
│ │ Executor │ │ Scheduler │ │ Executor │ │
│ │ │ │ │ │ (Planned) │ │
│ │ • 매칭 처리 │ │ • Heartbeat │ │ • AI 채팅 │ │
│ │ • AI 게임 턴 │ │ • Connection │ │ • 응답 생성 │ │
│ │ │ │ Management │ │ │ │
│ └─────────────────┘ └─────────────────┘ └─────────────┘ │
│ │
│ 장점: │
│ • 도메인별 성능 최적화 │
│ • 장애 격리 │
│ • 독립적 모니터링 │
│ │
└─────────────────────────────────────────────────────────────┘
스프링의 @ConfigurationProperties를 활용해 타입 안전한 설정 관리를 구현했습니다.
application.yml
│
▼
┌─────────────────────────────────────────────┐
│ MatchingProperties │
│ │
│ @ConfigurationProperties │
│ (prefix = "app.matching") │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ ThreadPool │ │
│ │ │ │
│ │ • corePoolSize: 2 │ │
│ │ • maxPoolSize: 10 │ │
│ │ • queueCapacity: 50 │ │
│ │ • keepAliveSeconds: 60 │ │
│ └─────────────────────────────────────────┘ │
└──────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ MatchingAsyncConfig │
│ │
│ @Configuration │
│ @EnableAsync │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ @Bean("matchingTaskExecutor") │ │
│ │ │ │
│ │ ThreadPoolTaskExecutor 생성 │ │
│ │ • Properties 값 주입 │ │
│ │ • RejectedExecutionHandler 설정 │ │
│ │ • 로깅 및 초기화 │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
🏪 던전톡 매칭 전용 주방:
👨🍳👨🍳 기본 요리사 2명 (Core Pool Size = 2)
→ "평상시에는 이 정도면 충분해!"
👨🍳👨🍳👨🍳👨🍳👨🍳👨🍳👨🍳👨🍳👨🍳👨🍳 최대 10명 (Max Pool Size = 10)
→ "점심시간처럼 바쁠 때는 임시 직원까지 동원!"
📋📋📋📋📋...(50개) 주문 대기판 (Queue Capacity = 50)
→ "주문이 몰려도 50개까지는 대기 가능!"
🚨 주방이 가득 찬다면? (RejectedExecutionHandler)
→ "사장님이 직접 요리하기!" (메인 스레드에서 처리)
┌─────────────────────────────────────────────────────────────────┐
│ 튜닝 매트릭스 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Core Threads (2) ←────┐ │
│ │ │
│ • CPU 집약적 작업 고려 │ ┌─── Queue Capacity (50) │
│ • 기본 처리량 확보 │ │ │
│ │ │ • 버스트 트래픽 대응 │
│ ▼ ▼ • 메모리 사용량 제한 │
│ │
│ ┌─────────────────────────────┐ │
│ │ Thread Pool Behavior │ │
│ │ │ │
│ │ Low Load: Core만 활성 │ │
│ │ Med Load: Queue 활용 │ │
│ │ High Load: Max까지 확장 │ │
│ │ Overflow: Main Thread │ │
│ └─────────────────────────────┘ │
│ │ ▲ │
│ │ │ │
│ ▼ └─── Max Threads (10) │
│ │
│ Keep Alive (60s) ────────────────┐ │
│ │ │
│ • 유휴 스레드 정리 │ │
│ • 메모리 효율성 │ │
│ ▼ │
└─────────────────────────────────────────────────────────────────┘
Application Startup
│
▼
┌─────────────────────────────────────────┐
│ "매칭 전용 스레드 풀 초기화 완료: │
│ core=2, max=10, queue=50" │
└─────────────────────────────────────────┘
│
▼ (Runtime)
┌─────────────────────────────────────────┐
│ "매칭 처리 스레드 풀이 가득 참. │
│ 요청 거부됨" │
└─────────────────────────────────────────┘
│
▼ (메트릭 수집 가능 지점)
┌─────────────────────────────────────────┐
│ • Active Thread Count │
│ • Queue Size │
│ • Completed Task Count │
│ • Rejected Task Count │
└─────────────────────────────────────────┘