Reservation-System
1 서론
- 본 글에서는 대규모 예약 시스템 설계 시 발생하는 동시성 문제와 해결 방안에 대해 설명합니다.
- 항공권 예약, 공연 티켓팅, 숙박 예약 등 다양한 도메인에서 적용할 수 있는 내 용을 다룹니다.
- 실제 시스템 구현에 필요한 구체적인 기술과 패턴을 소개합니다.
2 시스템 요구사항 및 규모 설정
- 일일 사용자 수: 100만 명
- 피크 시간대 동시 접속자: 10만 명
- QPS (Query Per Second): 5,000
- [[TPS-QPS]] 참고
- TPS (Transaction Per Second): 1,000
- [[TPS-QPS]] 참고
- 데이터 특성:
- 예약 가능 객실 수: 10만 개
- 일일 예약 건수: 5만 건
- 데이터 저장 기간: 1년
3 데이터베이스 선택
- 예약 시스템의 특성상 데이터의 정합성이 매우 중요합니다.
- ACID 특성이 보장되어야 하므로 관계형 데이터베이스(RDBMS)를 선택합니다.
- NoSQL과 RDBMS 비교:
- RDBMS 장점:
- 트랜잭션 지원
- 데이터 정합성 보장
- 복잡한 쿼리 처리 가능
- RDBMS 단점:
- 수평적 확장이 어려움
- 비용이 상대적으로 높음
- NoSQL 장점:
- 수평적 확장 용이
- 높은 처리량
- 유연한 스키마
- NoSQL 단점:
- 제한적인 트랜잭션 지원
- 일관성 보장이 상대적으로 약함
- RDBMS 장점:
4 동시성 문제 해결
4.1 문제 시나리오
- 동일 사용자의 중복 예약:
- 네트워크 오류로 인한 재시도
- 사용자의 중복 클릭
- 서로 다른 사용자의 동시 예약:
- 마지막 남은 1개 객실에 대한 동시 예약 시도
- 경쟁 상태(Race Condition) 발생
4.2 클라이언트 측 해결 방안
- 중복 제출 방지:
- 예약 버튼 비활성화
- 로딩 인디케이터 표시
- 멱등성 키 사용:
- 각 요청에 고유한 식별자 부여
- 서버에서 중복 요청 필터링
멱등성 키 구현 예시
const reservationRequest = async (roomId) => {
const idempotencyKey = generateUUID();
try {
const response = await axios.post('/api/reservations', {
roomId,
idempotencyKey
});
return response.data;
} catch (error) {
if (error.response.status === 409) {
// 중복 요청 처리
return error.response.data;
}
throw error;
}
}
4.3 데이터베이스 격리 수준
- READ UNCOMMITTED:
- 더티 리드 발생 가능
- 예약 시스템에 부적합
- READ COMMITTED:
- 더티 리드 방지
- 반복 가능하지 않은 읽기 발생 가능
- REPEATABLE READ:
- 반복 가능한 읽기 보장
- 팬텀 리드 발생 가능
- SERIALIZABLE:
- 가장 강력한 격리 수준
- 성능 저하가 큼
4.4 락(Lock) 전략
4.4.1 비관적 락
- 트랜잭션 시작 시점에 락을 획득합니다.
- 동시성이 낮고 충돌이 자주 발생하는 경우 적합합니다.
비관적 락 구현 예시
BEGIN TRANSACTION;
SELECT * FROM rooms
WHERE id = :roomId
FOR UPDATE;
UPDATE rooms
SET availability = availability - 1
WHERE id = :roomId
AND availability > 0;
INSERT INTO reservations (room_id, user_id, created_at)
VALUES (:roomId, :userId, NOW());
COMMIT;
4.4.2 낙관적 락
- 충돌이 발생하지 않을 것이라 가정하고 버전 정보를 이용합니다.
- 동시성이 높고 충돌이 적은 경우 적합합니다.
낙관적 락 구현 예시
@Entity
public class Room {
@Id
private Long id;
@Version
private Long version;
private Integer availability;
public void decreaseAvailability() {
if (availability > 0) {
availability--;
} else {
throw new NoAvailabilityException();
}
}
}