대용량 트래픽 티켓팅 시스템 설계하기
1. 질문
프로듀서로부터 1000개의 메시지가 메시지 큐에 들어왔다고 하자. 큐에 들어온 메시지는 순차적으로 컨슈머에게 전달되며, 큐 내부에서는 순서가 보장된다. 컨슈머가 1개인 경우 컨슈머에서 메시지의 순서가 보장되겠지만, 컨슈머가 여러 개인 경우 컨슈머에서도 그 순서가 보장된다고 확신할 수 없다.
선착순으로 1000개의 좌석 티켓을 판매하는 시스템을 개발하는 상황이다. 10000명 정도의 트래픽이 예상되며, 안정적으로 1000개를 판매하기 위해서는 어떤 식으로 시스템을 설계하고 구현해야 할까?
2. 고려사항
대용량 트래픽
티켓은 1000장인데, 10배에 달하는 동시 접속자 트래픽이 예상된다. 티켓 좌석에 대한 읽기/쓰기 요청을 모두 데이터베이스로 보낼 시 부하 문제가 발생할 수 있다. 적절한 캐싱 전략을 구성해 읽기/쓰기 요청을 처리한다면 높은 트래픽을 관리할 수 있을 것이다.
비동기 처리
각 사용자의 요청을 한 대의 서버가 처리한다면, 서버 부하로 응답 속도가 저하될 수 있다. 각 사용자의 요청을 나눠서 처리해야 할 필요가 있다. 프로듀서-컨슈머 모델에서 티켓 결제 기능을 담당하는 컨슈머를 여러 대 생성한 후, 메시지 브로커를 활용하면, 순차적으로 들어오는 요청들을 적절한 컨슈머에 분산해 전달함으로써 사용자 요청을 보다 안정적으로 처리할 수 있을 것이다.
3. 비즈니스 분석
좌석이 선점되는 시점은 티켓팅 서비스마다 다를 수 있지만, 일반적으로 두 가지 방식이 있다.
1. 좌석 선택 즉시 선점 (Soft Lock)
- 사용자가 좌석을 선택하는 순간 해당 좌석을 일정 시간 동안 다른 사용자에게 할당되지 않도록 잠근다.
- 예: KTX, 항공권 예매 시스템
- 일반적으로 10~20분 정도 유효 시간을 부여하며, 이 시간 안에 결제가 완료되지 않으면 좌석이 자동으로 해제된다.
2. 결제 완료 후 확정 (Hard Lock)
- 좌석은 결제가 완료되는 시점에서만 확정된다.
- 예: 콘서트 티켓팅, 스포츠 경기 예매
- 좌석을 선점하지 않고, 결제 승인된 순서대로 좌석이 확정되기 때문에, 결제하는 동안 좌석이 다른 사람에게 넘어갈 수도 있다.
내 경험 상, 콘서트 티켓팅이나 축구 경기 티켓팅의 경우 결제까지 완료된 경우에만 좌석이 선점되는 방식이 많았다. 그 이유에 대해서 생각해봤다. 좌석을 즉시 선점(Soft Lock)하면 많은 사용자가 좌석을 대량으로 확보한 뒤 결제를 포기할 가능성이 커진다. KTX 예매의 경우 좌석 경쟁이 비교적 치열하지 않고, 항공권 좌석 예매의 경우 대부분 모든 결제가 완료되고 체크인하는 시점에 정해진 인원으로 한정해 진행되는 경우가 많아, 큰 문제가 되진 않는다. 그러나 좌석 선택이 먼저 이루어진 후 결제가 진행되는 티켓팅의 경우 인기 공연일수록 좌석 경쟁이 치열하기 때문에 빠른 결제 순으로 확정하는 것이 공정하다.
4. 구현 설계
사용자 시나리오 플로우
1. 티켓팅 대기
- 사용자는 10시에 오픈되는 티켓을 잡기 위해 대기한다.
2. 좌석 선택 페이지 접속
- 10시가 되면 최대 10,000명의 사용자가 동시에 좌석 선택 페이지에 접속을 시도한다.
- 접속 요청을 메시지 큐에 순차적으로 입력하고, 사용자 화면에는 대기열을 표시한다.
3. 좌석 정보 조회
- 컨슈머(Consumer)가 접속 요청 메시지를 처리할 때, 먼저 Redis에서 좌석 관련 데이터가 캐싱되어 있는지 확인한다.
- 캐싱 데이터가 있는 경우: 해당 데이터를 사용자에게 반환한다.
- 캐싱 데이터가 없는 경우: 데이터베이스에서 좌석 정보를 조회한 후, Redis에 캐싱하고 사용자에게 반환한다.
4. 좌석 선택 및 결제 정보 입력 페이지 접속
- 사용자는 원하는 좌석을 선택하고 결제 정보 입력 페이지로 이동을 시도한다.
- 접속 요청을 메시지 큐에 순차적으로 입력하며, 사용자 화면에 대기열을 표시한다.
5. 결제 요청
- 본인의 차례가 되면 결제 정보 입력 후 결제를 요청한다.
- 결제 요청을 메시지 큐에 입력하고, 사용자에게 "결제 시도 중"이라는 알림을 표시한다.
6. 결제 처리
- 컨슈머가 결제 요청을 소비하면 결제 사이트와 연동하여 실제 결제 프로세스를 진행한다.
- 사용자에게 "결제 진행 중"이라는 알림을 띄운다.
- 결제 성공 시
- 사용자에게 결제 성공 메시지를 반환한다.
- 좌석 상태를 선점 상태로 변경한다.
- Redis 캐시를 삭제하고, DB의 좌석 상태를 업데이트한다.
- 결제 실패 시
- 사용자에게 결제 실패 메시지를 반환한다.
- 처음 페이지로 돌아가도록 안내한다.
- 결제 성공 시
- 결제 결과 처리
7. 티켓팅 완료
- 결제가 성공적으로 완료되면 사용자에게 티켓팅 성공 메시지를 출력한다.
설계 고려 사항
1. 데이터베이스 구조 및 성능 최적화
- 읽기 전용 DB와 쓰기 전용 DB를 분리한다.
- 결제 성공 시 쓰기 전용 DB에서 좌석 상태를 업데이트한 후, 읽기 전용 DB와 Redis 캐싱 데이터를 삭제 또는 업데이트한다.
- 쓰기 작업이 읽기 전용 DB 및 Redis에 반영되기까지의 지연 시간을 고려하여 설계한다.
2. 동시 결제 요청 처리 및 중복 결제 방지
- 다수의 컨슈머가 동시에 결제 요청을 처리할 수 있으므로 중복 결제 방지(Double Checking) 로직이 필요하다.
- 쓰기 작업이 완료되기 전까지 동일 좌석에 대한 결제 요청을 방지하는 방법을 고려한다.
- 좌석을 "결제 시도 중" 상태로 Redis에 캐싱하여 중복 요청 차단
- 데이터베이스에서 트랜잭션을 활용하여 동시성 제어
3. 메시지 큐 활용
- 메시지 큐를 활용하여 트래픽을 분산 처리하고, 요청을 순차적으로 처리할 수 있도록 한다.
- 대기열 시스템을 구축하여 사용자 경험을 향상한다.
4. 장애 대비 및 복구 방안
- Redis 캐시가 유실될 경우를 대비해 DB와 동기화하는 주기적인 백업 프로세스 운영을 고려할 수 있다.
- 결제 처리 중 장애 발생 시 결제 요청 재시도 정책을 도입하여 안정성을 확보한다.
5. 결론
대량의 사용자가 동시에 접속하는 상황에서 안정적인 티켓팅 시스템을 운영하기 위한 방안을 고려하여 설계해보았다.
- 메시지 큐를 활용한 순차적 처리
- Redis 캐싱을 활용한 빠른 응답 및 중복 결제 방지
- 읽기/쓰기 DB 분리를 통한 최적화로 안정적인 성능과 사용자 경험 보장