배경
지난 랭킹 시스템 개선에서 ‘최고 기록 랭킹’을 도입하고, ‘최고 기록’ 집계 객체(테이블)을 만들었다.
쓰기 성능을 약간 희생해 조회 성능을 개선하고, ‘최고 기록 갱신’을 코드로 표현할 수 있게 되었다.
“해치웠나?”라고 생각할 때가 가장 위험하다고 했던가.
집계 객체 갱신에 대해 고민하던 중 동시성 문제가 생길 수 있는 상황을 발견했다.
상황
최고 기록이 전무한 경우
최고 기록은 게임을 한 판이라도 마쳐야 생성된다.
최고 기록이 없는 사용자가 동시에 다수의 게임을 완료한다면 어떨까?
방금 가입한 사용자 1
이 2개의 게임을 동시에 진행하는 상황을 생각해보자.
두 트랜잭션은 격리되어 있어, 커밋되기 전까지 다른 최고 점수의 존재를 모른다.
따라서 두 트랜잭션은 각각 새 최고 점수를 저장한다.
결과적으로 유니크 제약조건이 있다면 예외가, 없다면 한 사용자에게 2개의 최고 점수가 생긴다.
최고 기록이 이미 있는 경우
기존 사용자 사용자 1
의 최고점수는 10점이다.
사용자 1
은 동시에 2개의 게임을 진행했고, 트랜잭션 1
은 70점, 트랜잭션 2
는 80점을 쓰려고 한다.
논리적으로는 80점이 되는 것이 맞지만, 결국 70점이 되었다.
두 트랜잭션은 격리되어 있어 각각의 입장에선 최고 점수가 10점으로 보이기 때문이다.
실제로 그럴까?
어떻게 풀 것인가?
잦은 문제인가?
잦은 문제라면, 처리 성능도 챙겨야 한다.
DB 커넥션은 한정적이기 때문에 이 트랜잭션의 처리 속도가 늦어지면 다른 트랜잭션이 대기하기 때문이다.
‘한 사용자가 동시의 2개의 게임을 플레이하는 경우’는 잘 발생하지 않는다.
이 점을 염두해서 해결 방법을 고민해보자.
동시 게임 실행을 막는다면?
우선 이 방법은 사용자의 행위를 제한하는 대신, 모든 문제를 근본적으로 해결한다.
하지만 ‘동시 실행 방지 시스템’을 개발하고 유지하는 비용을 고려했을 때, 약간 망설여졌다.
동시 게임 실행을 막기 위해선 상태를 관리해야 한다.
그 ‘상태’는 또 어떻게 관리할 것인가?
자주 일어나지도 않는 문제를 해결하기 위한 비용이 너무 크다는 생각이 들었다.
대표적으로 몇 가지가 생각났다.
- 개발자가 실행 상태가 관리되고 있다는 것을 신경쓰는 비용
- 상태 관리 실패를 관리하는 비용
- 상태 관리 오류로 발생하는 비용
- 사용자 이탈
- 디버깅 및 버그 데이터 수리 인력(시간)
ex) 사용자 연결이 끊기면 게임을 종료시킨다, 클라이언트와 heartbeat 체크를 한다.
그래서 이 방법은 최후의 방법으로 사용하는 것이 좋겠다고 생각했다.
기술적으로 풀 것인가?
문제를 항상 기술적으로 풀어야 하는 것은 아니다.
조회수나 좋아요 같이, 비즈니스적 중요도가 낮은 문제는 실시간성을 낮추는 선택지도 있다.
메모리에 쌓은 후 주기적으로 반영해주는 것이다. 이 주기를 짧게 가져가면 꽤 쓸만할 것이다.
랭크 시스템은 어떨까?
실시간으로 보여야하는것이 아니라면, 큐를 통한 순차 반영으로 문제를 우회할 수 있다.
하지만 그러지 않고 문제를 직면하기로 했다.
욕심일 수 있지만, 사용자에게 실망감을 주고싶지 않았기 때문이다.
최고 점수를 갱신했는데, 바로 보이지 않는다면 실망스러울 것이라는 생각이 들었다.
다음 글에서 이 문제를 실제로 풀어보자.