경쟁 상태

Race Condition

개념

동시에 들어온 작업들이 어느 순서로 처리되느냐에 따라 결과가 달라지는 고장을 가리킵니다.

등장 배경

요청이 하나씩 차례로 들어오는 동안에는 대부분의 코드가 맞게 동작합니다. 문제는 두 요청이 같은 순간에 같은 데이터를 건드릴 때 생기고, 이때는 코드를 아무리 읽어도 틀린 곳이 보이지 않습니다.

페이지덱에서 그런 일이 있었습니다. 무료 플랜은 문서를 세 개까지만 만들 수 있게 되어 있었는데, 문서가 네 개인 무료 계정이 운영 데이터에서 나왔습니다. 태오가 코드를 훑어도 검사는 분명히 들어 있었습니다. 원인은 사용자가 새 문서 버튼을 빠르게 두 번 누른 것이었습니다. 두 요청이 거의 같은 순간에 도착해 둘 다 "지금 문서가 두 개" 라는 답을 받고, 둘 다 세 개째를 만들어도 된다고 판단했습니다.

잘못된 것은 검사도 저장도 아니고 그 둘 사이에 시간이 있었다는 것입니다. 이 고장에 따로 이름이 붙은 이유는 원인을 코드의 한 줄로 지목할 수 없기 때문입니다. 두 요청이 달리기하듯 겹쳐 들어온 상황 자체가 원인입니다.

작동 방식

읽고 판단하고 쓰는 세 동작 사이의 틈에서 생깁니다. 한 요청이 읽은 값이 쓰기 전에 다른 요청 때문에 바뀌면 그 판단은 이미 낡은 값에 근거한 것이 됩니다.

문서 한도 검사를 순서대로 늘어놓으면 이렇게 됩니다.

  1. 요청 A 가 문서 수를 읽습니다. 2 입니다.
  2. 요청 B 가 문서 수를 읽습니다. 아직 2 입니다.
  3. A 가 3 미만이니 만들어도 된다고 판단해 저장합니다. 3 이 됩니다.
  4. B 도 2 를 기준으로 판단해 저장합니다. 4 가 됩니다.

같은 요청이 조금만 늦게 도착하면 3번과 4번 사이에 다시 읽기가 들어가 막혔을 겁니다. 결과가 도착 간격에 달려 있다는 점이 이 고장의 성질입니다.

순서가 보장되지 않는 이유는 여러 겹입니다. 서버가 여러 대여서 두 요청이 다른 대에서 처리되고, 한 대 안에서도 여러 작업이 겹쳐 돌고, 데이터베이스까지 오가는 시간이 요청마다 다릅니다.

막는 방법은 틈을 없애거나 틈이 있어도 결과가 같게 만드는 쪽입니다.

  1. 규칙을 데이터베이스에 박습니다. 같은 값이 두 번 들어올 수 없게 제약을 걸어 두면 두 요청 중 하나는 저장 단계에서 실패합니다. 코드의 검사는 통과해도 저장이 막힙니다.
  2. 읽기와 쓰기를 하나로 묶습니다. 전부 성공하거나 전부 취소되는 작업 묶음인 트랜잭션으로 감싸고, 그동안 다른 요청이 그 행을 못 건드리게 잠급니다.
  3. 값을 읽어 와 계산하지 않고 한 번에 더하고 뺍니다. 잔액을 읽어 1,000을 뺀 값을 쓰는 대신 1,000을 빼라고 지시하면 중간에 낄 틈이 없습니다.
  4. 같은 요청이 두 번 와도 한 번으로 처리합니다. 요청마다 고유한 표를 붙여 이미 처리한 표는 무시하는 방식이고, 이 성질을 멱등성이라고 부릅니다.
  5. 순서를 강제합니다. 메시지 큐에 줄을 세워 한 계정의 작업은 반드시 하나씩 처리하게 만듭니다.

화면에서 버튼을 잠그는 것은 이 목록에 들어가지 않습니다. 두 번 누르기는 줄여 주지만 두 개의 탭, 새로 고침, 통신이 느려 자동으로 다시 보내는 요청은 그대로 통과합니다. 규칙은 서버 쪽에 있어야 지켜집니다.

쓰이는 곳

이 말은 원인을 설명할 때보다 원인을 의심할 때 더 자주 쓰입니다. 데이터가 규칙을 어긴 상태로 남아 있고 코드에는 문제가 없어 보일 때 가장 먼저 꺼내는 이름입니다. 자주 나오는 자리는 넷입니다.

  1. 한도 검사입니다. 무료 플랜의 문서 세 개, 요금제의 좌석 수처럼 개수를 세어 막는 곳입니다. 세고 나서 만들기까지의 틈이 항상 있습니다.
  2. 선착순과 재고입니다. 100명 한정 쿠폰이 103장 발급되는 일이 여기서 생깁니다.
  3. 결제입니다. 같은 결제가 두 번 일어나는 사고이고, 손해가 바로 돈이라 이 자리에는 대개 멱등성 처리가 먼저 들어갑니다.
  4. 뒤에서 도는 작업과 사용자 요청이 같은 데이터를 건드릴 때입니다. 새벽 3시에 사용량을 집계하는 작업이 도는 동안 사용자가 문서를 지우면 집계가 어긋납니다.

발견은 대개 데이터에서 합니다. 규칙을 어긴 행을 찾는 점검을 정기적으로 돌려 무료 계정 중 문서가 네 개 이상인 것을 세어 보는 식입니다. 재현이 어려우니 로그에 요청 도착 시각을 밀리초까지 남기는 것도 값을 합니다.

실무 용례

"재현이 안 되는데 데이터에는 남아 있어요. 경쟁 상태 같습니다."

코드에서 원인을 찾지 못했고 동시 요청을 의심한다는 말입니다.

"검사만 있고 유일 제약이 없어서 뚫립니다."

규칙이 코드에만 있고 데이터베이스에 없다는 지적입니다.

"버튼 막는 건 우회고 서버에서 막아야 합니다."

화면 처리로 끝내면 다른 경로로 같은 사고가 난다는 뜻입니다.

태오가 문서 한도 사고를 고칠 때 한 일은 두 가지였습니다. 계정별 문서 수를 세는 대신 계정과 문서 번호를 묶은 유일 제약을 걸어 네 개째 저장이 데이터베이스에서 막히게 했고, 요청마다 고유한 표를 받아 같은 표가 두 번 오면 두 번째를 무시하게 했습니다. 그다음 지난 6개월 데이터를 훑어 규칙을 어긴 계정 열한 개를 찾아 정리했습니다. 고치는 데 이틀, 이미 생긴 데이터를 정리하는 데 사흘이 들었습니다.

흔한 오해

사용자가 적으면 생기지 않는다고 여기는 경우가 가장 많습니다. 동시 접속자가 열 명뿐인 서비스에서 두 요청이 같은 순간에 도착할 확률이 낮아 보이기 때문입니다. 하지만 이 고장에 필요한 것은 두 사람이 아니라 두 요청입니다. 한 사람이 버튼을 두 번 누르거나, 통신이 느려 앱이 자동으로 다시 보내거나, 탭 두 개를 열어 둔 것만으로 조건이 갖춰집니다. 사용자 수가 적은 서비스에서는 사고가 드물게 일어나는 대신 원인을 찾기가 더 어렵습니다.

재현되지 않으면 고쳐진 것이라고 읽는 것도 흔합니다. 같은 조작을 스무 번 해 봐도 다시 나지 않으면 해결됐다고 판단하게 됩니다. 실제로는 틈이 밀리초 단위라서 손으로는 거의 맞출 수 없고, 개발 환경에서는 요청이 하나씩 처리되어 틈이 아예 생기지 않는 경우도 있습니다. 이 고장은 재현으로 확인하지 않고 규칙이 데이터베이스에 걸렸는지, 같은 요청을 두 번 보내도 결과가 하나인지로 확인합니다. 두 조건이 갖춰지면 재현이 되든 안 되든 막힌 것입니다.

자주 헷갈리는 용어

용어무엇을 가리키는가경쟁 상태와의 차이
동시성여러 작업을 겹쳐 진행하는 처리 방식설계와 능력을 가리키는 말이라 좋고 나쁨이 없습니다. 경쟁 상태는 그렇게 겹쳐 돌리면서 순서를 보장하지 않았을 때 나는 고장이라, 동시성이 있는 곳에만 생깁니다

같이 보면 좋은 페이지

  • 백엔드

    이 고장이 어느 영역에서 다뤄지는지 먼저 봅니다

  • 서버

    규칙을 어디에 두어야 지켜지는지 확인합니다

마지막 업데이트 2026-09-16