1인 1회 이벤트가 10번 참여된 이유

기획자가 운영에서 처음 이해한 레이스 컨디션

by 써니

서비스를 운영하다 보면 가끔 이해하기 어려운 순간을 만난다.
논리적으로는 문제가 없어 보이는데 실제 서비스에서는 전혀 다른 결과가 나타나는 순간이다.


이벤트 기능을 운영하던 중이었다.

룰렛 이벤트로 참여 조건은 단순했다.

1인 1회 참여.

사용자가 버튼을 누르면 중복참여여부를 체크하고,

참여 정보가 없으면 룰렛이 돌아가고,

룰렛 참여 기록은 데이터베이스에 저장되는 구조였다.
개발 단계에서도 여러 번 테스트했고 특별한 문제는 없었다.


그런데 운영 로그를 확인하다가 이상한 기록을 발견했다.

한 사용자가 같은 시간에 10번 참여한 기록이었다.

시간도 거의 동일한 시간이었다.

개발자에게 물어보니 아래와 같은 답변을 주었다.

"0.01초 단위로 10번 요청되었고 db에 저장되어 중복체크를 하기도 전에 다음 뽑기가 진행 되어 중복체크를 뚫은것으로 보입니다."


처음에는 오류라고 생각했다.

개발기에서 동일 오류를 재현하기 위해 단시간에 클릭을 여러번 시도해봤지만

몇번을 해도 개발기에서는 문제가 발생하지 않았다.
테스트 계정일까?

그러나 실제 사용자였다.


같은 코드인데 왜 운영에서만 이런 일이 발생했을까...


image.png


레이스 컨디션(Race Condition)


지금 해당 원인을 파악하면서 알게 된 개념이 있었다.

레이스 컨디션(Race Condition) 이었다.

레이스 컨디션은 컴퓨터 과학에서 다음과 같이 정의된다.

여러 프로세스나 스레드가 동시에 동일한 데이터를 접근할 때
실행 순서나 타이밍에 따라 결과가 달라지는 상황을 의미한다.

쉽게 말하면 여러 작업이 동시에 실행될 때
누가 먼저 실행되느냐에 따라 결과가 달라지는 문제다.


이 개념을 이해하고 나서야 그 사건을 설명할 수 있었다.

룰렛 이벤트의 서버 로직은 단순했다.

사용자가 이미 참여했는지 확인

참여하지 않았다면 룰렛 실행

참여 기록 저장


논리적으로 보면 문제가 없어 보인다.

하지만 실제 시스템에서는 이 과정이 동시에 실행될 수 있다.


예를 들어 사용자가 버튼을 여러 번 빠르게 클릭하면
브라우저는 여러 개의 요청을 서버로 보낸다.

그리고 운영 환경에서는 이 요청들이 거의 동시에 처리된다.

이때 서버에서는 다음과 같은 일이 발생할 수 있다.

요청 A : 참여 기록 조회 → 없음
요청 B : 참여 기록 조회 → 없음
요청 C : 참여 기록 조회 → 없음

그리고 그 다음에야 참여 기록이 저장된다.

결과적으로 모든 요청이
“아직 참여하지 않았다” 라고 판단하게 된다.

그래서 1인 1회 이벤트에 같은 사용자가 여러 번 참여하는 상황이 생긴다.

이것이 바로 레이스 컨디션이다.


이 문제는 특히 웹 서비스에서 자주 나타난다.

예를 들어 티켓 예매 시스템에서도 비슷한 문제가 발생할 수 있다.

남은 티켓이 한 장일 때 두 사용자가 동시에 예매를 시도하면

사용자 A : 티켓 확인 → 있음
사용자 B : 티켓 확인 → 있음

이후 두 주문이 모두 생성될 수 있다.

이 역시 레이스 컨디션의 대표적인 사례다.


흥미로운 점은 이런 문제가 개발 환경에서는 잘 나타나지 않는다는 것이다.

개발 환경에서는 보통

서버가 한 대이고

동시 요청이 거의 없으며

트래픽이 낮다.

그래서 요청이 거의 순차적으로 처리된다.


하지만 운영 환경은 다르다.

운영 시스템은 보통

여러 대의 서버가 동시에 요청을 처리하고

네트워크 지연이 존재하며

여러 사용자가 동시에 기능을 사용한다.

그래서 동시성이 실제로 발생한다.

레이스 컨디션이 운영에서 발견되는 이유도 여기에 있다.


실제로 레이스 컨디션은 재현하기 어렵고 테스트에서 발견하기 어려운 버그로 알려져 있다.

사실 내가 이 개념을 완전히 이해하게 된 것은
룰렛 이벤트 이전에 겪었던 또 다른 사건 때문이었다.

알림톡 발송 기능에서였다.

어떤 작업에서 담당자가 A에서 B로 변경되었다.

그런데 알림톡이 변경된 B가 아니라 변경 전 담당자인 A에게 발송되었다.

처음에는 단순한 버그라고 생각했다.

하지만 구조를 살펴보면 이 역시 레이스 컨디션으로 설명할 수 있었다.

한 프로세스는 담당자를 조회하고 있었고
다른 프로세스는 담당자를 변경하고 있었다.

조회 시점과 변경 시점이 겹치면서
알림 발송 대상이 이전 데이터로 결정된 것이다.

겉으로 보면 전혀 다른 문제처럼 보였지만
두 사건의 원인은 같았다.

동시성 문제.

즉 레이스 컨디션이었다.


이 경험을 통해 하나의 사실을 깨닫게 되었다.

서비스는 우리가 생각하는 것처럼 항상 순서대로 동작하지 않는다.

기획 문서에서는 보통 이런 흐름을 그린다.

참여 여부 확인

참여 처리

기록 저장

하지만 실제 시스템에서는 여러 요청이 동시에 들어온다.


그래서 더 중요한 질문이 생긴다.

이 기능은 동시에 여러 요청이 들어오면 어떻게 되는가.

그리고 또 하나.

중복은 어디에서 막는가.

서버인가
아니면 데이터베이스인가.


운영 경험이 쌓일수록
기획자는 기능의 흐름뿐 아니라 시스템의 동작 방식도 함께 생각하게 된다.

그 이후로 나는 이벤트 기능을 기획할 때
항상 한 가지 질문을 덧붙인다.

“이 기능은 동시 요청을 어떻게 처리하나요?”

그리고 그 질문 하나가
운영에서의 많은 문제를 미리 막아주기도 한다.