본문 바로가기

직접 만든 소프트웨어, 두 가지 개발 기록

구현 구조와 적용 범위, 확인한 기능과 남은 한계를 함께 기록합니다.

소프트웨어개발

Cloudflare Durable Objects 자동 재시도: 기존 Worker 재시도와 겹치면?

by 아빠띠띠뽀 2026. 11. 9.

Cloudflare Durable Objects의 자동 재시도를 검토 중이라면, 먼저 지금 운영 중인 Worker 쪽 재시도를 찾아보세요. 호출을 다시 보낼 수 있는 연결 오류에서 런타임이 이미 재시도하기 때문에, 호출 코드까지 같은 종류의 오류를 다시 보내면 시도 횟수가 겹칠 수 있습니다. 그리고 이미 한 번 실행됐을 가능성이 있는 쓰기는 더 조심해야 합니다. 재시도가 있다는 소식과 같은 작업을 다시 실행해도 안전하다는 판단은 서로 다릅니다.

Cloudflare는 2026년 11월 5일 변경 기록에서 이 동작을 안내했습니다. 이 글은 Worker가 Durable Object의 스텁, 즉 원격 객체에 요청을 보내는 손잡이를 만들어 fetch()나 RPC로 호출하는 팀을 위한 설명입니다. 11월 8일 공식 자료를 확인했으며, 아래 장바구니와 시도 횟수는 동작을 이해하기 위한 예입니다. 실제 배포, 장애 유도, 청구나 성능 측정은 하지 않았습니다.

같은 실패처럼 보여도 세 구간이 다릅니다

장바구니 페이지에서 수량 변경을 눌렀는데 응답을 못 받았다고 해 볼게요. 사용자는 한 가지 오류를 보지만 Worker와 객체 사이에서는 여러 상황이 가능합니다. 요청이 아직 객체에 도착하지 않았을 수 있고, 도착은 했지만 응답만 사라졌을 수도 있습니다. 아예 객체 코드가 오류를 던졌거나 서버가 500 응답을 보내 준 경우도 있습니다. 화면에서 실패처럼 보인다는 이유로 이들을 같은 방식으로 다시 보내면 원인을 확인하기 어려워집니다.

공식 문서에 따르면 런타임은 객체가 받기 전이거나 받았는지 확인할 수 없는 오류 중 .retryable이 true인 경우를 다룹니다. 기본적으로 같은 요청의 재시도를 객체가 이미 처음 요청을 받은 뒤라면 거절합니다. 원래 오류가 호출자에게 돌아오는 것은, 곧바로 ‘처음 쓰기가 전혀 일어나지 않았다’는 증거가 아닙니다. 요청을 받았는지 불명확한 상황에서 실패 결과만으로 기록 상태를 단정하지 마세요.

이 차이가 생기는 이유는 요청이 전달된 시점과 결과를 돌려받은 시점이 같지 않아서입니다. 요청을 보내는 쪽의 타임아웃은 ‘응답을 기다리는 시간이 끝났다’고 알려줄 뿐, 받는 쪽의 기록이 없다고 알려 주지 않습니다. 작업 식별자나 서비스 내부의 결과 조회가 이미 있다면, 값을 다시 증가시키기 전에 그 경로로 상태를 확인할 수 있는지 검토해 보세요. 이는 애플리케이션 설계 제안이며 자동으로 제공되는 기능이라고 가정한 것은 아닙니다.

관찰한 상황 재시도를 판단할 단서
미도달 또는 수신 불명확 같은 호출을 다시 보낼 수 있고 오류가 적격인지 봅니다.
코드가 던진 오류 런타임 재시도 범위에 들지 않습니다.
HTTP 500 응답 오류 응답을 받은 상황입니다. 원인을 따로 확인하세요.
과부하 신호 .overloaded가 참이면 자동으로 다시 보내지 않습니다.

처음부터 재전송할 수 없는 호출도 있습니다. 요청 본문이 ReadableStream인 fetch(), 스트림이나 함수 등을 인수로 넘기는 일부 RPC는 그대로 보낼 수 없다고 문서에 나옵니다. 반대로 문자열처럼 다시 전송할 수 있는 본문은 이 제한의 예에 해당하지 않습니다. 그렇다고 문자열만 쓰면 항상 재시도한다는 뜻도 아닙니다. 오류의 종류와 스텁의 출처도 조건이기 때문입니다.

문서에 나오는 또 다른 예외는 RPC의 결과가 아직 준비되기 전에 그 위에서 다음 호출을 잇는 프라미스 파이프라이닝입니다. namespace의 get() 또는 getByName()으로 만든 스텁과 RPC 인수나 반환값으로 받은 스텁의 범위도 다릅니다. 호출 경로를 점검할 때 ‘모든 객체 호출에 동일하게 적용’이라고 적기보다, 실제로 어느 코드에서 스텁을 만들고 어떤 데이터를 보내는지부터 표시해 보세요.

같은 수량을 정하는 일과 하나 더하는 일

수량을 3으로 설정하는 동작과 수량을 하나 늘리는 동작을 놓고 생각해 봅시다. 사이에 다른 변경이 없고 수량 값만 본다면, 첫 번째 동작은 두 번 실행돼도 마지막 값이 3입니다. 두 번째 동작은 시작이 2일 때 첫 실행 뒤 3, 다음 실행 뒤 4가 됩니다. 이처럼 같은 요청을 여러 번 실행해도 의도한 효과가 달라지지 않는 성질을 멱등성이라고 부릅니다.

Cloudflare 공식 예제는 이런 차이를 retryable()로 설명합니다. 클래스의 프로토타입 메서드를 표시하면, 런타임이 첫 요청을 받았는지 알 수 없는 상황에서도 표시한 메서드를 다시 실행할 수 있습니다. 표시를 했다고 자동 재시도할 오류 종류가 늘어나거나 횟수 제한이 풀리지는 않습니다. 모든 메서드를 표시하는 공통 스위치로 쓰기보다, 호출마다 부작용을 확인할 계기로 삼는 편이 좋습니다.

예를 들어 수량이 결국 3으로 끝나도, 실행할 때마다 외부 시스템에 ‘변경’ 이벤트를 보내면 이벤트는 두 건이 될 수 있습니다. 다른 사용자의 수정이 중간에 끼어들면 먼저 적용한 값을 나중 재실행이 덮어쓸 가능성도 생각해야 합니다. 화면에 보이는 최종 값, 저장되는 변경 기록, 알림이나 다른 서비스가 받는 신호를 따로 적어 보면 단순한 예와 우리 서비스의 차이가 드러납니다.

안전성을 확인하기 어렵다면 표시를 추가하기 전에 오류 응답 이후의 확인 절차부터 정해도 됩니다. 예를 들어 호출자가 이미 가진 작업 번호로 결과를 조회하거나, 요청이 실제로 중복됐는지 담당자가 판별할 수 있는 기록을 남기는 식입니다. 이런 설계가 곧바로 모든 외부 처리의 정확히 한 번 실행을 보장하지는 않아요. 중복을 막는 책임이 어느 서비스에 있는지와 기록이 남지 않았을 때의 처리까지 함께 정해야 합니다.

시도 횟수와 기다리는 시간을 함께 적으세요

기본 설정은 첫 시도에 최대 네 번의 재시도를 더해 총 다섯 번까지입니다. 최초 호출 시작 후 10초가 넘으면 새 재시도를 시작하지 않습니다. 문서는 첫 시도 자체는 이 한도로 취소하지 않고, 진행 중인 재시도는 그 시간이 끝날 때 취소한다고 설명합니다. 따라서 기본값 10초를 ‘어떤 요청도 10초 안에 반환’하는 전체 제한으로 사용하면 안 됩니다. 재시도 간격도 지수 백오프와 무작위 지연을 적용하므로 일률적인 간격으로 읽지 마세요.

가상의 Worker가 최초 호출을 포함해 세 번까지 같은 동작을 호출한다고 해 볼까요? 매번 재시도 요건과 시간 여유가 있고 내부 재시도 네 번을 모두 쓴다는 조건에서, 전달 시도 상한의 예는 3 × (1 + 4) = 15입니다. 이 숫자는 메서드가 열다섯 번 실행되거나 사용자 응답이 일정 시간 지연됐다는 뜻이 아닙니다. Worker가 만든 세 호출은 별개의 호출이고, 각 호출 안의 런타임 재시도는 그 호출에 붙습니다.

문서상 런타임 재시도는 하위 요청 한도에서 다시 세지 않고 첫 시도만 셉니다. 이를 보고 청구나 비용도 반드시 같다고 결론 내리지 마세요. 한도 집계 설명과 서비스 사용량, 업무 중복 처리의 비용은 다른 문제입니다. 이 글의 산술은 시도 구조만 보여 줍니다. 실제 지연을 보려면 같은 호출 유형과 오류 조건에서 시작과 끝을 관찰하고, 청구에 관해서는 해당 공식 요금 기준과 사용 기록을 별도로 확인해야 합니다.

내 코드의 상태 먼저 확인할 것
바깥에서 재시도하지 않음 호출 종류와 런타임 기본값의 적용 여부
Worker에 재시도 반복문이 있음 두 단계의 횟수와 응답 대기 시간
사용자가 화면에서 다시 제출함 새 요청이 같은 업무 효과를 낼 수 있는지
여러 종류의 호출이 한 스텁을 공유함 읽기와 쓰기에 같은 제한이 맞는지

설정을 바꾸기 전에 호출 하나부터 그려 보세요

스텁을 생성할 때 넘기는 retry 옵션은 해당 스텁을 거치는 fetch()와 RPC의 제한에 영향을 줍니다. 공식 문서의 maxRetries는 첫 시도 뒤 0부터 10까지의 정수로, 0은 그 스텁의 런타임 재시도를 끕니다. timeoutMs는 500부터 60,000까지의 정수입니다. retryable()과 이 옵션을 사용하려면 공지에 따르면 Wrangler 4.158.0 이상이 필요합니다. 버전 확인은 적용 준비이며 운영 배포를 했다는 뜻이 아닙니다.

먼저 요청이 오가는 길을 종이나 문서에 한 줄로 써 보세요. ‘사용자 → Worker → 객체 → 외부 서비스’처럼 적은 다음, 다시 시도하는 코드를 그 옆에 하나씩 표시하면 됩니다. 오류 처리 함수에서 포착하는 실패와 사용자가 다시 버튼을 누르는 행동은 분리해서 적습니다. 같은 기능 안에서도 조회와 쓰기, 임시 장바구니와 최종 확정처럼 효과가 다른 요청을 묶어 판단하지 않는 편이 좋습니다.

그다음 시험할 가상의 요청과 기대 결과를 정합니다. 예를 들어 수량 설정 요청에서는 마지막 값뿐 아니라 변경 이벤트가 몇 번 기록되는지, 수량 증가 요청에서는 시작 값과 끝 값이 맞는지를 관찰합니다. 중간 응답이 사라진 상황을 승인된 시험 환경에서 다뤄 볼 계획이라면, 테스트의 첫 시도와 실제 처리 횟수를 따로 기록할 수 있게 하세요. 결과 조회가 없거나 부작용을 되돌릴 수 없다면 표시나 제한 변경을 미룰 이유가 충분합니다.

배포를 검토하는 팀이라면 현재 옵션과 코드 버전, 적용 범위, 되돌릴 방법도 적어 두세요. 옵션을 되돌려도 이미 두 번 나간 알림이나 외부 작업이 취소되는 것은 아닙니다. 원복은 앞으로의 호출 동작을 회복하는 절차이고, 발생한 업무 결과를 대조하는 일은 별도입니다. 재시도를 끄거나 늘리는 선택은 서비스 특성에 따라 달라지므로 한 가지 숫자를 모든 객체의 기본값으로 권하지 않습니다.

자주 나오는 질문

응답 코드가 500이면 자동으로 네 번 다시 보내나요? 문서의 설명은 그렇지 않습니다. 500은 이미 받은 HTTP 응답입니다. 코드가 어느 시점에 실패를 처리하는지, 그 전에 상태가 바뀌었는지 확인해야 합니다. 런타임 연결 재시도의 적격 오류와 애플리케이션이 받은 응답을 같은 것으로 보지 마세요.

이미 재시도 코드를 쓰는데 바로 지울까요? 무슨 실패를 처리하려고 추가한 코드인지부터 확인하세요. 호출자 수준에서 처리하는 오류와 런타임이 다시 보내는 범위가 전부 같지는 않습니다. 다른 제품과 비교할 때도 조건은 따로입니다. AWS Lambda 비동기 재시도의 함수 오류와 제한 구분은 비동기 함수 호출에 관한 글이어서, 그 규칙을 객체 스텁에 그대로 적용하지 않습니다.

수량이 같으면 재실행해도 괜찮지 않나요? 같은 숫자는 확인의 시작점입니다. 저장 이력, 경쟁하는 변경, 이벤트 발송까지 살펴야 해당 업무에서 같은 효과인지 답할 수 있습니다. 시험 결과를 남길 때는 최종 값과 실행 기록을 나누어 적어 두세요.

확인한 공식 자료

2026년 11월 8일 Durable Objects 11월 5일 변경 기록과 자동 재시도 공식 문서를 확인했습니다. 변경 기록은 기능 도입과 Wrangler 요건을, 공식 문서는 재전송 대상과 예외, 스텁 제한을 설명합니다. 팀의 실제 재시도 횟수나 배포 결과는 별도로 확인할 사항입니다.

Durable Objects 재시도에서 사용자 요청, Worker 호출, 런타임 재시도, 저장과 알림을 나눈 개념도
Worker 호출과 런타임 재시도, 실제 업무 효과를 나누어 보는 자체 개념도입니다.