작성: COZYGONO 개발·편집팀 · 수정:
오래 열어 둔 탭이 최신 기록을 되감았다: 저장 경계를 다시 그은 이야기
한 인물이 익힌 무공이 예전 상태로 돌아가 있었습니다. 버전 번호는 정상적으로 올라가 있었는데도요. 운영 중이던 웹게임에서 겪은 저장 사고와 그 뒤 바꾼 설계를 나눕니다.
- 탭 A가 오전 10시에 인물 정보(무공 1성)를 읽어 메모리에 보관
- 오전 10시 30분, 서버 정산이 돌아 인물 무공이 2성으로 승급
- 오전 11시, 탭 A에서 단순 배치 순서만 변경 후 저장 요청
- 탭 A가 들고 있던 옛 사본(무공 1성)이 통째로 기록되며 승급 내역 증발
- 인물 정본: 능력치·무공 경지는 서버 정산(Settlement)과 수련 명령만 수정 가능
- 문파 운영 정본: 배치·표시 순서 변경은 해당 필드만 부분 패치(Partial Patch)
- 실행 원장(Append-Only): 모든 정산 결과를 결정 ID와 함께 이력으로 보존해 복구 가능성 확보
2026년 7월, 운영 중이던 웹게임에서 실제로 겪은 사고입니다. 사고 기록에서 원인과 교훈 부분만 발췌합니다.
1. 무공이 사라졌다
한 인물이 특정 무공을 2성까지 익힌 기록이 서버 정산 결과에 남아 있었습니다. 그런데 얼마 뒤 확인해 보니 그 무공이 없었습니다. 이전 상태로 되돌아가 있었습니다.
데이터가 깨진 흔적은 없었습니다. 저장 버전 번호도 순서대로 올라가 있었습니다. 겉으로는 모든 것이 정상이었습니다.
2. 버전 번호는 무엇을 보장하지 않는가
원인은 저장 단위에 있었습니다.
플레이어가 문파 운영 설정을 바꾸면, 웹앱은 그 설정만이 아니라 인물 문서 전체를 다시 기록하고 있었습니다. 저장 직전에 서버 버전을 읽어 숫자를 올리는 처리는 있었습니다. 하지만 함께 기록되는 내용물은 브라우저가 한참 전에 읽어 둔 사본이었습니다.
그 사이 서버는 정산을 돌려 인물의 무공을 갱신해 둔 상태였습니다. 그러니 순서는 이렇게 됩니다.
- 브라우저가 인물 정보를 읽는다.
- 서버 정산이 돌아 무공이 추가된다.
- 플레이어가 관계없는 설정을 바꾼다.
- 브라우저가 오래된 사본 전체를 다시 쓴다. 버전 숫자는 정상적으로 증가한다.
버전 번호는 "누가 마지막에 썼는가"를 알려줄 뿐, "쓰려는 내용이 최신인가"를 보장하지 않습니다. 충돌 감지가 아니라 저장 단위가 문제였습니다.
3. 정본을 나누다
그래서 데이터마다 "이 값의 진짜 주인은 어느 문서인가"를 정하고 경계를 그었습니다.
- 인물의 능력과 무공: 인물 문서. 개인값이 실제로 바뀐 경우에만 기록한다.
- 배치와 표시 순서 같은 운영값: 문파 쪽 문서에 부분 기록한다.
- 계산 스냅샷: 정산 버전이 일치할 때만 통째로 커밋한다.
핵심은 "관계없는 값을 저장할 때 남의 정본을 건드리지 않는다"였습니다. 운영 설정을 바꾸는 동작이 인물의 무공 문서를 다시 쓸 이유는 없었습니다.
4. 되돌릴 수 있는 것과 없는 것
사라진 무공은 서버 정산 기록이 append-only로 남아 있어 복구할 수 있었습니다. 어느 회차에 무엇이 추가됐는지 증거가 남아 있었기 때문입니다.
반면 플레이어가 사고 전에 선택했던 운영 설정 값은 복구하지 못했습니다. 그 값은 변경 이력을 따로 남기지 않고 현재 상태만 저장하고 있었습니다. 결국 사용자에게 한 번 다시 지정해 달라고 안내할 수밖에 없었습니다.
무엇을 이력으로 남기고 무엇을 현재 값만 둘지는 성능 문제로 보이지만, 사고가 났을 때는 복구 가능성의 문제가 됩니다.
5. 남은 습관
지금은 저장 코드를 볼 때 두 가지를 먼저 확인합니다. 이 저장이 건드리는 문서가 정말 이 동작의 소관인지, 그리고 함께 실려 가는 내용물이 언제 읽은 것인지입니다. 오래 열어 둔 탭은 생각보다 흔하고, 사용자는 탭을 닫아야 할 이유를 모릅니다.
6. 재현 순서를 짧게 고정하기
같은 계정의 화면 A와 B를 열고, B에서 새 결과를 만든 뒤 A에서 관계없는 표시 순서를 저장하는 장면을 생각해 볼 수 있습니다. 기대 결과는 새 상태를 보존하면서 표시 순서만 바뀌는 것입니다. 현재 값이 유지되는지, 재시도가 같은 변경을 두 번 적용하지 않는지, 충돌을 사용자가 알아볼 수 있는지 차례로 확인합니다.
이 순서는 새 장애가 발생했다는 보고가 아니라 저장 경계를 검토하기 위한 재현 시나리오입니다. 인물 전체 객체를 보내는 대신 변경 필드와 기준 버전을 함께 다루는 이유를 작은 예시로 설명할 수 있습니다.