본문 바로가기
AI

콘서트 티켓 예매 시스템에서 Overbooking을 막는 백엔드 설계 사고

by 거대웅 TitanBear 2026. 6. 9.

인기 아이돌 콘서트 티켓 예매 시스템은 단순히 서버를 많이 띄운다고 해결되지 않는다. 오픈 직후 1분 동안 10만 명이 동시에 들어오고 좌석은 1만 석뿐이라면, 진짜 문제는 “많은 요청을 받는 것”보다 “같은 좌석이나 같은 재고가 두 번 팔리지 않게 하는 것”이다.

이번 글은 엔지니어링 문제 해결 능력 진단 과제의 Scenario B, 콘서트 티켓 예매 시스템을 선택해 작성한 답변이다. 코드를 나열하기보다, 어떤 기술적 문제가 핵심인지, 어떤 요구사항을 먼저 세워야 하는지, 그리고 DB Lock, Redis, 메시지 큐 같은 선택지가 어떤 Trade-off를 갖는지 중심으로 정리했다.

상황 요약

오픈 1분 만에 10만 명의 동시 접속이 예상된다. 좌석은 1만 석 한정이다. 사용자가 “예매하기”를 누를 때 재고 중복 차감 없이 정확히 1만 명까지만 티켓을 판매해야 한다.

1. 문제의 핵심: 대량 트래픽 속 재고 정합성

이 시나리오에서 해결해야 할 핵심 기술 문제는 동시성 경쟁 상황에서 한정된 자원을 정확히 한 번만 배정하는 것이다. 사용자가 많다는 사실만 보면 트래픽 처리 문제처럼 보이지만, 실제로는 “재고를 읽고, 차감하고, 예약을 생성하는 과정”이 동시에 실행될 때 생기는 Race Condition이 더 위험하다.

예를 들어 남은 좌석이 1석인 순간 두 요청이 거의 동시에 들어왔다고 하자. 두 요청이 모두 “남은 좌석 1석”을 읽은 뒤 각각 주문을 생성하면, 시스템은 실제 좌석보다 많은 티켓을 판매하게 된다. 이것이 Overbooking이다.

핵심은 “10만 요청을 모두 빠르게 성공시키는 것”이 아니라, “성공 가능한 1만 요청만 일관된 기준으로 통과시키고 나머지는 안전하게 거절하거나 대기시키는 것”이다.

또 하나 중요한 점은 결제와 예매가 외부 시스템까지 포함한다는 것이다. DB 트랜잭션은 롤백할 수 있지만, 결제 승인 요청처럼 외부 PG사로 나간 부수 효과는 같은 방식으로 되돌리기 어렵다. 따라서 티켓 예매 시스템은 재고 차감뿐 아니라 예약 상태, 결제 상태, 실패 복구, 중복 요청 처리까지 하나의 상태 흐름으로 설계해야 한다.

2. 핵심 엔지니어링 요구사항

이 문제를 풀기 위해 시스템이 반드시 만족해야 할 조건은 다음과 같다.

콘서트 티켓 예매 시스템의 핵심 엔지니어링 요구사항요구사항 설명 실패 시 문제
재고 정합성 확정 판매 수는 좌석 수 1만 석을 절대 초과하지 않아야 한다. 중복 판매, 환불, 고객 불만, 운영 사고가 발생한다.
원자적 처리 좌석 배정, 재고 차감, 예약 생성은 논리적으로 하나의 성공 또는 실패로 처리되어야 한다. 재고는 줄었는데 예약이 없거나, 예약은 있는데 재고가 줄지 않는 상태가 생긴다.
중복 요청 방지 더블 클릭, 새로고침, 네트워크 재시도로 같은 사용자의 같은 요청이 반복되어도 결과가 중복되면 안 된다. 한 사용자가 여러 좌석을 의도치 않게 확보하거나 결제가 중복될 수 있다.
피크 트래픽 흡수 오픈 직후 1분 동안 들어오는 10만 명의 요청을 직접 DB에 모두 꽂지 않아야 한다. DB 커넥션 고갈, Lock 대기 증가, 장애 전파가 발생한다.
공정한 처리 먼저 도착한 사용자가 불합리하게 뒤로 밀리지 않도록 대기열 또는 순번 기준이 필요하다. 사용자 경험이 무너지고, 재시도 폭주가 더 커질 수 있다.
장애 복구 예약 생성 후 결제 실패, 서버 장애, Redis와 DB 불일치 같은 상태를 복구할 수 있어야 한다. 좌석이 영구적으로 묶이거나, 결제는 되었는데 티켓이 없는 상태가 생긴다.

수치 목표를 잡는다면, “확정 예약은 좌석 수를 1건도 초과하지 않는다”, “대기열 응답은 수백 ms 안에 반환한다”, “예약 보류 시간은 5분 또는 10분처럼 명확히 둔다”, “DB 쓰기 경로는 큐와 워커로 제어해 커넥션 상한을 넘기지 않는다” 정도로 정의할 수 있다.

3. 가장 먼저 부딪힐 기술적 난관

가장 큰 난관은 고경합 자원에 대한 일관성 유지와 처리량 확보를 동시에 만족시키는 것이다. 좌석 수량은 하나의 공유 자원이고, 오픈 직후 모든 요청이 같은 콘서트 재고를 향해 몰린다. 이때 가장 단순한 구현은 DB 한 줄에 남은 좌석 수를 저장하고 모든 요청이 그 행을 갱신하는 방식이다.

하지만 이 방식은 요청이 적을 때는 안전해도, 10만 명이 몰리면 문제가 달라진다. 하나의 재고 행에 Lock이 집중되면서 Lock 대기 시간이 길어지고, 트랜잭션이 쌓이고, DB 커넥션 풀이 고갈될 수 있다. 결국 정합성을 지키기 위해 Lock을 세게 걸수록 처리량과 사용자 경험이 나빠진다.

반대로 Lock을 약하게 걸거나 애플리케이션 메모리에서만 재고를 관리하면 속도는 빨라질 수 있지만, 서버가 여러 대일 때 재고 중복 차감이 발생하기 쉽다. 즉 이 문제는 “정합성 vs 처리량”의 균형을 어떻게 잡느냐가 핵심이다.

4. 접근 방식 가설과 Trade-off

현실적으로는 하나의 기술만으로 모든 요구사항을 만족시키기 어렵다. 그래도 각 선택지의 성격을 비교하면 어떤 조합이 적합한지 판단할 수 있다.

대안 1. DB 비관적 Lock

가장 직관적인 방식은 DB 트랜잭션 안에서 재고 행이나 좌석 행을 SELECT ... FOR UPDATE로 잠그고, 남은 좌석이 있을 때만 예약을 생성하는 방식이다. PostgreSQL 공식 문서 기준으로 FOR UPDATE는 조회된 행이 다른 트랜잭션에서 수정되거나 잠기는 것을 현재 트랜잭션 종료 전까지 막는다.

장점: DB가 최종 Source of Truth가 되므로 정합성을 이해하기 쉽다. 트랜잭션, Unique Constraint, 외래 키를 함께 쓰면 중복 좌석 배정 방어가 명확하다.

단점: 모든 요청이 같은 재고 행을 잠그면 병목이 생긴다. Lock 대기, Deadlock, 커넥션 고갈 문제가 발생할 수 있고, 피크 트래픽에서는 DB가 가장 먼저 힘들어진다.

대안 2. DB 낙관적 갱신 또는 조건부 Update

두 번째 방식은 명시적인 긴 Lock 대신, DB의 원자적 Update를 이용하는 것이다. 예를 들어 “남은 좌석이 0보다 클 때만 sold_count를 1 증가시킨다”는 조건부 Update를 수행하고, 변경된 행 수가 1이면 성공으로 판단한다. 버전 컬럼을 두고 Optimistic Lock을 적용할 수도 있다.

장점: 트랜잭션을 짧게 유지할 수 있고, 긴 Lock 점유를 줄일 수 있다. 구현 복잡도가 비교적 낮고 DB만으로 재고 초과를 막을 수 있다.

단점: 트래픽이 몰리면 실패와 재시도가 많아진다. 결국 같은 행을 계속 갱신하는 Hot Row 문제가 남고, 공정한 순서 보장이 어렵다.

대안 3. Redis 원자 연산으로 좌석 토큰 선점

세 번째 방식은 예매 오픈 전에 Redis에 좌석 토큰 1만 개를 준비해두고, 사용자가 예매하기를 누르면 Redis의 원자 연산 또는 Lua Script로 토큰을 하나 꺼내는 방식이다. Redis는 단일 스레드 실행 모델과 빠른 인메모리 연산 덕분에 짧은 시간의 대량 요청을 흡수하는 데 유리하다.

장점: DB에 직접 몰리는 쓰기 부하를 크게 줄일 수 있다. 좌석 토큰이 1만 개뿐이라면 Redis 단계에서 자연스럽게 1만 명까지만 예약 보류 상태로 보낼 수 있다.

단점: Redis에서 토큰을 뺐지만 DB 저장에 실패하는 경우, 또는 Redis 장애와 복제 지연이 발생하는 경우를 별도로 복구해야 한다. Redis Lock도 분산 환경에서는 안전성 조건을 신중하게 따져야 한다.

대안 4. 메시지 큐와 대기열 기반 처리

네 번째 방식은 사용자의 요청을 바로 예매 처리하지 않고, 대기열에 넣은 뒤 제한된 수의 워커가 순서대로 처리하는 방식이다. 사용자에게는 “대기 순번” 또는 “진입 가능 상태”를 먼저 반환하고, 실제 좌석 배정은 큐 뒤쪽에서 수행한다.

장점: 서버와 DB가 감당할 수 있는 속도로 요청을 흘려보낼 수 있다. 장애가 났을 때 재처리, 모니터링, 처리량 조절이 쉽다. 피크 트래픽을 완충하는 데 가장 현실적이다.

단점: 사용자는 즉시 성공/실패를 받지 못할 수 있다. 큐 중복 전달, 워커 재시도, 처리 순서, Idempotent Consumer 설계를 추가로 고려해야 한다.

5. 현재 시점에서 선호하는 설계

내가 더 적합하다고 보는 방식은 대기열 + Redis 좌석 토큰 + DB 최종 제약 조건을 함께 쓰는 하이브리드 구조다. 이유는 단순하다. 10만 명이 동시에 들어오는 상황에서 모든 요청을 DB 트랜잭션으로 바로 처리하는 것은 정합성은 지킬 수 있어도 장애 위험이 크다. 반대로 Redis만 믿고 최종 판매를 확정하기에는 장애 복구와 영속성 측면에서 불안하다.

따라서 역할을 나누는 편이 낫다.

  1. 대기열은 10만 명의 동시 접속을 흡수하고, 서버와 DB에 들어갈 요청 속도를 제한한다.
  2. Redis는 짧은 시간 동안 1만 개의 좌석 토큰을 빠르게 선점시키는 역할을 한다.
  3. DB는 최종 예약과 결제 상태를 저장하는 Source of Truth가 된다.
  4. DB에는 event_id + seat_id, event_id + user_id, idempotency_key 같은 Unique Constraint를 둬 최후의 중복 방어선을 만든다.

이 구조에서는 Redis에서 토큰을 얻었다고 바로 “구매 확정”이 아니다. 먼저 PENDING_RESERVED 상태의 예약을 만들고, 제한 시간 안에 결제가 완료되면 CONFIRMED로 바꾼다. 결제가 실패하거나 시간이 지나면 예약은 EXPIRED가 되고 좌석 토큰은 회수 대상이 된다.

6. 설계 흐름 예시

초기 설계 흐름은 다음과 같이 잡을 수 있다.

  1. 예매 오픈 전에 좌석 1만 개를 DB에 등록하고, Redis에도 동일한 좌석 토큰을 준비한다.
  2. 사용자가 접속하면 바로 예매 API로 보내지 않고 대기열 토큰을 발급한다.
  3. 대기 순번이 된 사용자만 예매 API를 호출할 수 있게 한다.
  4. 예매 API는 사용자 인증, 중복 요청 키, 대기열 토큰 유효성을 먼저 확인한다.
  5. Redis Lua Script로 “이미 이 사용자가 선점했는지”와 “남은 좌석 토큰이 있는지”를 한 번에 검사한다.
  6. 토큰을 얻으면 DB 트랜잭션으로 예약 행을 생성한다. 이때 Unique Constraint로 좌석 중복과 사용자 중복을 다시 막는다.
  7. DB 저장이 실패하면 Redis 토큰 회수 또는 보정 작업 대상에 넣는다.
  8. 결제가 완료되면 예약 상태를 CONFIRMED로 바꾸고, 실패 또는 타임아웃이면 EXPIRED로 바꾼다.
  9. 주기적인 Reconciliation Job이 Redis, DB, 결제 상태의 불일치를 찾아 보정한다.

여기서 중요한 것은 “한 번에 완벽한 Exactly-once 시스템을 만든다”가 아니다. 분산 시스템에서는 네트워크 실패, 재시도, 중복 메시지가 언제든 생길 수 있다. 그래서 각 단계가 중복 실행되어도 최종 상태가 깨지지 않도록 멱등성, Unique Constraint, 상태 전이 규칙을 함께 두는 것이 현실적인 설계다.

7. 면접 답변처럼 요약하면

문제 정의: 10만 명 동시 접속 상황에서 1만 개 좌석이라는 한정 자원을 중복 없이 배정해야 하는 고경합 동시성 문제다.

핵심 요구사항: 재고 초과 판매 0건, 좌석/사용자/요청 단위 중복 방지, 피크 트래픽 완충, 결제 실패 복구, 장애 후 데이터 보정이 필요하다.

가장 큰 난관: DB Lock만으로 정합성을 지키면 Hot Row 병목이 생기고, Redis만으로 처리하면 영속성과 복구가 약해진다. 정합성과 처리량을 동시에 만족시키는 경계 설계가 어렵다.

선호 방식: 대기열로 유입량을 제어하고, Redis로 좌석 토큰을 빠르게 선점시키며, DB의 Unique Constraint와 트랜잭션으로 최종 정합성을 보장하는 하이브리드 방식을 선택한다.

이 설계의 핵심은 각 기술을 만능 해결책으로 보지 않는 것이다. Redis는 빠른 선점과 부하 흡수에 강하고, DB는 최종 정합성에 강하며, 메시지 큐는 피크 트래픽을 안정적으로 흘리는 데 강하다. 좋은 설계는 이 장점을 역할별로 나누고, 실패했을 때 복구할 수 있는 상태 모델을 함께 갖춘다.

 

이 글은 시스템 설계 학습과 과제 답변을 목적으로 작성한 기술 분석 글입니다. 실제 티켓 예매 시스템에서는 좌석 정책, 결제사 연동 방식, 법적 고지, 장애 대응 프로세스에 따라 세부 설계가 달라질 수 있습니다.