호텔 예약 시스템에서 사용자가 객실 예약을 할 때 발생할 수 있는 동시성 문제에 대해서 알아보겠다. 아래와 같이 두 가지 시나리오를 예상해 볼 수 있다.
이러한 동시성 문제는 나쁜 사용자 경험을 제공하고 비즈니스 적으로 심각한 이슈를 발생할 수 있기 때문에 이러한 문제가 발생하지 않도록 방지해야 된다. 이어서 위 두 가지 시나리오에 대해서 자세히 살펴보고 해결 방법을 알아보겠다.
만약 사용자가 연속으로 예약 버튼을 누르게 되면 예약 요청이 두 번 연속으로 전송될 수 있다. 그러면 객실이 중복으로 예약되는 문제가 발생할 수 있다.
이 문제는 클라이언트 측에서 예약 버튼을 누를 때 버튼을 비활성화 처리하면 간단하게 해결할 수 있지만 모든 상황에서의 해결책은 될 수 없다. 예를 들어서 네트워크 지연 등의 문제로 클라이언트 측에서 의도하지 않은 재요청 등이 발생하면 여전히 다중 요청이 발생할 가능성이 존재한다.
따라서, 클라이언트 측에서 처리를 했다고 하더라도 서버 측에서도 이러한 문제에 대해서 예측하고 대비를 해둬야 한다. 서버 측에서는 아래와 같이 멱등 API 구현을 통해서 방지할 수 있다.
전체 예약 가능 객실 수가 100개이고 현재 예약된 객실 수가 99개인 상황을 가정해보자. 이때 두 명의 사용자가 동시에 마지막 남은 객실에 대해서 예약 요청을 진행한다면 아래와 같은 경쟁 조건이 발생하게 된다.
결과적으로 잔여 객실이 1개인 상황에서도 2명이 예약했기 때문에 데이터의 일관성이 지켜지지 않았다. 이로 인해서 두 사용자가 호텔에 갔더니 서로 객실을 예약했다고 주장하는 상황이 펼쳐지는 등 비즈니스적인 문제도 발생할 수 있다.
따라서, 이러한 곤란한 상황을 겪지 않으려면 동시성 문제를 방지해야 한다.
위와 같은 동시성 문제는 데이터베이스 락을 활용해서 해결할 수 있다. 락 메커니즘은 대표적으로 비관적 락과 낙관적 락 방식이 있다.
비관적 락은 먼저 쿼리한 사용자가 레코드를 선점하고 락을 거는 방식이다.
아래 그림과 같이 트랜잭션1에서 먼저 쿼리를 실행해서 락을 걸어두면 트랜잭션2는 트랜잭션1이 완료될 때까지 기다리고 나서 갱신을 수행할 수 있다.
MySQL에서는 비관적 락을 “SELECT ••• FOR UPDATE” 쿼리 문으로 구현할 수 있다.
장점
단점
낙관적 락은 레코드의 버전을 관리하고 해당 버전으로 충돌을 감지해서 동시성을 제어하는 방식이다. 버전 값으로는 일반적으로는 정수 번호를 사용하지만 상황에 따라서 타임스탬프나 해시 값을 사용할 수 있다.
다음 그림은 두 사용자가 데이터 갱신을 성공하는 사례와 실패하는 사례다.
SQLAlchemy에서는 간단하게 version_id_col에 컬럼 지정으로 구현 가능
class User(Base): __tablename__ = "user" id = mapped_column(Integer, primary_key=True) version_id = mapped_column(Integer, nullable=False) __mapper_args__ = {"version_id_col": version_id}
낙관적 락은 데이터베이스에 락을 걸지 않기 때문에 일반적으로 비관적 락보다 빠르다. 하지만 동시성 수준이 아주 높아지면 성능이 급격하게 나빠진다. 예를 들면 다음과 같은 상황이 있을 수 있다.
장점
단점
위와 같이 낙관적 락 방식은 동시성 수준이 높은 상황에서는 오히려 좋지 않은 선택이 될 수 있다. 따라서, 각 상황에 맞는 락 매커니즘 방식을 선택하는 게 중요하다.
비관적 락을 선택해야 하는 경우
낙관적 락을 선택해야 하는 경우
이처럼 호텔 예약 시스템에서 발생할 수 있는 동시성 문제와 해결 방법을 살펴보았다.
동시성 문제가 실무적인 이슈에 좀 더 가까워서 먼저 다뤄봤는데, 추후에는 이번 글에서 다루지 못한 트랜잭션의 ACID 특성이나 격리 수준과 같이 이론적인 부분에 대해서도 다뤄보도록 하겠다.