나만의 작은 도서관
[TIL][C++] 250619 MMO 서버 개발 40일차: MMORPG 게임 서버에서는 Redis를 어떻게 활용할 수 있을까?, 빈도가 잦은 중요 데이터 갱신을 위한 Dirty Flag 활용 본문
Today I Learn
[TIL][C++] 250619 MMO 서버 개발 40일차: MMORPG 게임 서버에서는 Redis를 어떻게 활용할 수 있을까?, 빈도가 잦은 중요 데이터 갱신을 위한 Dirty Flag 활용
pledge24 2025. 6. 19. 22:44주의사항: 해당 글은 일기와 같은 기록용으로, 다듬지 않은 날것 그대로인 글입니다.
MMORPG 게임 서버에서는 Redis를 어떻게 활용할 수 있을까?
- 가만히 생각해 보면 Redis를 MMORPG 게임 서버에서 사용하면 좋을 듯 하지만, 정확히 어떻게 써야 할지 감이 오지 않는다. 어떻게 사용해야 할까?
Redis의 사용 목적 분류
- Redis는 여러 가지 용도로 사용될 수 있기 때문에(그리고 여러 개의 Redis를 사용할 수 있기 때문에), Redis의 사용 목적을 분류해 두는 것이 좋다. 저번에도 한 번 다루었듯, Redis의 용도는 1) 캐시, 2) 공용 컴포넌트, 3) Atomic 한 자료 구조들이 있다. Atomic 한 자료 구조 사용은 부가적인 것으로 제쳐두고 1번과 2번에 대해서 구분을 지어야 한다.
1. MainDB 데이터의 캐시 용도
- Redis를 사용하는 읽기/쓰기 전략 중 가장 대표적인 전략으로 Cache-aside + Write Around 전략이 있다. 즉, Redis는 Read-Only 캐시로 쓰고, 쓰기 작업은 DB에만 하는 것이다. 이 경우, 데이터의 갱신이 일어날 때마다 Redis가 가지는 데이터와 MainDB가 가지는 데이터의 일관성이 무너지는 상황이 오게 되므로 정합성이 크게 요구되는 데이터를 저장하면 안 된다.
- 정리하자면, 게임 서버에 위 전략으로 Redis를 적용하려면 1) 자주 읽히지만 갱신 빈도가 낮고, 2) 갱신되어도 정합성이 크게 요구되지 않는 데이터를 저장해야 한다. 이에 해당하는 데이터는 아래와 같다.
- 유저 세션 정보: 로그인 정보, 토큰 등
- 유저 기본 정보(프로필): 닉네임, 직업, 레벨 등
- 게임 시작 시 UI에 자주 사용됨
- 랭킹 정보
2. 공용 컴포넌트(각 서버 데이터의 만남의 광장)
- 개인적으로 생각하기엔 여기가 중요해 보인다. 하나의 월드에서 여러 채널을 운영하고, 각 채널을 서버 한 대씩 맡는다면, 서버가 가지는 각 메모리에 대한 접근은 서로 할 수가 없다. 이때 각 서버가 통신을 하고 싶지만 영속적인 데이터는 아니라 MainDB에는 올리고 싶지 않은 데이터들을 저장해 두는 역할로 Redis를 사용할 수 있다. 이에 해당하는 데이터는 아래와 같다.
- 채팅 메시지: 다른 채널에 있어도 친구/길드 메시지는 받아야 한다.
- 온라인 플레이어 목록: 다른 채널에 있어도 온라인 중인 친구 목록을 알 수 있어야 한다. + 채널이나 레벨 정도도 알 수 있으면 좋다.
- 월드 단위 기능(경매장 등): 채널이 달라도 월드 단위 기능은 같은 기능을 사용할 수 있어야 한다.
빈도가 잦은 중요 데이터 갱신을 위한 Dirty Flag 활용
- 몬스터를 잡으면 캐릭터는 경험치(exp), 골드, 일부 아이템 및 무기를 획득할 수 있다. 문제는 이 하나하나가 전부 DB에 저장되어야 할 데이터들이며, 몬스터를 잡을 때마가 이를 DB에 접근해서 갱신하면 DB가 터져버릴 것이라는 것이다. 이는 어떻게 해결할 수 있을까?
Redis를 사용하면 해결?
- 클라우드 Redis를 하나 두고 거기에 다 때려 박아두면 해결될까? 이를 해결책으로 제시하는 경우가 종종 보이지만, 내가 생각했을 땐 그리 좋은 방식처럼 보이진 않는다. 개인적으로는 Redis는 어디까지나 캐시의 역할로 남겨야지,
- MainDB의 부하를 줄이기 위해 지워지면 큰일 나는 데이터를 저장해 두고 갱신시키는 건 굉장히 위험한 방법이라고 본다.
- 물론, 복제본(Replica)을 떠서 장애 발생 시 대응할 수 있는 방안을 마련해 두면 오케이!라는 합리적인 주장도 있다. 이게 맞는 말인 건 알겠지만 역할을 분류했을 때 “Redis는 지워져도 되는 데이터만 있어야 한다”는 느낌을 가지고 가고 싶다는 것이다.
또 다른 대안: Dirty Flag를 활용한 주기적 갱신
- Redis를 사용하는 대신, Dirty Flag를 사용하는 방식도 있다. Dirty Flag란 하지 않아도 되는 갱신은 최대한 미루는 방식으로, 타깃으로 잡은 데이터가 갱신될 때만 Dirty Flag를 올리고, 주기적 갱신 타임이 왔을 때 Dirty Flag가 올라간 데이터만 MainDB에 갱신하는 방식이다. 과정을 요약하자면 아래와 같다.
- 클라이언트 → 서버에게 몬스터를 잡았다고 패킷 전달
- 서버는 검증 이후 서버 메모리에 결과를 저장
- 1~2번 과정을 다음 주기(10~30초)가 올 때까지 반복
- 다음 주기가 되었을 때, dirty flag가 올라간 유저의 데이터들을 긁어모아 한 번에 MainDB에 갱신
- 위 방식을 활용하면 아무리 많은 몬스터들을 잡아도 MainDB에 갱신되는 주기는 일정하므로, DB 부하를 크게 줄일 수 있다.
저장한 데이터가 서버 다운으로 인해 날아간다면? : 서버는 죽어도 로그를 남겨놓았다
- 많은 데이터가 날아가지는 않겠지만 혹여나 서버가 죽어버려서 MainDB에 올라가지 않은 데이터가 그대로 날아갈 수 있다. 이런 경우를 대비하기 위해, 장애가 발생해도 복원할 수 있도록 로그를 남겨두는 방식을 사용한다. 서버 장애 발생 시, 로그를 통한 복원 과정은 아래와 같다.
- 클라 ↔ 서버 통신으로 서버 메모리에 경험치, 레벨과 같은 데이터 갱신
- 데이터 갱신할 때마다 로그로 기록
- 다음 주기가 오기 전에 갑자기 서버가 다운(죽음)
- 죽은 서버가 남긴 로그를 보고 DB에 저장
- 데이터 복원 완료!
- 이렇듯 서버 메모리 + Dirty Flag + 주기적 갱신 + 로그 남겨놓기 조합으로 Redis 없이 DB 부하를 줄이면서도 데이터 소실에 대응책도 마련해 둘 수 있게 된다.
또 하나의 고민: 채널 이동시 Redis활용은?
- 플레이어는 원한다면 다른 채널로 이동할 수 있어야 한다. 여기서 고민되는 상황은 ‘채널 이동을 고려해서 Redis에서 채널 이동에 필요한 데이터를 관리해야 하는가?’이다.
- Redis에서 채널 이동에 필요한 데이터를 동기화가 완벽히 된 상태로 들고 있는다면, 채널을 이동했을 때 서버만 옮기면 되지, 서버 메모리를 옮길 필요는 사라지게 된다. 문제는, 이걸 위해서는 Redis에서 Hp/Mp, 골드 등 현재 플레이어 캐릭터가 가지고 있는 대부분의 데이터를 Redis가 들고 있어야 한다는 것이다.
- 갱신 빈도가 높은 데이터들이 대부분인데 Redis에 가서 갱신하고 읽어오고 하는 것은 영 맘에 들지 않는 방식처럼 보인다. 이동할지도 모르는 플레이어를 위해 Redis에 올려두는 것은 굉장히 비효율적인 것처럼 느껴진다.
- 그래서 최종적으로는, 서버 메모리에서 관리하되, 채널 이동시 Redis에 올려서 이동시키는 방식이 합리적인 것 같다는 판단을 내렸다. 즉, 과정은 아래와 같다.
서버 A → 서버 B 이동:
1. 서버 A: 현재 상태를 Redis에 즉시 저장
2. 서버 B: Redis에서 상태 로드 후 로컬 메모리로 이동