스레드풀 아키텍처

MJ·2025년 8월 23일

스레드풀 아키텍처

🔰 스레드풀이 뭔가요?

스레드(Thread)란?

스레드는 프로그램이 동시에 여러 일을 처리할 수 있게 해주는 작업 단위입니다.

  • 음식점에서 요리사 1명 = 스레드 1개라고 생각하면 됩니다
  • 요리사가 1명이면 주문을 하나씩만 처리 가능 (동기)
  • 요리사가 여러 명이면 여러 주문을 동시에 처리 가능 (비동기)

스레드풀(ThreadPool)이란?

스레드풀은 미리 준비해둔 스레드들의 집합입니다.

음식점 비유:
┌─────────────────────────────────────┐
│        주방 (스레드풀)               │
│                                     │
│ 👨‍🍳 👨‍🍳 👨‍🍳 (요리사 = 스레드)      │
│                                     │
│ 📋 📋 📋 (대기 주문 = 큐)           │
└─────────────────────────────────────┘

- 기본 요리사 2명 (corePoolSize = 2)
- 바쁠 때 최대 10명까지 (maxPoolSize = 10)
- 주문 50개까지 대기 가능 (queueCapacity = 50)

왜 스레드풀을 사용하나요?

  1. 효율성: 스레드 생성/삭제 비용 절약
  2. 안정성: 시스템 리소스 보호 (무한정 스레드 생성 방지)
  3. 제어: 동시 실행 작업 수 제한

개요

DungeonTalk 백엔드는 도메인별 분리된 스레드풀을 사용하여 비동기 작업을 처리합니다.

핵심 개념 (실제 예시로 이해하기)

1. 매칭 전용 스레드풀

🎮 게임 매칭 상황:
유저A: "게임 매칭해주세요!"
유저B: "저도 매칭해주세요!" 
AI게임: "AI 턴 처리해주세요!"

👨‍🍳 매칭 전용 주방(스레드풀):
- 요리사 2~10명이 이런 요청들만 처리
- 게임 관련 작업에만 집중

2. Heartbeat 전용 스케줄러

💓 WebSocket 연결 유지:
"클라이언트야, 살아있니?" (매 30초마다)
"네, 살아있어요!" 

👨‍🍳 Heartbeat 전용 직원:
- 요리사 2명이 연결 확인만 담당  
- 게임 처리와 완전 분리

3. 장애 격리 (진짜 중요한 개념!)

❌ 만약 스레드풀을 분리하지 않았다면:
매칭 처리가 너무 느림 → 모든 스레드 점유 → WebSocket 연결 끊김 😱

✅ 스레드풀 분리 후:
매칭 처리가 느림 → 매칭용 스레드만 느림 → WebSocket은 정상 작동 😊

왜 이렇게 설계했나?

  1. 안정성: 매칭 처리가 느려져도 WebSocket 연결은 유지됨
  2. 확장성: 부하가 높은 기능만 독립적으로 스케일업 가능
  3. 모니터링: 도메인별 성능 추적 및 문제 파악 용이

전체 아키텍처 다이어그램

┌─────────────────────────────────────────────────────────────────┐
│                    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는 별도 스케줄러로 실시간 연결을 안정적으로 유지합니다.

단계별 실행 과정

1. 매칭 처리 흐름 (실제 상황)

🎮 사용자: "매칭해주세요!" 클릭
      │
      ▼ HTTP 요청 전송
┌─────────────┐    
│ Controller  │ ──── "어? 매칭 요청이 왔네!" 
└─────────────┘           
      │                          
      ▼ MatchingService 호출
┌──────────────────┐
│ MatchingService  │ ──── "@Async로 비동기 처리해야지!"
└─────────┬────────┘      
          │ @Async("matchingTaskExecutor")
          ▼ 스레드풀로 작업 위임
┌─────────────────────────┐
│  matchingTaskExecutor   │ ── "매칭 전용 주방이야!"
│                         │
│ 👨‍🍳 👨‍🍳 (T1)(T2)        │ ── "지금 2명 대기중"  
│                         │
│ 📋📋📋 Queue(50개 대기가능) │ ── "주문 대기열"
└─────────────────────────┘
          │
          ▼ 실제 매칭 작업 수행
┌─────────────────────────┐
│    매칭 처리 로직        │
│  • Redis에서 대기자 찾기 │ ── "다른 플레이어 있나?"
│  • 유저 상태 업데이트    │ ── "매칭중 상태로 변경"  
│  • 게임방 생성          │ ── "방 만들어서 입장!"
└─────────────────────────┘

💡 핵심 포인트: Controller는 바로 응답하고, 실제 매칭은 백그라운드에서 처리!

2. AI 게임 턴 처리 흐름

Game Event
      │
      ▼
┌──────────────┐   Event   ┌───────────────────┐
│ EventListener│ ────────► │ AiGameFlowService │
└──────────────┘           └─────────┬─────────┘
                                     │ @Async("matchingTaskExecutor")
                                     ▼
                           ┌─────────────────────────┐
                           │  matchingTaskExecutor   │ (Same pool)
                           │                         │
                           │ ┌─────┐ ┌─────┐ ┌─────┐ │
                           │ │ T1  │ │ T2  │ │ ... │ │
                           │ └─────┘ └─────┘ └─────┘ │
                           └─────────────────────────┘
                                     │
                                     ▼
                           ┌─────────────────────────┐
                           │   AI Turn Processing    │
                           │  • AI API Call          │
                           │  • Response Processing  │
                           │  • WebSocket Message    │
                           └─────────────────────────┘

3. WebSocket Heartbeat 흐름

WebSocket Connection
         │
         ▼
┌─────────────────┐     ┌────────────────────┐
│ WebSocketConfig │────►│ SimpleBroker       │
└─────────────────┘     └─────────┬──────────┘
                                  │ setTaskScheduler(stompTaskScheduler)
                                  ▼
                        ┌─────────────────────────┐
                        │   stompTaskScheduler    │
                        │                         │
                        │ ┌─────┐ ┌─────┐         │
                        │ │ HB1 │ │ HB2 │         │
                        │ └─────┘ └─────┘         │
                        │                         │
                        │ Heartbeat: 30s interval │
                        └─────────────────────────┘
                                  │
                                  ▼
                        ┌─────────────────────────┐
                        │  Client ←→ Server       │
                        │   Heartbeat Messages    │
                        └─────────────────────────┘

스레드풀 분리 전략

핵심 아이디어: "관심사 분리"

  • 매칭 도메인: 사용자 매칭, AI 게임 처리 → 비즈니스 로직 중심
  • 인프라 레이어: WebSocket 연결 유지 → 기술적 요구사항 중심

도메인별 격리

┌─────────────────────────────────────────────────────────────┐
│                     Thread Pool 분리 전략                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────┐  │
│  │   Matching      │  │   WebSocket     │  │   Future    │  │
│  │   Domain        │  │   Infrastructure│  │  Extension  │  │
│  │                 │  │                 │  │             │  │
│  │ matchingTask    │  │ stompTask       │  │ aiChatTask  │  │
│  │ Executor        │  │ Scheduler       │  │ Executor    │  │
│  │                 │  │                 │  │ (Planned)   │  │
│  │ • 매칭 처리      │  │ • Heartbeat     │  │ • AI 채팅    │  │
│  │ • AI 게임 턴     │  │ • Connection    │  │ • 응답 생성  │  │
│  │                 │  │   Management    │  │             │  │
│  └─────────────────┘  └─────────────────┘  └─────────────┘  │
│                                                             │
│  장점:                                                       │
│  • 도메인별 성능 최적화                                        │
│  • 장애 격리                                                │
│  • 독립적 모니터링                                           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

설정 관리 구조

설정의 흐름: YAML → Properties → Bean 생성

스프링의 @ConfigurationProperties를 활용해 타입 안전한 설정 관리를 구현했습니다.

Configuration 계층

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 2개: 매칭은 보통 몇 초 내 처리. CPU 과부하 방지
  • Max 10개: 서버 메모리를 너무 많이 쓰면 안 되니까!
  • Queue 50개: 동시접속자 급증해도 버틸 수 있게
  • Fallback: 절대 매칭 요청을 버리지 않겠다는 의지!

스레드풀 크기 결정 기준

┌─────────────────────────────────────────────────────────────────┐
│                     튜닝 매트릭스                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│ 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                  │
└─────────────────────────────────────────┘
profile
..

0개의 댓글