나만의 작은 도서관

[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. 서버는 검증 이후 서버 메모리에 결과를 저장
    3. 1~2번 과정을 다음 주기(10~30초)가 올 때까지 반복
    4. 다음 주기가 되었을 때, dirty flag가 올라간 유저의 데이터들을 긁어모아 한 번에 MainDB에 갱신
  • 위 방식을 활용하면 아무리 많은 몬스터들을 잡아도 MainDB에 갱신되는 주기는 일정하므로, DB 부하를 크게 줄일 수 있다.

 

저장한 데이터가 서버 다운으로 인해 날아간다면? : 서버는 죽어도 로그를 남겨놓았다

  • 많은 데이터가 날아가지는 않겠지만 혹여나 서버가 죽어버려서 MainDB에 올라가지 않은 데이터가 그대로 날아갈 수 있다. 이런 경우를 대비하기 위해, 장애가 발생해도 복원할 수 있도록 로그를 남겨두는 방식을 사용한다. 서버 장애 발생 시, 로그를 통한 복원 과정은 아래와 같다.
    1. 클라 ↔ 서버 통신으로 서버 메모리에 경험치, 레벨과 같은 데이터 갱신
    2. 데이터 갱신할 때마다 로그로 기록
    3. 다음 주기가 오기 전에 갑자기 서버가 다운(죽음)
    4. 죽은 서버가 남긴 로그를 보고 DB에 저장
    5. 데이터 복원 완료!
  • 이렇듯 서버 메모리 + Dirty Flag + 주기적 갱신 + 로그 남겨놓기 조합으로 Redis 없이 DB 부하를 줄이면서도 데이터 소실에 대응책도 마련해 둘 수 있게 된다.

 

또 하나의 고민: 채널 이동시 Redis활용은?

  • 플레이어는 원한다면 다른 채널로 이동할 수 있어야 한다. 여기서 고민되는 상황은 ‘채널 이동을 고려해서 Redis에서 채널 이동에 필요한 데이터를 관리해야 하는가?’이다.
  • Redis에서 채널 이동에 필요한 데이터를 동기화가 완벽히 된 상태로 들고 있는다면, 채널을 이동했을 때 서버만 옮기면 되지, 서버 메모리를 옮길 필요는 사라지게 된다. 문제는, 이걸 위해서는 Redis에서 Hp/Mp, 골드 등 현재 플레이어 캐릭터가 가지고 있는 대부분의 데이터를 Redis가 들고 있어야 한다는 것이다.
  • 갱신 빈도가 높은 데이터들이 대부분인데 Redis에 가서 갱신하고 읽어오고 하는 것은 영 맘에 들지 않는 방식처럼 보인다. 이동할지도 모르는 플레이어를 위해 Redis에 올려두는 것은 굉장히 비효율적인 것처럼 느껴진다.
  • 그래서 최종적으로는, 서버 메모리에서 관리하되, 채널 이동시 Redis에 올려서 이동시키는 방식이 합리적인 것 같다는 판단을 내렸다. 즉, 과정은 아래와 같다.
서버 A → 서버 B 이동:
1. 서버 A: 현재 상태를 Redis에 즉시 저장
2. 서버 B: Redis에서 상태 로드 후 로컬 메모리로 이동