ONE QUESTION AT A TIME.

천천히 생각하고, 분명하게.

TODAY’S QUESTION1244 / 1500
분산 시스템난이도 하5분 답변 목표 · 보강본

주문 생성 요청이 타임아웃됐을 때 서버에 이미 저장됐는지 알 수 없습니다. 그대로 재시도해도 되며 중복 주문은 어떻게 막나요?

잠깐, 답변을 보기 전에
먼저 내 말로 설명해 볼까요?

답변과 점검 포인트 펼치기답변 접기
01

구두 답변

정의를 외우기보다, 원리와 조건을 연결해 설명해 보세요.

그대로 재시도해도 되는지는 작업의 성격에 달려 있습니다. 타임아웃은 제가 결과를 못 받았다는 뜻이지 서버가 실행하지 않았다는 뜻은 아닙니다. 주문은 저장됐는데 응답만 유실됐을 수도 있습니다. 그래서 저는 실패했는지 모르는 상태에서 다시 보내도 같은 변경이 두 번 반영되지 않는지부터 확인하겠습니다.

이때 필요한 성질이 멱등성입니다. 같은 논리적 요청을 반복해도 상태 변화는 한 번 실행한 것과 같게 만드는 것입니다. 잔액 차감처럼 반복하면 결과가 달라지는 API에는 클라이언트가 요청 키를 만들고, 재시도에도 같은 키를 보내게 하겠습니다. 서버는 이미 완료한 키라면 기존 결과를 돌려주고, 같은 키로 다른 내용이 오면 잘못된 재사용으로 거절해야 합니다.

중요한 것은 키를 조회한 다음 작업을 수행하는 틈입니다. 두 요청이 동시에 키가 없다고 읽으면 둘 다 실행할 수 있습니다. 같은 DB 안에서 끝나는 작업이라면 키의 고유 제약으로 중복을 막고, 키 기록과 실제 데이터 변경, 결과 저장을 하나의 트랜잭션으로 묶겠습니다. 그러면 커밋 전 실패는 함께 취소되고, 커밋 뒤 응답을 잃어도 저장된 결과로 재시도에 답할 수 있습니다.

외부 결제까지 포함하면 이 방법만으로는 부족합니다. 외부 결제가 성공했는데 로컬 결과 저장 전에 죽을 수 있기 때문입니다. 저는 상대가 멱등성 키를 지원하면 같은 작업 키를 넘기고, 결과가 불확실하면 상태를 조회해 확인하겠습니다. 지원하지 않는다면 무조건 다시 결제하기보다 확인이 필요한 상태로 남겨 대사하는 절차가 필요합니다. 여기서는 빠른 자동 복구보다 중복 결제를 막는 쪽을 우선하겠습니다.

재시도 자체가 장애를 키우지 않게 전체 시간과 횟수도 제한하겠습니다. 잠깐의 과부하라면 간격을 늘리고 약간씩 무작위로 분산해 다시 보내지만, 여러 계층에서 각각 재시도하지는 않도록 책임을 정해야 합니다. 테스트는 특히 커밋 직후 응답이 끊긴 경우와 같은 키가 동시에 들어온 경우를 재현하겠습니다. 정상 요청이 한 번 성공하는 것보다 불확실한 실패 뒤에도 효과가 한 번만 남는지를 확인하는 것이 핵심입니다.

같은 요청을 식별하는 범위를 정합니다

이 상황은 단순한 실패가 아니라 호출자가 성공 여부를 모르는 결과 불확실성(ambiguous outcome)입니다. 서버의 커밋과 클라이언트의 응답 수신은 별개의 사건이므로 타임아웃만으로 두 사건을 구분할 수 없습니다. 같은 요청을 여러 번 보내도 최종 상태 변화가 한 번 실행한 것과 같아야 한다는 성질을 멱등성(idempotency)이라고 부릅니다. 이는 요청이 물리적으로 한 번만 실행됐다는 뜻도, 어떤 순서로 실행해도 된다는 뜻도 아닙니다. 같은 값을 다시 설정하는 연산은 자연스럽게 멱등적일 수 있지만, 잔액을 일정 금액만큼 빼는 연산은 별도의 중복 방지 장치가 필요합니다.

멱등 키는 네트워크 시도 하나가 아니라 사용자의 한 번의 주문 의도를 식별해야 합니다. 타임아웃 뒤 재시도에는 같은 키를 쓰고, 같은 상품을 다음 날 다시 사는 새 주문에는 새 키를 써야 합니다. 키만 전역으로 비교하면 다른 사용자의 키와 충돌하거나 저장된 결과를 잘못 반환할 수 있으므로, 인증된 사용자·테넌트와 작업 종류를 포함한 범위를 정하겠습니다. 같은 키인데 금액이나 상품이 달라졌다면 기존 결과를 조용히 돌려주지 말고 잘못된 재사용으로 거절합니다. 이때 비교할 요청의 의미를 정규화한 뒤 해시로 보관하면 본문 전체를 반복 저장하지 않고도 불일치를 확인할 수 있습니다.

확인과 실행 사이의 경쟁을 없앱니다

‘키가 없다면 실행한다’는 조회와 실제 삽입이 떨어져 있으면 두 요청이 동시에 통과할 수 있습니다. 조건을 확인한 시점과 사용하는 시점 사이에 상태가 바뀌는 문제를 TOCTOU(time-of-check to time-of-use)라고 합니다. 같은 DB에서 끝나는 짧은 작업은 고유 제약으로 키 기록을 선점하고, 주문 변경과 결과 저장을 같은 트랜잭션에서 커밋하는 것이 핵심입니다. 고유 제약 충돌이 나면 DB에 따라 현재 트랜잭션을 롤백해야 할 수 있으므로, 오류를 무시한 채 계속 쿼리하지 않고 유효한 새 읽기 경로에서 기존 결과를 확인해야 합니다.

긴 외부 호출 때문에 먼저 처리 중 상태를 커밋하는 설계를 택하면 문제가 하나 더 생깁니다. 그 상태를 남긴 워커가 죽었을 때 누가 이어서 처리할지 정해야 합니다. 작업 소유권과 만료·세대 번호를 두고 이전 워커의 늦은 결과를 거절하되, 소유권을 새로 얻었다는 이유만으로 외부 결제도 미실행이었다고 판단해서는 안 됩니다. 외부의 멱등 키나 결과 조회가 여전히 필요합니다. 중복 방지 테이블은 단순한 완료 ID 목록이 아니라 필요한 경우 복구 상태를 가진 작은 상태 머신이 됩니다.

기록의 수명도 보장의 일부입니다. 키를 하루 뒤 삭제했는데 클라이언트나 메시지 재생이 이틀 뒤 같은 요청을 가져오면 새 요청처럼 실행될 수 있습니다. 가능한 재시도 기간을 계약으로 제한하거나, 주문 고유성처럼 더 오래 남는 제약으로 추가 보호해야 합니다. 재시도 간격을 점점 늘리는 지수 백오프(exponential backoff)와 시도 시점을 흩어 놓는 지터(jitter)는 부하를 줄일 뿐 중복 반영을 막지는 않습니다. 반대로 멱등성만 있어도 무제한 재시도가 무료인 것은 아닙니다. 두 책임을 따로 설계해야 합니다.

02

득점 포인트

이해가 드러나는 지점을 확인해 보세요.

  • 타임아웃의 불확실성을 사례로 설명한다. 서버 커밋과 응답 전달을 분리해서 생각한다.
  • 중복 방지의 원자적 범위를 정한다. 고유 제약과 키 기록·데이터 변경·결과 저장의 동일 트랜잭션으로 경쟁과 크래시 구간을 다룬다.
  • 외부 효과에서 보장의 한계를 인정한다. 외부 멱등성·상태 확인·대사 중 가능한 수단으로 중복 위험을 통제한다.
  • 재시도 제한과 장애 주입을 구체화한다. 횟수·시간·책임 계층을 정하고 응답 유실과 동시 중복을 테스트한다.
03

감점 포인트

놓치기 쉬운 조건과 오해를 짚어 보세요.

  • “타임아웃이면 서버도 실패했다”고 말한다. 작업은 커밋됐고 응답만 유실됐을 수 있다.
  • “키 존재 여부를 먼저 조회하면 충분하다”고 말한다. 조회와 실행 사이에 다른 요청이 통과할 수 있다.
  • “재시도마다 새 키를 사용한다”고 말한다. 같은 논리 작업의 반복임을 식별할 수 없어진다.
  • “외부 호출도 로컬 트랜잭션이 롤백해 준다”고 말한다. 외부 성공은 별도의 상태 확인과 복구가 필요하다.
04

더 파고들 거리

하나 더 질문하며 이해의 범위를 넓혀 보세요.

  • 처리 중 상태를 먼저 저장한 워커가 죽으면 요청 소유권과 재처리를 어떻게 안전하게 넘길까요?
  • outbox는 어떤 유실을 막으며, 메시지를 받는 쪽의 중복 방지가 여전히 필요한 이유는 무엇일까요?
  • 멱등성 기록을 삭제한 뒤 늦게 온 재시도에 대해서는 어떤 유효 기간 계약을 둘까요?
  • 여러 계층의 재시도가 겹치지 않게 데드라인과 재시도 예산을 어떻게 전파할까요?

득점·감점 포인트는 학습용 자기 점검 기준이며, 특정 회사의 실제 채점표가 아닙니다.

원리부터 공부하고 싶다면

답변을 보충하는 개념·예제·슈도코드를 읽어 보세요.

← 전체 질문 보기