주문 상태를 enum으로 저장했지만 두 요청이 동시에 변경하거나 외부 결제 결과가 불확실하면 중복 처리가 생깁니다. 상태 전이와 재시도를 어떻게 설계하나요?
잠깐, 답변을 보기 전에
먼저 내 말로 설명해 볼까요?
답변과 점검 포인트 펼치기답변 접기
구두 답변
정의를 외우기보다, 원리와 조건을 연결해 설명해 보세요.
PENDING, RUNNING, DONE 같은 enum은 현재 상태를 표현할 뿐, 어떤 요청이 어떤 조건에서 다음 상태로 갈 수 있는지까지 보장하지 않습니다. 두 요청이 동시에 PENDING을 읽고 각각 결제 승인이나 작업 실행을 시작하면, 마지막 저장이 이기는 방식으로는 중복 효과와 잘못된 역전이가 생길 수 있습니다. 상태 모델에는 허용 전이, 전이의 주체, 버전, 작업 ID를 함께 둬야 합니다.
예를 들어 UPDATE orders SET state='CANCELLED', version=version+1 WHERE id=? AND state='PENDING' AND version=?처럼 읽었을 때의 상태와 버전을 조건으로 갱신하면 한 요청만 전이에 성공합니다. 영향받은 행이 0이면 최신 상태를 다시 읽어 이미 완료됐는지, 다른 요청과 충돌했는지, 재시도 가능한지 판단합니다. 결제 승인과 취소가 동시에 들어올 때 단순히 마지막 시간을 기준으로 덮어쓰지 말고 승인·취소 요청의 우선순위와 종료 상태 규칙을 정하겠습니다.
외부 호출은 미실행, 처리 중, 성공, 실패, 결과 불확실을 구분해야 할 수 있습니다. 결과 불확실은 재시도해도 되는 상태와 확인·대사가 필요한 상태가 다릅니다. 작업 의도와 외부 요청 ID를 저장하고, 응답 유실 뒤 같은 ID로 결과를 조회하거나 보상 흐름으로 넘기겠습니다. 운영자 도구도 직접 enum을 바꾸지 않고 동일한 전이 조건과 감사 기록을 사용해야 합니다.
검증은 정상 전이뿐 아니라 금지 전이, 동시 승인·취소, 워커 중단, 오래 멈춘 상태, 수동 복구를 재현합니다. 상태 수를 줄이는 것보다 서로 다른 복구 행동을 안전하게 구분하는 것이 우선입니다.
상태 이름보다 전이 조건이 중요합니다
상태와 사건, 사건을 받아 다음 상태로 이동하는 규칙을 묶은 모델을 유한 상태 머신(finite-state machine)이라고 부릅니다. 실제 주문 처리에는 상태뿐 아니라 금액·소유자·버전 같은 데이터가 함께 있으므로, 어떤 조건에서 전이를 허용하는지를 명시해야 합니다. 그 조건을 전이 가드(transition guard)라고 합니다. 예를 들어 미결제 주문만 취소할 수 있고 이미 배송된 주문은 반품 절차로 가야 한다면, CANCELLED라는 값이 존재한다는 사실만으로 임의의 상태에서 그 값으로 갈 수는 없습니다.
동시 요청에서 가드를 안전하게 적용하려면 읽기와 변경 사이에 다른 요청이 들어오는 틈을 막아야 합니다. 현재 상태와 버전을 UPDATE 조건에 함께 넣는 방식은 낙관적 동시성 제어입니다. 한 요청이 성공해 버전을 올렸다면 다른 요청은 변경한 행이 0개라는 결과로 충돌을 알게 됩니다. 이때 같은 SQL을 무한히 반복하지 않고 최신 상태가 이미 목표 상태인지, 다른 요청이 진행 중인지, 더 이상 허용할 수 없는 전이인지 다시 판단하겠습니다. 버전은 상태의 신선도를 검증하고 작업 ID는 같은 요청의 반복을 식별하므로 서로 대체할 수 없습니다.
외부 호출에는 결과 불확실 상태가 필요합니다
결제 요청을 보내기 전에 호출 의도와 안정적인 요청 ID를 저장하고, 응답을 받은 뒤 결과를 기록한다고 하겠습니다. 응답 전에 워커가 죽으면 로컬에는 처리 중만 남지만 결제는 성공했을 수 있습니다. 이를 무조건 실패로 옮겨 새 ID로 재결제하면 중복이 생깁니다. 결과를 알 수 없다는 불확정 결과(ambiguous outcome)를 상태에 드러내고, 같은 외부 요청 ID로 조회하거나 멱등 재시도하는 복구 경로를 두겠습니다. 어떤 행동을 해야 하는지가 다르면 상태도 구분할 이유가 있습니다.
처리 중인 워커를 기다리기만 하면 영원히 멈출 수 있으므로, 소유권에 기한을 두는 임대(lease)를 사용할 수 있습니다. 새 워커가 이어받을 때 세대 번호를 올리고 이전 세대의 완료는 조건부 저장으로 거절합니다. 하지만 이는 로컬 기록을 보호하는 장치일 뿐, 외부에 이미 나간 결제를 취소하지는 않습니다. 오래된 워커를 차단하는 펜싱과 외부 멱등성·결과 조회를 함께 설계해야 합니다. 진행 중 상태를 하나 더 만들었다는 것만으로 복구가 자동으로 되지는 않습니다.
완료 상태에서도 정정은 새 사실로 남깁니다
종료 상태를 다시 열어야 한다면 임의로 DONE을 PENDING으로 바꾸기보다 정정 요청 ID와 근거를 가진 별도 전이를 만들겠습니다. 그래야 이전 성공 응답과 새 변경을 구분하고 같은 정정을 반복 적용하지 않을 수 있습니다. 기록을 덮어쓰는 대신 언제 어떤 사건이 어떤 전이를 일으켰는지 남기는 것을 감사 추적(audit trail)이라고 합니다. 모든 시스템을 이벤트 소싱으로 만들 필요는 없지만, 금전·권한 변경은 변경 전후 상태와 수행 주체를 복원할 수 있어야 합니다.
테스트에서는 상태별로 가능한 사건을 표로 정리하고 금지된 조합을 먼저 확인하겠습니다. 이어서 같은 버전의 동시 요청, 외부 성공 직후 중단, 임대 인계 뒤 늦은 완료를 재현합니다. 오류가 나지 않았다는 것보다 중복 결제가 없고, 확인이 필요한 주문이 성공이나 실패로 숨겨지지 않는지가 중요합니다. 운영자 복구도 같은 가드와 감사 기록을 지나게 해야 자동 경로에서 지킨 규칙이 수동 수정으로 무너지지 않습니다.
득점 포인트
이해가 드러나는 지점을 확인해 보세요.
- 상태 값과 허용 전이·버전·작업 ID를 구분한다.
- 조건부 갱신으로 동시 전이를 하나만 성공시키는 방법을 제시한다.
- 외부 결과 불확실 상태와 운영 복구를 모델에 포함한다.
감점 포인트
놓치기 쉬운 조건과 오해를 짚어 보세요.
- enum을 만들면 상태 전이가 자동으로 안전하다고 말한다.
- 마지막 저장이 이기면 상태 변경 순서도 안전하다고 말한다.
- 운영자가 불변식을 우회해 상태 값을 직접 바꿔도 된다고 말한다.
더 파고들 거리
하나 더 질문하며 이해의 범위를 넓혀 보세요.
- 상태 전이와 이벤트 발행을 어떤 원자적 경계로 연결하나요?
- 완료·취소 상태를 다시 바꿔야 하는 정정이 있습니다. 종료 상태를 몰래 덮지 않고 어떤 별도 전이를 두나요?
- 상태 버전과 멱등 작업 ID의 역할은 어떻게 다른가요?
득점·감점 포인트는 학습용 자기 점검 기준이며, 특정 회사의 실제 채점표가 아닙니다.
원리부터 공부하고 싶다면
답변을 보충하는 개념·예제·슈도코드를 읽어 보세요.