# MMORPG 아키텍처 적합성 분석 보고서 ## 1. 분석 목적 현재 서버 구조가 **다수의 플레이어가 동시 접속하여 실시간으로 상호작용하는 MMORPG**에 적합한지 평가하고, 부족한 부분과 개선 방향을 제시한다. --- ## 2. MMORPG 서버의 핵심 요구사항 | 요구사항 | 설명 | |----------|------| | 대규모 동시접속 (CCU) | 수백~수천 명이 같은 월드에서 실시간 플레이 | | 저지연 패킷 처리 | 위치/액션 동기화는 50ms 이내 응답 필요 | | 관심 영역 관리 (AOI) | 내 주변 플레이어만 패킷을 주고받아야 대역폭 절약 | | 게임 루프 (Tick) | 고정 주기(예: 20~60Hz)로 월드 상태를 업데이트 | | 존/채널 분리 | 지역별 부하 분산 및 인스턴스 관리 | | 엔티티 관리 | 플레이어, NPC, 몬스터 등의 상태를 중앙에서 관리 | | 서버 간 통신 | 로그인/로비/게임/채팅 등 역할별 서버 분리 | | 장애 복구 | 서버 크래시 시 세션 복구, 데이터 무결성 보장 | --- ## 3. 현재 구조 평가 ### 3.1 네트워크 계층 **현재 구조:** ``` 단일 ServerBase → 단일 NetManager → 단일 스레드 PollEvents() ``` | 항목 | 현재 상태 | MMORPG 요구 | 평가 | |------|-----------|-------------|------| | 프로토콜 | UDP (LiteNetLib) | UDP 기반 필수 | **적합** | | 직렬화 | ProtoBuf | 고성능 바이너리 직렬화 | **적합** | | 폴링 모델 | 단일 스레드 `while + Task.Delay(1)` | 멀티스레드 or 고성능 이벤트 루프 | **부적합** | | 패킷 라우팅 | type 기반 if 분기 | 핸들러 매핑 테이블 + 디스패처 | **부적합** | **문제점:** - **단일 스레드 병목**: `PollEvents()`가 단일 스레드에서 모든 패킷을 처리한다. CCU 500 이상에서 패킷 큐가 쌓이면 처리 지연이 발생한다. MMORPG에서 플레이어 100명이 초당 10패킷을 보내면 초당 1,000패킷인데, `HandlePacket` 내부에서 DB 조회 같은 블로킹 작업이 끼면 전체 루프가 멈춘다. - **패킷 디스패처 없음**: `HandlePacket(peer, hashKey, type, payload)` 하나로 모든 게임 패킷을 처리해야 한다. 패킷 종류가 17개인 현재도 거대한 switch문이 필요하며, 패킷이 늘어날수록 유지보수가 어려워진다. - **`Task.Delay(1)` 해상도**: Windows에서 실제로 ~15ms 간격으로 폴링된다. 60Hz 게임 루프가 필요한 MMORPG에서는 16.6ms마다 정확한 틱이 필요한데, 현재 구조로는 불가능하다. --- ### 3.2 세션 관리 **현재 구조:** ``` Dictionary sessions (hashKey → peer) Dictionary pendingPeers (peerId → peer) ``` | 항목 | 현재 상태 | MMORPG 요구 | 평가 | |------|-----------|-------------|------| | 자료구조 | 일반 Dictionary | ConcurrentDictionary 또는 락 기반 | **위험** | | 인증 | 8byte hashKey raw 전송 | JWT/OAuth + 토큰 갱신 | **부적합** | | 세션 정보 | hashKey + NetPeer만 | 플레이어 ID, 존, 위치, 상태 등 | **부족** | | 재연결 | hashKey 기반 피어 교체 | 세션 상태 전체 복구 | **부분 구현** | **문제점:** - **인증 보안 취약**: 8바이트 hashKey를 평문으로 보내면, 패킷 스니핑으로 다른 사람의 세션을 탈취할 수 있다. MMORPG는 JWT 같은 시간 제한 토큰 + 서버 측 검증이 필수다. - **세션에 게임 상태 없음**: `Session` 클래스에 `HashKey`와 `Peer`만 있다. MMORPG에서는 세션이 플레이어의 현재 존, 위치, 파티, 인벤토리 캐시 등을 들고 있어야 패킷 처리 시 매번 DB를 조회하지 않는다. - **재연결 시 상태 미복구**: WiFi→LTE 전환 시 피어만 교체하고 게임 상태(존, 위치, 버프 등)를 클라이언트에 재전송하는 로직이 없다. --- ### 3.3 게임 루프 & 월드 관리 **현재 구조:** ``` 없음. GameServer 클래스는 빈 껍데기. ``` | 항목 | 현재 상태 | MMORPG 요구 | 평가 | |------|-----------|-------------|------| | 게임 루프 | 없음 | 고정 틱 (20~60Hz) | **미구현** | | 존/맵 관리 | 없음 | Zone/Channel/Instance 분리 | **미구현** | | AOI (관심 영역) | 없음 | Grid/Quadtree 기반 영역 관리 | **미구현** | | 엔티티 시스템 | 없음 | Player/NPC/Monster 상태 관리 | **미구현** | | 물리/충돌 | 없음 | 서버 권위 이동 검증 | **미구현** | **문제점:** 이것이 가장 큰 갭이다. MMORPG 서버의 핵심인 **게임 루프**가 존재하지 않는다. - **게임 루프 없음**: 현재는 "클라이언트가 패킷을 보내면 서버가 반응"하는 순수 리액티브 구조다. MMORPG는 클라이언트 입력이 없어도 서버가 자체적으로 NPC AI, 몬스터 이동, 버프 타이머, 리스폰 등을 주기적으로 처리해야 한다. - **AOI 없음**: 현재 `Broadcast()`는 **모든 접속자**에게 패킷을 보낸다. 플레이어 100명이 모두 움직이면 초당 `100 × 100 = 10,000`패킷이 발생한다. 1,000명이면 `1,000,000`패킷이다. AOI 없이는 CCU 50만 넘어도 대역폭이 폭발한다. - **존/채널 없음**: 모든 플레이어가 하나의 `sessions` Dictionary에 평면적으로 존재한다. "마을에 있는 플레이어"와 "던전에 있는 플레이어"를 구분할 방법이 없다. --- ### 3.4 데이터베이스 계층 **현재 구조:** ``` Handler → Service → Repository(Dapper) → MySQL 매 요청마다 새 커넥션 (풀링은 MySQL 드라이버에 위임) ``` | 항목 | 현재 상태 | MMORPG 요구 | 평가 | |------|-----------|-------------|------| | ORM | Dapper (경량) | 경량 ORM 적합 | **적합** | | 커넥션 풀링 | MySQL 드라이버 위임 | 명시적 풀 관리 | **보통** | | 캐싱 | 없음 | Redis/인메모리 캐시 필수 | **부적합** | | 비동기 | async/await | 논블로킹 필수 | **적합** | **문제점:** - **캐시 계층 없음**: 플레이어가 스킬을 쓸 때마다 DB에서 스탯을 읽으면 게임이 안 된다. MMORPG는 로그인 시 플레이어 데이터를 메모리에 올리고, 주기적으로 DB에 저장(Write-Back)하는 구조가 필수다. - **게임 루프에서 DB 호출 위험**: 현재는 패킷 핸들러에서 직접 DB를 호출하는 구조인데, 게임 루프 스레드에서 DB await를 하면 틱 지연이 발생한다. 게임 로직과 DB I/O는 반드시 분리해야 한다. --- ### 3.5 서버 구조 (스케일링) **현재 구조:** ``` 단일 프로세스, 단일 서버 ``` | 항목 | 현재 상태 | MMORPG 요구 | 평가 | |------|-----------|-------------|------| | 서버 분리 | 모놀리식 단일 서버 | 로그인/로비/게임/채팅 분리 | **부적합** | | 수평 확장 | 불가 | 존별 서버 분산 | **부적합** | | 메시지 큐 | 없음 | Redis Pub/Sub, RabbitMQ 등 | **미구현** | | 로드밸런싱 | 없음 | 게이트웨이 + 존 서버 라우팅 | **미구현** | **문제점:** - **단일 서버 한계**: 하나의 `ServerBase` 인스턴스가 인증, 에코, 게임 패킷을 모두 처리한다. CCU가 늘어나면 서버를 추가할 방법이 없다. - **서버 간 통신 없음**: "A 서버의 플레이어가 B 서버의 플레이어에게 귓속말"이 불가능하다. 파티, 길드, 거래 등 크로스 존 기능을 구현할 수 없다. --- ## 4. 종합 평가 ### 적합성 점수 | 영역 | 점수 (10점) | 비고 | |------|:-----------:|------| | 네트워크 프로토콜 | 8 | UDP + ProtoBuf 조합 우수 | | 패킷 처리 파이프라인 | 3 | 단일 스레드, 디스패처 없음 | | 세션 / 인증 | 3 | 보안 취약, 게임 상태 없음 | | 게임 루프 | 0 | 미구현 | | 월드 / 존 관리 | 0 | 미구현 | | AOI (관심 영역) | 0 | 미구현 | | 엔티티 시스템 | 0 | 미구현 | | DB / 캐시 | 4 | Dapper 좋으나 캐시 없음 | | 서버 확장성 | 1 | 단일 프로세스 모놀리식 | | **종합** | **2.1 / 10** | | ### 결론 > 현재 구조는 **에코 서버 / 소규모 매칭 게임 (10~20명)** 수준에 적합하며, > **MMORPG로 사용하기에는 핵심 시스템 대부분이 미구현** 상태이다. > > 네트워크 기반(LiteNetLib + ProtoBuf)과 DB 레이어(Dapper + Repository 패턴)는 > 좋은 출발점이지만, 그 위에 올라가야 할 게임 서버 계층이 빠져 있다. --- ## 5. MMORPG 전환을 위한 로드맵 ### Phase 1: 게임 루프 & 엔티티 시스템 (최우선) ``` 현재: 클라이언트 패킷 → ServerBase → GameServer(빈 껍데기) 목표: 클라이언트 패킷 → 입력 큐 → GameLoop(고정 틱) → 월드 상태 업데이트 → 브로드캐스트 ``` **구현 항목:** 1. **고정 틱 게임 루프** - `Stopwatch` 기반 정밀 타이머로 50ms(20Hz) 또는 33ms(30Hz) 간격 틱 - 패킷 수신은 입력 큐에 적재, 게임 루프에서 꺼내서 처리 - 네트워크 I/O와 게임 로직 스레드 분리 2. **엔티티 관리자** - `GamePlayer`, `GameNpc`, `GameMonster` 등 인게임 엔티티 클래스 - 위치, 상태, 스탯을 서버 메모리에서 관리 - 로그인 시 DB → 메모리 로드, 주기적으로 메모리 → DB 저장 3. **패킷 디스패처** - `Dictionary>` 핸들러 매핑 테이블 - 패킷 타입별 핸들러 자동 등록 (어트리뷰트 또는 초기화 시 등록) ``` 예시 구조: GameLoop ├── ProcessInputQueue() // 클라이언트 패킷 처리 ├── UpdateNpcAI() // NPC 행동 업데이트 ├── UpdateBuffTimers() // 버프/디버프 타이머 ├── UpdateRespawns() // 몬스터 리스폰 ├── BroadcastWorldState() // 변경된 상태만 클라이언트에 전송 └── SaveDirtyEntities() // 변경된 엔티티 DB 저장 (N틱마다) ``` ### Phase 2: 존/채널 & AOI **구현 항목:** 1. **존 시스템** - `Zone` 클래스: 해당 존의 플레이어/NPC/몬스터 목록 관리 - 존 진입/퇴장 시 엔티티 이동 - 인스턴스 던전은 동적 Zone 생성/파괴 2. **AOI (Area of Interest)** - 그리드 기반 관심 영역: 맵을 NxN 셀로 나누고, 플레이어가 속한 셀 + 인접 셀만 패킷 전송 - `Broadcast()` → `BroadcastToNearby(position, radius, data)`로 교체 - CCU 1,000명 기준 패킷 수: 1,000,000 → ~50,000 (95% 감소) ``` 예시: ┌───┬───┬───┐ │ │ B │ │ B는 A의 AOI 범위 안 → 패킷 수신 ├───┼───┼───┤ │ │ A │ │ A가 이동 패킷 발신 ├───┼───┼───┤ │ │ │ C │ C는 AOI 범위 밖 → 패킷 안 받음 └───┴───┴───┘ ``` ### Phase 3: 인증 강화 & 세션 확장 **구현 항목:** 1. **JWT 기반 인증** - 로그인 서버에서 JWT 발급 → 게임 서버에서 검증 - hashKey 대신 토큰 기반 인증, 만료/갱신 처리 2. **세션 확장** ```csharp public class GameSession { public long AccountId { get; init; } public NetPeer Peer { get; set; } public GamePlayer? Player { get; set; } // 인게임 엔티티 참조 public Zone? CurrentZone { get; set; } // 현재 존 public DateTime LastActivity { get; set; } // AFK 감지 } ``` 3. **재연결 상태 복구**: 재연결 시 현재 존, 위치, 파티 상태를 클라이언트에 재전송 ### Phase 4: 캐시 & DB 최적화 **구현 항목:** 1. **인메모리 캐시 도입** - 접속 중인 플레이어 데이터는 메모리에 상주 - Write-Back 패턴: 변경 사항을 큐에 쌓고 N초마다 일괄 DB 저장 - 아이템/스킬 테이블 등 정적 데이터는 서버 시작 시 로드 2. **DB I/O 분리** - 게임 루프 스레드에서 직접 DB 호출 금지 - 별도 DB Worker 스레드에서 저장 큐 처리 - 긴급 저장(거래, 결제)만 즉시 처리 ### Phase 5: 서버 분산 아키텍처 **구현 항목:** 1. **서버 역할 분리** ``` 클라이언트 ↓ 게이트웨이 서버 (인증 + 라우팅) ├── 로비 서버 (매칭, 파티, 채팅) ├── 게임 서버 A (마을 존) ├── 게임 서버 B (필드 존) ├── 게임 서버 C (던전 인스턴스) └── DB/캐시 서버 (Redis + MySQL) ``` 2. **서버 간 메시지 큐** - Redis Pub/Sub 또는 RabbitMQ로 서버 간 통신 - 크로스 존 기능: 귓속말, 길드, 파티 초대, 거래 3. **수평 확장** - 존 단위로 게임 서버 인스턴스 추가 - 게이트웨이가 플레이어를 적절한 존 서버로 라우팅 --- ## 6. 현재 코드에서 재사용 가능한 부분 현재 코드가 완전히 쓸모없는 것은 아니다. 아래 부분은 그대로 또는 약간의 수정으로 재사용 가능하다. | 컴포넌트 | 재사용성 | 비고 | |----------|:--------:|------| | LiteNetLib 기반 네트워크 | **높음** | UDP 전송 계층으로 그대로 사용 | | ProtoBuf 패킷 정의 | **높음** | PacketBody.cs의 17개 패킷 구조 재사용 | | PacketSerializer | **높음** | 직렬화/역직렬화 로직 그대로 사용 | | ARepository + Dapper | **높음** | DB 계층 그대로 사용 | | ServerBase 인증 흐름 | **보통** | JWT로 교체하되 pending → auth 흐름은 유지 | | Session/재연결 로직 | **보통** | 확장 후 재사용 | | Docker/compose 설정 | **높음** | 배포 파이프라인 그대로 사용 | --- ## 7. 권장 우선순위 요약 ``` [즉시] Phase 1: 게임 루프 + 엔티티 시스템 + 패킷 디스패처 → 이것 없이는 어떤 게임 로직도 구현 불가 [단기] Phase 2: 존/채널 + AOI → CCU 50명만 넘어도 Broadcast 폭발 [중기] Phase 3: 인증 강화 + 세션 확장 → 보안과 재연결 안정성 [중기] Phase 4: 캐시 + DB 분리 → 성능 병목 해소 [장기] Phase 5: 서버 분산 → CCU 1,000+ 대응 ```