뱅크샐러드가 게임을 만들 때 데이터 정합성을 지키는 방법 (feat. 낙관적 락)
일해라 김뱅샐은 update_version 기반 낙관적 락으로 정합성을 지켰다.
일해라 김뱅샐은 유저가 캐릭터를 육성하며 SAL을 얻는 방치형 앱테크 게임이고, 이 SAL은 실제 원화로 전환 가능한 자산이다. 그래서 게임의 재미를 유지하면서도 자산 계산의 데이터 정합성을 반드시 보장해야 했다.
핵심 상태인 character_state는 에너지 충전, 레벨업, 쓰레기 치우기, 스킬 사용 같은 유저 API, 백오피스의 수동 조정, 향후 배치·비동기 이벤트까지 다양한 경로로 갱신된다. 이 구조에서는 레이스 컨디션이 곧 보상 누락이나 신뢰 훼손으로 이어지기 때문에, 단순한 성능 문제가 아니라 서비스 안전장치의 문제로 동시성 제어를 다뤘다.
잠금 방식으로는 비관적 락, 분산 락, 낙관적 락을 비교했다. 충돌 빈도가 낮고, Redis 같은 별도 인프라를 늘리고 싶지 않으며, DB 대기 때문에 게임 흐름이 끊기는 경험을 피해야 했기 때문에 최종적으로 낙관적 락을 선택했다.
이 선택이 가능했던 이유는 서비스 구조 자체가 충돌을 크게 흡수하도록 설계됐기 때문이다.
- 보상 계산은 개별 이벤트 적립이 아니라
마지막 업데이트 시점과현재 시점의 차이를 기준으로 하는 time-delta 정산이다. - 충돌로 한 번 실패해도 이전 기간의 보상이 사라지지 않고, 다음 성공 시점에 한꺼번에 정산된다.
- 서버는 무리한 재시도 루프를 돌리지 않아도 되므로, 시스템이 스스로 최신 상태로 수렴하는 self-correcting 구조를 만들 수 있다.
구현은 DB 스키마에 update_version을 두고, 업데이트 시 WHERE character_id = ? AND update_version = ? 조건으로 CAS(Compare-And-Swap) 형태의 원자적 갱신을 수행하는 방식이었다. character와 character_state를 분리해 자주 바뀌는 동적 데이터를 따로 관리했고, numUpdated == 0이면 충돌로 판단해 ErrConcurrentStateUpdate를 반환했다.
실무적으로는 updated_at을 버전 필드처럼 쓰면 안 된다고 짚는다. 타임스탬프 정밀도 한계나 클럭 스큐 때문에 같은 밀리초 안의 연속 업데이트를 놓칠 수 있어서, 충돌 검출에는 반드시 정수형 버전 필드가 필요하다.
운영에서는 replication lag와 stale read를 경계해야 한다. 정합성이 중요한 경로는 Primary DB에서 최신 버전을 읽어야 하고, 이벤트 처리에서도 DB 버전과 메시지 버전을 대조해야 한다.
충돌을 다룰 때는 재시도를 무조건 늘리는 대신 네 가지 원칙을 제시한다.
- Fresh Read: 재시도 전에 최신 버전을 다시 읽는다.
- 지수 백오프와 지터: 즉시 재시도로 인한 연쇄 충돌을 줄인다.
- 멱등성:
request_id기반으로 중복 요청을 안전하게 처리한다. - 최대 재시도 횟수 제한: 반복 충돌은 시스템 경합의 신호로 보고 다른 대응을 검토한다.
결국 이 서비스는 복잡한 락 메커니즘보다 도메인의 흐름을 활용해, 가장 단순하면서도 서비스 특성에 맞는 낙관적 락으로 성능과 정합성을 함께 확보했다. 운영 이후에는 데이터 정합성 이슈가 0건이었다고 정리한다.
이 요약은 원문 이해를 돕기 위한 큐레이션입니다. 저작권은 원저작자에게 있으며, 정확한 내용과 맥락은 원문을 확인하세요.
요약 오류, 출처 표기 문제, 삭제 요청은 문의 · 건의로 알려주세요.