실시간 좌석 예약 시스템에서 Race Condition을 해결한 방법 (그리고 잠을 못 잤다)

발행: (2026년 3월 29일 오전 09:28 GMT+9)
4 분 소요
원문: Dev.to

Source: Dev.to

문제

두 명의 사용자, 한 좌석 – 완전한 혼란.
사용자 A와 사용자 B가 거의 같은 순간에 좌석을 조회했으며, 둘 다 사용 가능한 것으로 보고 예약을 진행했습니다. 데이터베이스가 두 번 업데이트되어 같은 좌석에 두 명의 소유자가 생겼습니다.

실패한 접근 방식

간단한 읽기‑후‑쓰기 검사

먼저 if 문을 이용한 직관적인 방법을 시도했습니다: 좌석 상태를 읽고, 새로운 예약을 기록합니다. 실제 동시성 상황에서는 바로 깨졌습니다. 읽기‑후‑쓰기 순서는 원자적이지 않 않기 때문입니다. 두 연산 사이의 아주 작은 틈이 바로 레이스 컨디션이 발생하는 지점입니다.

클라이언트‑측 플래그

다음으로 잠금을 표시하는 클라이언트‑측 플래그를 추가했습니다. 이것도 실패했습니다—Socket.io 이벤트는 순서가 뒤바뀔 수 있고, UI 상태는 신뢰할 수 있는 진실의 원천이 아니기 때문입니다.

해결책

읽기와 쓰기를 하나의 원자적 MongoDB 작업으로 합치고, 이를 Socket.io 브로드캐스트와 결합해 모든 연결된 클라이언트가 즉시 잠금을 확인하도록 합니다.

// reserveSeat.js
async function reserveSeat(tripId, seatNo, userId) {
  // Atomically find the seat if it is still free and mark it as taken
  const seat = await Seats.findOneAndUpdate(
    { tripId, seatNo, taken: false },          // query condition (the guard)
    { $set: { taken: true, userId } },        // update (atomic at document level)
    { returnDocument: 'after' }              // return the updated document
  );

  if (!seat) {
    throw new Error("Seat already taken!");
  }

  // Broadcast the lock to all travelers
  io.emit('seatLocked', {
    seatNo,
    userId,
    message: `Seat ${seatNo} secured for User ${userId}`
  });
}

핵심 단계

  1. 쿼리 조건이 가드 역할 – 아직 비어 있는 좌석만 매치합니다.
  2. findOneAndUpdate는 문서 수준에서 원자적 – 읽기와 쓰기가 하나의 작업으로 수행됩니다.
  3. null 체크가 오류 경계 – 반환된 문서가 없으면 좌석이 이미 잡힌 것입니다.
  4. Socket.io가 즉시 잠금을 브로드캐스트 – 모든 클라이언트가 실시간으로 업데이트를 받습니다.

가장 큰 교훈

수정이 단순히 MongoDB에만 국한된 것이 아니라 사고 방식의 전환이라는 점입니다: 동시 시스템에서는 읽기 후 쓰기를 절대 신뢰하지 말라. 가드 로직을 데이터베이스 작업 자체에 넣어 원자적으로 만들세요.

여러분의 차례

프로젝트에서 레이스 컨디션을 겪어본 적이 있나요? 어떻게 해결했는지 공유해 주세요.

0 조회
Back to Blog

관련 글

더 보기 »

왜 Node.js를 공부해야 할까?

왜 Node.js를 공부해야 할까요? 개발 세계에 입문하거나 프로그래머로 성장하고 싶다면, Node.js를 공부하는 것이 가장 전략적인 선택 중 하나가 될 수 있습니다…

왜 node.js를 공부해야 할까

왜 Node.js를 공부해야 할까요? 🚀 개발 세계에 입문했거나 프로그래머로 성장하고 싶다면, Node.js를 공부하는 것이 가장…