- 내 코드에서 여러 데이터 변경이 함께 일어나야 하는 곳은 어디인가?
- 그 변경 중 일부만 성공했을 때 데이터는 어떤 상태가 되는가?
- 사용자는 그 상태에서 무엇을 보게 되는가?
- 어디까지를 하나의 작업 단위로 묶을 것인가?
- 지금까지 정한 API/에러 처리 규칙은 대기 승인 기능에서도 그대로 유효한가?
현재 코드에는 @Transactional이 붙어 있지 않다. 다만 현재 구현된 기능 대부분은 실제 데이터 변경이 한 번의 INSERT, UPDATE, DELETE로 끝난다.
대기 순번도 별도 컬럼으로 저장하지 않고, reservation_waiting.requested_at과 id를 기준으로 조회 시 계산한다. 그래서 대기 신청이나 대기 취소 시 "대기 ROW 변경 + 순번 재정렬" 같은 두 번째 데이터 변경은 현재 존재하지 않는다.
하지만 대기 승인 기능을 추가하면 예약, 대기, 대기 순서가 하나의 흐름 안에서 함께 바뀌어야 한다. 이때부터 트랜잭션 경계가 중요해진다.
| 기능 | 함께 변경되는 데이터 | 첫 번째만 성공 시 상태 | 사용자가 보게 되는 것 |
|---|---|---|---|
| 예약 생성 | 현재 구현 기준으로는 reservation ROW 추가 1개만 변경된다. 참조 데이터인 theme, reservation_time은 조회만 한다. |
예약 INSERT만 있으므로 중간 실패 상태는 거의 없다. 다만 중복 확인 후 INSERT 전에 같은 슬롯 예약이 동시에 들어오면 DB UNIQUE 제약이 최종 방어선이 된다. | 성공하면 예약 목록에 새 예약이 보인다. 중복이면 정책상 409 응답을 기대하지만, 동시성으로 DB 제약 예외가 직접 터지면 의도한 에러 응답이 깨질 수 있다. |
| 예약 변경 | 현재 구현 기준으로는 reservation.date, reservation.time_id UPDATE 1개만 변경된다. |
UPDATE 1개라 일부 성공 상태는 없다. 다만 중복 확인 후 UPDATE 사이에 다른 요청이 같은 슬롯을 차지하면 DB UNIQUE 제약 또는 예외 처리가 필요하다. | 성공하면 변경된 날짜/시간이 보인다. 충돌 시 "동일한 시기에 예약을 할 수 없습니다." 같은 정책 메시지를 기대한다. |
| 예약 취소 | 현재 구현 기준으로는 reservation DELETE 1개만 변경된다. 삭제 전 reservation_waiting 존재 여부를 조회해 대기가 있으면 삭제를 막는다. |
DELETE 1개라 일부 성공 상태는 없다. 다만 대기 존재 확인 직후 다른 사용자가 대기를 추가하는 동시 요청이 있으면 삭제와 대기 생성이 엇갈릴 수 있다. | 대기가 없으면 예약이 사라진다. 대기가 있으면 예약 삭제가 거부되고, 사용자는 "예약 대기가 존재하는 예약은 바로 삭제할 수 없습니다."를 보게 된다. |
| 내 예약 취소 | reservation DELETE 1개만 변경된다. 이름 검증, 과거 예약 검증, 대기 존재 검증은 조회/검증이다. |
DELETE 1개라 일부 성공 상태는 없다. 대기 존재 확인과 삭제 사이의 동시성은 예약 취소와 동일하게 위험하다. | 성공하면 내 예약 목록에서 예약이 사라진다. 지난 예약이거나 대기가 있으면 취소 불가 메시지가 표시된다. |
| 대기 신청 | 현재 구현 기준으로는 reservation_waiting ROW 추가 1개만 변경된다. 순번은 저장하지 않고 조회 시 계산한다. |
INSERT 1개라 "대기는 추가됐지만 순번 갱신 실패" 상태는 없다. 다만 중복 확인 후 INSERT 전에 같은 이름의 대기가 동시에 들어오면 DB UNIQUE 제약이 최종 방어선이 된다. | 성공하면 내 예약 조회에서 WAITING 상태와 계산된 sequence가 보인다. 동시성 예외를 의도한 409로 처리하지 못하면 사용자에게 의미 없는 서버 오류가 보일 수 있다. |
| 대기 취소 | 현재 구현 기준으로는 reservation_waiting DELETE 1개만 변경된다. 남은 대기의 순번은 조회 시 다시 계산된다. |
DELETE 1개라 일부 성공 상태는 없다. 별도 순번 컬럼이 없기 때문에 재정렬 실패 상태도 없다. | 취소한 사용자의 내 예약 목록에서 해당 대기가 사라진다. 다른 대기자의 순번은 다음 조회 때 자연스럽게 당겨져 보인다. |
| 내 예약 조회 | 데이터 변경은 없다. reservation과 reservation_waiting을 UNION으로 조회하고, 대기 순번은 ReservationWaitingLine에서 계산한다. |
변경이 없으므로 일부 성공 상태는 없다. 다만 조회 2번 사이에 대기 데이터가 바뀌면 목록과 순번 계산 기준이 순간적으로 어긋날 수 있다. | 예약은 sequence 0, 대기는 현재 계산된 sequence로 보인다. |
가장 위험한 기능은 앞으로 추가할 "대기 승인"이다. 현재 기능 중에서는 예약 삭제와 대기 신청이 엇갈리는 동시성 상황이 가장 신경 쓰인다. 예약 삭제는 대기가 없다고 확인한 뒤 삭제하는데, 그 사이에 대기가 생기면 "대기가 걸린 예약이 삭제되는 상태"가 만들어질 수 있다.
현재 코드는 순번을 저장하지 않기 때문에 "대기 ROW는 추가됐는데 순번이 비어 있음" 같은 문제는 없다. 이 결정은 대기 신청/취소의 작업 단위를 작게 만들어준다.
다만 "조회 후 변경" 구조가 많다. 중복 예약 확인 후 예약 저장, 대기 중복 확인 후 대기 저장, 대기 존재 확인 후 예약 삭제는 모두 동시 요청이 들어오면 검증 시점과 변경 시점 사이에 상태가 달라질 수 있다.
사용자가 다음 요청을 보내면 DB에 실제로 남은 상태를 기준으로 화면이 다시 계산된다. 그래서 순번 자체는 복구되지만, 의도하지 않은 예약 삭제나 중복 요청 예외가 발생하면 사용자 경험은 깨질 수 있다.
선택: 자동 전환을 선택한다. 예약자가 취소했을 때 대기 1번을 바로 예약으로 전환해야 사용자가 별도 관리자 개입 없이 자연스럽게 자리를 이어받을 수 있기 때문이다.
- 예약 취소 요청을 받는다.
- 취소 대상 예약을 조회하고 취소 가능한 예약인지 검증한다.
- 해당 예약의 대기 목록 중 가장 빠른 대기 1번을 찾는다.
- 대기자가 없으면 기존처럼 예약만 삭제한다.
- 대기자가 있으면 기존 예약의 이름을 대기 1번 사용자 이름으로 바꾸거나, 기존 예약을 삭제하고 새 예약을 만든다.
- 예약으로 전환된 대기 ROW를 삭제한다.
- 남은 대기 순번은 별도 저장하지 않고 다음 조회 때 다시 계산한다.
| 변경 | 하나로 묶을지 | 이유 |
|---|---|---|
| 기존 예약을 대기 1번 사용자 예약으로 전환 | 묶는다 | 대기 승인 흐름의 핵심 상태 변경이다. 이 변경만 성공하고 대기 ROW 삭제가 실패하면 한 사용자가 예약과 대기를 동시에 가진다. |
| 승인된 대기 ROW 삭제 | 묶는다 | 예약으로 전환된 사람은 더 이상 대기자가 아니어야 한다. 예약 전환과 같은 작업 단위다. |
| 남은 대기 순번 재정렬 | 따로 처리하지 않는다 | 순번을 저장하지 않고 requested_at, id 기준으로 계산하므로 별도 UPDATE가 필요 없다. 이 구조를 유지하면 승인 시 변경할 데이터가 줄어든다. |
| 알림 발송 같은 외부 부수 효과 | 따로 처리한다 | DB 상태 변경과 외부 알림은 실패 성격이 다르다. 알림 실패 때문에 예약 전환을 롤백할지 별도 정책이 필요하며, 현재 범위에서는 DB 트랜잭션 밖으로 두는 편이 낫다. |
하나의 작업 단위는 "한 슬롯의 소유자가 바뀌고, 그에 맞게 대기 상태가 제거되는 것"까지다.
즉, 예약 전환과 승인된 대기 삭제는 같은 트랜잭션으로 묶는다. 둘 중 하나만 성공한 상태는 허용하지 않는다.
남은 대기자의 순번은 저장값이 아니라 계산값이므로 트랜잭션에 포함할 데이터 변경이 아니다. 이 구조를 계속 유지하면 대기 승인 후 다음 조회에서 자연스럽게 2번이 1번으로 보인다.
| 상황 | 상태 | 허용 가능 여부 |
|---|---|---|
| 예약 전환 성공, 승인된 대기 삭제 실패 | 한 사용자가 예약자이면서 동시에 대기자로 남는다. 내 예약 조회에서 같은 슬롯이 RESERVATION과 WAITING으로 함께 보일 수 있다. |
허용 불가 |
| 승인된 대기 삭제 성공, 예약 전환 실패 | 대기 1번은 사라졌지만 실제 예약자는 바뀌지 않는다. 사용자는 자기 대기가 사라졌는데 예약도 없는 상태를 본다. | 허용 불가 |
| 예약 삭제 성공, 새 예약 생성 실패 방식으로 구현 | 해당 슬롯의 예약 자체가 비어버릴 수 있다. 뒤 대기자들은 여전히 기존 reservation_id를 참조해야 하므로 참조 무결성 문제도 생길 수 있다. | 허용 불가 |
| 알림 발송 실패 | DB 상태는 예약 전환 완료인데 사용자에게 알림만 가지 않는다. 다음 내 예약 조회에서는 예약으로 보인다. | 정책에 따라 허용 가능 |
동시에 두 사용자가 같은 슬롯에 대기 신청을 보내면 reservation_waiting의 UNIQUE (reservation_id, name) 제약은 같은 이름 중복은 막는다. 그러나 서로 다른 이름의 동시 대기는 모두 성공할 수 있고, 순번은 requested_at, id 순서로 정해진다.
동시에 두 관리 흐름 또는 취소 흐름이 같은 예약의 대기 1번을 승인하려 하면 같은 대기자가 두 번 처리될 수 있다. 승인 대상 대기 ROW를 잠그거나, 삭제/전환 결과의 affected row count를 확인해야 한다.
예약 취소와 대기 신청이 동시에 일어나면 현재 구조에서는 validateNoWaiting 확인 시점과 reservation DELETE 시점 사이가 위험하다. 대기가 생성된 직후 예약이 삭제되면 FK 제약으로 삭제가 막히거나, 제약 동작에 따라 대기 생성/삭제 중 하나가 실패한다. 이 예외도 사용자에게 의도한 에러 응답으로 바꿔야 한다.
사이클1에서 정한 "정책 위반은 409, 존재하지 않는 리소스는 404, 유효하지 않은 입력은 400" 규칙은 대기 승인 기능에도 유효하다.
추가로 필요한 규칙은 동시성으로 DB 제약이 깨졌을 때도 사용자가 이해할 수 있는 정책 응답으로 바꾸는 것이다. 예를 들어 동시에 승인되어 이미 처리된 대기라면 409로 "이미 처리된 대기입니다."를 반환하는 식이다.
대기 승인 기능의 핵심은 @Transactional을 어디에 붙일지가 아니라, "예약 전환과 승인된 대기 삭제는 사용자가 하나의 사건으로 인식한다"는 점이다. 사용자가 하나의 사건으로 보는 상태 변화는 하나의 작업 단위로 묶어야 한다.