예약·변환·저장·확정이 순서대로 성공하거나 실패 뒤 예약을 해제하도록 만들었다고 하자. 요청 하나만 실행하면 흐름이 정확해 보인다. 그런데 두 요청이 동시에 마지막 허용량을 예약하면 어떨까? 각 요청의 보상이 잘 작성돼 있어도 둘 다 자원이 남아 있다고 판단하고 예약할 수 있다. 순서 조정과 동시성 제어는 같은 문제가 아니다.
Saga에서는 로컬 트랜잭션이 단계별로 커밋된다. 예약 이후 변환이 끝날 때까지 그 예약은 이미 다른 요청에서 볼 수 있는 데이터다. 하나의 DB 트랜잭션처럼 전체 업무의 중간 상태가 숨겨지는 것은 아니다. 코레오그래피와 오케스트레이션 중 무엇을 선택했는지도 이 사실을 바꾸지 않는다.
예약 중 상태는 오류가 아니라 설계 대상이다
예제에서 총 허용량이 하나이고 두 요청 A와 B가 거의 동시에 시작한다고 가정하자. A가 남은 수량 = 1을 읽고, B도 같은 값을 읽은 다음 각각 예약 행을 추가한다. 조회와 쓰기를 안전하게 연결하지 않았다면 예약 두 개가 생길 수 있다. 이후 변환이 성공하면 둘 다 확정하려 할 것이다.
이 문제를 ‘나중에 하나를 보상하면 된다’고 처리할 수도 있지만 그 과정에서 허용하지 않은 중복 사용이 이미 발생할 수 있다. 실제 작업을 시작하기 전에 총량을 제한해야 한다면 예약 생성 자체가 경쟁 요청 사이에서 조건을 지켜야 한다. 해당 원장을 소유한 서비스의 DB 트랜잭션과 제약, 조건부 갱신 같은 수단을 검토한다.
UPDATE capacity
SET available = available - 1
WHERE resource_id = :resourceId
AND available >= 1;
이 SQL은 아이디어를 보여 주는 일부다. 수정된 행 수를 확인하고 예약 기록 쓰기까지 같은 로컬 트랜잭션에 묶어야 한다. 선택 DB의 격리 수준과 동시 갱신 동작도 확인해야 한다. 이 한 문장만 복사하면 모든 자원 예약 문제가 해결된다고 주장하지 않는다. 업무 키 중복 방지와 해제 시 총량 복원도 별도로 필요하다.
중간 상태를 읽는 쪽에도 규칙이 필요하다
예약 원장을 안전하게 만들었더라도 결과 조회 서비스가 RESERVED를 완료로 해석하면 사용자에게 잘못된 상태를 보여 줄 수 있다. 결과 파일이 저장됐지만 예약 확정은 아직 끝나지 않은 시점도 있다. 이런 상태를 처리 중, 확정 대기, 완료 중 어떻게 노출할지 명시해야 한다.
| 내부에서 관찰한 상태 | 바로 단정하면 안 되는 것 | 필요한 판단 |
|---|---|---|
| 예약 존재 | 문서 처리가 성공함 | 변환·저장·확정 진행 확인 |
| 결과 파일 존재 | 사용자에게 공개 가능한 완료 결과 | 공개 조건과 확정 상태 확인 |
| 해제 요청 전송 | 예약이 실제 해제됨 | 응답 또는 원장 상태 확인 |
| timeout 발생 | 외부 작업이 중지됨 | 진행 중 효과와 늦은 결과 확인 |
상태 이름을 더 많이 만든다고 문제가 자동으로 해결되지는 않는다. 각 상태에서 허용되는 명령과 조회 의미가 있어야 한다. 이미 확정된 예약을 해제할 수 있는지, 취소 처리 중 새 확정이 도착하면 무엇을 우선하는지 정한다. 이런 규칙이 없으면 어느 조정 도구를 사용하더라도 서로 다른 호출 순서에서 결과가 갈린다.
Saga에 자동 격리가 없다는 말은 아무 제어도 할 수 없다는 뜻은 아니다. 예약 상태로 사용 가능량을 줄이거나, 버전 조건으로 갱신을 제한하거나, 특정 업무가 끝날 때까지 충돌하는 명령을 거절하는 방식을 설계할 수 있다. 각각 대기·충돌·복구 비용이 있으므로 실제 필요한 불변 조건에 맞춰 선택한다. Saga 패턴 설명은 이 격리 부족을 명시적인 고려 사항으로 다룬다.
두 요청을 동시에 실행해 본다
이번 실험은 처리량 측정이 아니라 마지막 하나의 자원을 지키는지 확인하는 것이다. 두 스레드가 같은 시점에 예약을 시도하도록 준비한다. 처음에는 조회 후 INSERT를 별도로 수행하는 구현을 사용하고, 다음에는 선택 DB에 맞는 조건부 변경과 트랜잭션을 적용한다. 실제 재현 여부는 스케줄링과 DB 설정에 따라 달라지므로 한 번 겹치지 않았다고 안전하다고 결론내리지 않는다.
- 허용량과 기존 예약을 알려진 초기 상태로 준비한다.
- 두 요청이 남은 수량을 확인한 뒤 함께 예약하도록 실행 지점을 맞춘다.
- 성공 응답 수와 원장의 활성 예약 수를 비교한다.
- 한 예약의 해제를 반복 호출한 뒤 사용 가능량이 과도하게 증가하지 않는지 확인한다.
예상되는 위험은 안전하지 않은 구현에서 두 예약이 모두 만들어지는 것이다. 수정판의 목표는 하나만 허용되고 다른 요청은 명시적인 거절 또는 재시도 가능한 충돌을 받는 것이다. 이 글은 계획된 학습 실험이며 측정 결과를 제공하지 않는다. 나중에 Temporal Worker를 여러 개 띄웠을 때도 같은 예약 서비스 계약이 유지되는지 다시 확인한다.
Temporal Workflow 하나 안에서 순서대로 코드를 실행한다고 모든 Workflow 사이의 외부 자원 경쟁까지 해결되지는 않는다. 전체 요청이 공유하는 한도라면 그 한도를 책임지는 곳이 있어야 한다. 로컬 Worker의 동시 실행 수를 줄이는 것과 서비스 전체의 업무 한도를 지키는 것도 구분해야 한다.
이해 확인
각 Saga가 자신의 예약을 정확히 보상한다면 총 허용량 초과도 자동으로 막힐까? 결과 파일을 찾았다는 이유만으로 문서 상태를 완료로 표시하면 어떤 중간 상태를 놓칠 수 있을까? 예약 원장과 조회 계약을 따로 떠올려 답해 보자.
참고 자료
- Saga 패턴: 자동 격리가 없을 때 필요한 별도 대책.
- PostgreSQL 트랜잭션 격리: 선택 격리 수준과 동시 변경 동작 확인용.
- AWS Saga orchestration: 조정 방식과 참여자 일관성의 고려 사항.