두 트랜잭션이 같은 자리 1개를 보고 둘 다 예약합니다.
좌석 하나가 두 명에게 팔렸습니다 — 중복 예약(오버부킹).
DB 동시성 미니랩
남은 자리는 하나. 두 사람이 같은 순간에 예약 버튼을 누릅니다. 잠금이 없으면 좌석 하나가 두 명에게 팔려요. 트랜잭션과 잠금이 어떻게 “한 명만 성공”을 만드는지 한 단계씩 따라가 보세요.
방식을 고르고 버튼을 누르세요
두 트랜잭션이 같은 자리 1개를 보고 둘 다 예약합니다.
‘예약 경쟁 시작’을 누르면 두 사용자가 동시에 마지막 좌석을 예약하는 과정을 한 단계씩 보여드려요.
아직 시작 전
공연 좌석 (DB 행)
아직 시작 전
한눈에 비교
두 트랜잭션이 같은 자리 1개를 보고 둘 다 예약합니다.
좌석 하나가 두 명에게 팔렸습니다 — 중복 예약(오버부킹).
SELECT … FOR UPDATE로 먼저 잠그고 한 명씩 순서대로 처리합니다.
A만 성공, B는 잠금이 풀린 뒤 '자리 없음'을 보고 거절됩니다.
일단 진행하고, 커밋할 때 버전 번호로 충돌을 감지합니다.
A만 성공, B는 버전이 바뀌어 0행 갱신 → 충돌로 거절됩니다.
이 데모에 대하여
남은 좌석이 하나인데 두 사용자가 같은 순간에 예약 버튼을 누르는 상황을, 세 가지 처리 방식으로 각각 재생합니다. 조건은 완전히 같고 달라지는 것은 데이터베이스를 다루는 방법뿐입니다. 잠금 없이 처리하면 좌석 하나가 두 명에게 팔리고, 나머지 두 방식은 각기 다른 방법으로 한 명만 성공시킵니다.
01
읽고 쓰는 사이가 문제입니다.
좌석을 확인하고 예약을 기록하기까지는 시간 간격이 있습니다. 그 사이에 다른 요청이 같은 값을 읽어 가면 둘 다 자리가 있다고 판단합니다. 화면에서 두 트랜잭션이 각각 읽은 값을 따로 표시하는 것은 이 간격을 보이게 하려는 것이고, 사고의 원인이 두 요청이 동시에 온 것 자체가 아니라 이 틈에 있다는 점이 중요합니다.
02
비관적 잠금은 미리 막습니다.
읽는 시점에 그 행을 잠가 두면 다른 요청은 잠금이 풀릴 때까지 기다립니다. 기다리던 쪽은 앞의 처리가 끝난 뒤에야 값을 읽으므로 이미 0 이 된 좌석을 보고 포기합니다. 확실하지만 기다리는 시간이 생기고, 잠금을 오래 쥐고 있으면 뒤의 요청들이 줄줄이 밀립니다.
03
낙관적 잠금은 나중에 확인합니다.
일단 잠그지 않고 진행하되, 기록할 때 내가 읽었던 버전이 아직 그대로인지 확인합니다. 그사이 누가 바꿨다면 버전이 달라져 있으므로 그 시점에 거절됩니다. 기다림이 없어 평소에는 빠르지만, 충돌이 잦으면 거절과 재시도가 늘어나 오히려 손해입니다.
04
버전 번호를 화면에 노출합니다.
각 단계마다 현재 버전과 각 트랜잭션이 읽었던 버전을 함께 표시합니다. 이 값이 어긋나는 순간이 거절이 일어나는 근거이므로, 숫자를 감추면 왜 실패했는지 설명할 수 없습니다. 버전이 예약할 때마다 하나씩 올라간다는 점도 화면에서 따라갈 수 있습니다.
05
누가 잠금을 쥐고 있는지 표시합니다.
비관적 잠금 시나리오에서는 잠금을 가진 쪽을 별도로 표시합니다. 기다리는 상태와 실행 중인 상태가 화면에서 같아 보이면 왜 한쪽이 멈춰 있는지 알 수 없기 때문입니다. 실제 장애 상황에서도 어느 트랜잭션이 무엇을 쥐고 있는지가 원인 파악의 출발점이 됩니다.
06
질의 형태의 로그를 함께 남깁니다.
각 단계에 대응하는 질의 한 줄을 함께 보여줍니다. 화면의 그림만 보면 개념은 이해되지만 실제 코드로 어떻게 옮기는지는 연결되지 않기 때문입니다. 로그를 위에서부터 읽으면 세 방식이 실제로 어떤 문장을 더 쓰거나 덜 쓰는지가 드러납니다.
07
결말을 명시적으로 판정합니다.
마지막 단계는 중복 예약인지 한 명만 성공했는지를 값으로 들고 있습니다. 화면을 보고 사람이 세어 판단하게 두면 오해할 여지가 있고, 자동 테스트가 확인할 대상도 필요하기 때문입니다. 세 방식 중 잠금 없음만 다른 결말을 갖는다는 점이 이 데모의 결론입니다.
08
어느 쪽이 낫다고 정하지 않습니다.
두 잠금 방식은 상황에 따라 유불리가 갈립니다. 충돌이 드물면 낙관적 쪽이 빠르고, 충돌이 잦으면 거절과 재시도가 반복되어 비관적 쪽이 오히려 낫습니다. 화면이 두 방식을 나란히 두고 어느 쪽에도 좋음 표시를 붙이지 않는 것은, 고를 기준이 성능 자체가 아니라 그 서비스에서 충돌이 얼마나 자주 일어나느냐이기 때문입니다.
09
거절도 정상 동작입니다.
낙관적 잠금에서 한쪽이 거절되는 것은 오류가 아니라 설계된 결과입니다. 다만 사용자에게 그대로 오류 화면을 보여주면 안 되고, 다시 시도하거나 다른 자리를 안내하는 처리가 필요합니다. 화면은 거절까지만 보여주므로 그 뒤의 사용자 경험은 별도로 설계해야 합니다.
10
값을 직접 계산해 쓰지 않는 방법도 있습니다.
읽은 값을 프로그램에서 빼고 다시 저장하는 대신, 남은 좌석을 하나 줄이라는 조건부 갱신 한 문장으로 처리하면 읽고 쓰는 사이의 틈 자체가 사라집니다. 이 화면은 세 방식의 차이를 보이려고 읽기와 쓰기를 나눠 두었지만, 실제로는 이렇게 한 문장으로 줄이는 것이 가장 간단한 해법인 경우가 많습니다.
가장 단순한 충돌 하나만 다룹니다.
이 화면은 데이터베이스에 연결하지 않고 미리 만들어 둔 단계를 재생합니다. 좌석 하나와 사용자 둘은 문제가 드러나는 최소 조건이며 실제 예약 시스템의 규모와는 다릅니다. 실제 구현에서는 여기 없는 것이 많은데, 격리 수준을 어떻게 잡느냐에 따라 같은 코드의 결과가 달라지고, 잠금 순서가 엇갈려 서로를 기다리는 교착 상태가 생길 수 있으며, 여러 자리를 한꺼번에 예약하거나 결제와 묶여 있는 경우에는 되돌리는 처리까지 필요합니다. 데이터베이스가 여러 대로 나뉘어 있으면 버전 비교만으로 해결되지 않는 문제가 새로 생깁니다. 화면의 질의는 개념을 보여주기 위한 형태이므로 그대로 옮겨 쓸 수 있는 코드가 아닙니다. 실제로는 쓰는 데이터베이스마다 문법과 동작이 다르므로 해당 제품의 문서를 확인해야 합니다.
실험을 마쳤다면