wargame 프로젝트 자체는 추가로 생각난 신기능 개발하는거 이외에 서버, 인프라적 기능은 대부분 완료된 상태다.
전적 검색 서비스나 통계 지표 분석 관련 기능을 만들어 둔게 있어서 프론트엔드를 개발해봤다.
react, vite.js를 이용해서 간단하게 프론트엔드를 구현하고, vercel을 통해 배포했다.
메인 페이지에서 원래 인게임 소개 영상(youtube), 플레이 이미지를 넣어서 간단한 게임 소개를 가미할려고 했다.
하지만 지금은 그딴거 없고 테스트 목적으로 올린거니 대충 느좋 플리 아무거나 집어넣었다.


백엔드는 클라우드 환경에서 테스트를 매번 생성하고자 해서, 서버가 안켜져있는 경우에는 더미를 생성하도록 처리했다.
추가된 게임 결과 데이터에 대해서 게임 서버에서부터 시간대별 팀 골드량같은 지표를 수집하도록 처리한 상태이다.
더미 History 내역에 있는 챔프나 아이템 아이콘이 전부 ?로 표시되고 있는데, 아직 이미지를 등록하지 않아서 그렇다.
추후 클라이언트 컨텐츠 업데이트와 함께 품질이 개선될 거라 본다.
새로운 소식 - 그동안 추가된 게임 내 기능
- 게임서버는 병종 처치시 CS, 골드 데이터를 수집합니다. (휘하 병종의 처치시도 포함)
- 실력 점수(Elo) 최상위의 10명에 대해서 매주 일요일 22시마다 랭커 티어를 갱신합니다.
- 챔프 픽창에서는 팀원간의 채팅만 가능하지만, 게임 안에서는 이제 전체채팅, 팀채팅을 Tab키로 구분해서 사용할 수 있습니다.
- 항복 투표 - 게임 시작시부터 투표를 진행할 수 있으며, 15초 동안 투표를 통해 조기 항복 여부를 결정할 수 있습니다.
- 클라이언트의 시야 처리 - 피아 식별과 팀원들과 시야를 공유하는 전장의 안개(War of Fog)를 구현했습니다.
비즈니스 목적과 상황에 따른 Redis 도입 논의
Player 서비스는 원래 Redis를 사용하지 않고 있고, 굳이 각 사용자의 정보를 캐싱하기 위한 목적으로만 새로 추가하기 부담스럽다 생각했다.
다른 서비스들은 비용, 리소스 부담의 이유로 캐싱이 필요했지만, Player 서비스같은 경우는 CQRS의 읽기 모델(MySQL)에서만 가져오기에 조회에 별다른 부담도 없다.
- 통계 데이터: DynamoDB 온디맨드 비용 문제
- 전적 History: elasticsearch에서 조회시 메모리 부하
전적 서비스에서 보여지는 영역 중 일부에 대해서 캐싱 전략을 세워서 일정 기간(TTL)마다 자료를 갱신하도록 처리했다.
- 고정 컨텐츠: 최상위 랭커 순위, 현재 가장 승률이 높은 5개 챔프
- 메인 페이지의 전체 챔프 승률 통계
- 각 사용자의 게임 전적 데이터와 지표 분석
이벤트 소싱에 대한 오해와 CQRS 패턴 적용시 읽기 동기화 문제 해결
result-query 서비스에서는 읽기 모델과 쓰기 모델을 DynamoDB에서 분리한 상태로 사용했었다.
근데 player 서비스는 시스템 상태 변화가 빈번한 Elo 변화에 대해서 Player Aggregate를 이벤트 소싱 방식으로 해결했다.
Axon Event Store(write)와 MySQL(read)를 사용하면서 서로간의 상태 동기화를 언제 진행할지에 대해서 어려움을 겪었다.
해결하고 나니.. 그냥 이벤트 소싱에 대한 이해가 부족했던 것 뿐이지만 공유하고자 한다.
이벤트 발생하고 나서 읽기 모델에 기록하면, 그냥 읽기 모델만 쓰는거랑 무슨 차이인건가?
Axon Framework에서 이벤트는 비동기로 처리되고, 읽기 모델의 조회는 실시간으로 이뤄진다.
당연히 읽기 모델과 쓰기 모델(Event Store)간의 동기화가 실시간으로 이뤄지지 않는다. (Near-realtime)
- @EventHandler를 통해서 DB 갱신해버리니 이벤트 복원할때 MySQL에 파바바 변경된다.
원천 데이터의 출처(Source of Truth)가 Event Store이고, 읽기 모델은 단순히 투영된 결과(Projection)일 뿐이다.
- 이벤트 재처리, 롤백, 감사 로그, 복원, 다른 리스너 반응 가능
읽기 모델에 기록한다는 "동작 결과"는 같지만, 여기서는 변경의 이유와 재구성 가능성이 담긴 이벤트가 중심
EventHandler는 읽기 모델을 갱신하는 Projection으로, 이벤트 스트림에 기반해서 상태를 투영할 뿐이다.
이때 Side Effect(외부 호출, 알림 전송 등)는 절대 없애야 한다. 이벤트 복원시 계속 발생하기 때문이다.
- 멱등성(Idempotent) 보장: 같은 이벤트에 대해서 여러 번 요청이 와도 같은 결과를 내야한다.
이 파바바는 Side Effect에 대해서만 금지해야하는데,, 이벤트 복원에 대해서 오해가 있었다.
이벤트 복원은 이벤트 스토어의 Aggregate만을 복원하면서 실행하는게 아니라, 읽기 모델인 MySQL에도 이벤트 재생(Replay)시켜야한다.
쓰기 모델에서 이벤트 재생을 통해 최신 상태의 Aggregate로 복원하면 EventHandler를 통해 읽기 모델도 갱신하면서 최신 상태를 찾아가야 한다.
읽기 모델은 투명하게 결과를 나타내는 Projection일 뿐이라는 점을 글로만 읽을 때와 문제 상황에서 직접 겪으면서 다시 볼 때 느끼는 바가 달라졌다.
이벤트 스토어에 수 백개의 이벤트를 복원(replay)한다면, 읽기 모델에 한꺼번에 과부하가 걸리진 않는가?
100번의 상태 변화가 있는 Aggregate에 대해서 복원하면 최신 상태로 만들기 위해서 읽기 모델의 데이터베이스에도 총 100번의 갱신이 이뤄진다.
당연히 갱신되는 과정이 너무 많거나, 동시다발적이면 Insert/Update 요청이 집중되면서 부하를 유발할 수 있다.
MySQL이라고 하면 트랜잭션 단위로 쿼리가 발생하니 InnoDB flush, Index 갱신으로 undo/redo 부담이 생길 수 있다.
해결 방법
1. Batch 처리 - 이벤트 1개씩 처리하지 말고, 10개~100개씩 묶어서 처리
2. Throttling 전략 - 이벤트 1개 처리 후 일정 시간동안 지연시키는 throttling 전략 사용
2. 비동기 큐 처리 - 리플레이된 이벤트를 바로 반영하지 않고, 메시지 큐에 보내서 비동기 처리
- 읽기 모델 처리 속도에 맞게 자연스럽게 백프레셔(Backpressure) 유도
3. 트랜잭션 단위로 처리 - 이벤트 하나마다 DB Connection을 열고 닫지 말고, 트랜잭션 단위로 여러 이벤트 처리
- 여러 이벤트를 하나의 DB 트랜잭션으로 묶어서 DB의 disk IO를 줄이는 효과
4. 이벤트 스냅샷(Event Snapshot) 처리 - 미리 일정량
만약 수백 개 수준이 아니라 수만 개 이상의 이벤트가 쌓인 경우라면 과부하에 대비해서 이벤트 스냅샷 사용
- Projection 자체를 리플레이하지 않고, Snapshot 저장한 시점으로 복원
아래 포스트에서 이벤트 스냅샷에 대한 설명과 실습이 이어지니 확인 바란다.
Axon Framework를 이용해서 이벤트 소싱을 도입하자
원래는 이벤트 소싱으로 도메인을 먼저 관리하고, CQRS 패턴을 이용해서 Command와 Query를 분리하면서 책임을 명확히 하는 식으로 단점을 보완하는게 맞다.본 프로젝트는 이와 다르게, 전적 통계
downfa11.tistory.com
'project > wargame' 카테고리의 다른 글
| [꿀팁대방출] Kafka Topic 재설계와 EDA 도입을 통한 획기적인 클라우드 비용 개선 (3) | 2025.07.12 |
|---|---|
| ElasticSearch Completion을 이용한 검색어 자동완성 구현 (feat. 검색 엔진의 성능 개선) (0) | 2025.03.16 |
| 게임종료시 전적 갱신에 대한 트랜잭션 롤백 처리 (Saga 패턴, Axon Framework) (0) | 2025.03.16 |
| 데이터에 기반한 사용자 행위 분석 - 어떤 데이터를 수집해야할까? (0) | 2025.03.12 |
| Hexagonal Architecture 마이그레이션 (0) | 2025.02.11 |