이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • HTTP method의 safe, idempotent, cacheable 의미가 retry와 장애 복구에 어떻게 연결되는가?
  • GET/POST/PUT/PATCH/DELETE를 API 설계에서 언제 어떻게 골라야 하는가?
  • 결제/주문/외부 API 호출처럼 중복 실행이 치명적인 요청을 어떻게 안전하게 재시도하게 만드는가?

개요

HTTP method는 서버에 기대하는 동작의 의미를 표현한다. 이 의미가 맞아야 cache, prefetch, retry, proxy, client SDK가 요청을 안전하게 다룬다. 멱등성은 특히 장애 복구와 중복 처리 방지의 기준이다.

  • Method는 서버에 기대하는 동작의 의미를 표현한다.
  • 멱등성은 retry와 중복 처리 방지의 기준이다.
  • 결제/주문은 POST라도 idempotency key로 중복 실행을 막아야 한다.

왜 백엔드 개발자가 알아야 하는가

method를 잘못 쓰면 장애 대응이 위험해진다. timeout이 났을 때 retry해도 되는지, cache가 응답을 저장해도 되는지, crawler가 링크를 따라가도 되는지, proxy가 재시도해도 되는지가 method 의미에 의존한다.

  • GET에 상태 변경을 넣으면 crawler, prefetch, cache가 장애를 만든다.
  • POST 결제를 timeout 후 무작정 retry하면 중복 결제가 난다.
  • PUT을 부분 수정처럼 쓰면 client가 전체 교체 의미를 오해한다.
  • DELETE 재시도에서 이미 삭제된 리소스를 500으로 처리하면 멱등성이 깨진다.
  • PATCH는 구현 방식에 따라 멱등일 수도, 아닐 수도 있으므로 명확한 계약이 필요하다.

요청 흐름에서의 위치

method는 HTTP request line의 앞부분에 있으며, proxy와 app이 가장 먼저 보는 의도다.

GET /orders/1 HTTP/1.1       # 조회
POST /orders HTTP/1.1        # 생성 또는 명령
PUT /orders/1 HTTP/1.1       # 전체 교체
PATCH /orders/1 HTTP/1.1     # 부분 변경
DELETE /orders/1 HTTP/1.1    # 삭제

백엔드 장애 분석에서는 “어떤 endpoint가 실패했는가”와 함께 “어떤 method인가”를 반드시 본다. retry 가능성이 달라지기 때문이다.

원리

method의 대표 성질은 다음과 같다.

MethodSafeIdempotent주의점
GET상태 변경 금지, cache 가능성 고려
HEADbody 없이 metadata 확인
POST아니오기본적으로 아니오생성/명령, idempotency key 검토
PUT아니오전체 교체 의미를 지키기
PATCH아니오경우에 따라 다름patch 문서 의미를 명확히
DELETE아니오이미 삭제된 상태를 어떻게 응답할지 정하기

멱등성은 “응답이 항상 같다”가 아니라 “같은 요청을 여러 번 보내도 서버의 최종 상태가 추가로 바뀌지 않는다”는 뜻이다.

핵심 판단 기준

  • 조회는 GET으로 두고 상태 변경은 GET에 숨기지 않는다.
  • retry가 필요한 POST는 idempotency key, unique constraint, processing state를 설계한다.
  • DELETE는 이미 없는 리소스를 어떻게 응답할지 정한다. 204/404 중 무엇을 택하든 client 계약이 중요하다.
  • PATCH는 JSON Patch, merge patch, command-style patch 중 무엇인지 문서화한다.
  • gateway/client retry policy는 method와 idempotency 여부를 기준으로 제한한다.

실무에서 자주 만나는 문제

  • POST 재시도로 중복 결제가 난다.
  • GET에 상태 변경을 숨겨 cache와 crawler가 문제를 만든다.
  • DELETE를 비멱등하게 구현해 재시도 때 500을 낸다.
  • PUT을 부분 수정처럼 구현해 누락 필드가 날아간다.
  • PATCH가 add operation을 반복해 배열에 중복 값이 들어간다.
  • timeout 후 client가 POST를 다시 보냈는데 서버는 첫 요청을 이미 처리했다.
  • proxy가 idempotent method만 retry하도록 설정되어 있는데 앱은 POST도 안전하다고 착각한다.
  • 202 Accepted로 비동기 처리를 시작했지만 작업 상태 조회 endpoint가 없다.

장애 중에는 “실패했으니 다시 누르세요”가 데이터 정합성 사고가 될 수 있다. 특히 결제, 주문, 포인트, 쿠폰, 재고, 알림 발송은 중복 효과를 별도로 막아야 한다.

디버깅 방법

  • 실패 요청의 method, idempotency key, request id, business key를 함께 본다.
  • 같은 key로 중복 요청이 왔는지 application log와 DB unique constraint를 확인한다.
  • retry가 client, SDK, gateway, app 중 어느 계층에서 발생했는지 본다.
  • timeout 시점과 실제 처리 완료 시점을 분리한다.
  • 409, 412, 425, 429, 503 응답이 retry 정책과 맞는지 확인한다.
curl -v -X POST https://api.example.com/orders \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: order-20260630-001' \
  -d '{"productId": 10, "quantity": 1}'
 
curl -v -X POST https://api.example.com/orders \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: order-20260630-001' \
  -d '{"productId": 10, "quantity": 1}'

두 번째 요청이 새 주문을 만들면 안 된다. 이전 성공 응답을 재사용하거나, processing 상태를 반환하거나, payload fingerprint 불일치를 거절해야 한다.

코드로 확인하기

Spring에서 method 의미를 API 표면에 드러낸다.

@GetMapping("/orders/{id}")
OrderResponse getOrder(@PathVariable Long id) {
    return orderService.get(id);
}
 
@PostMapping("/orders")
ResponseEntity<OrderResponse> createOrder(
    @RequestHeader("Idempotency-Key") String key,
    @RequestBody CreateOrderRequest request
) {
    OrderResponse created = orderService.createOnce(key, request);
    return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
 
@PutMapping("/orders/{id}")
OrderResponse replaceOrder(@PathVariable Long id, @RequestBody ReplaceOrderRequest request) {
    return orderService.replace(id, request);
}

createOnce 내부는 DB unique constraint와 처리 상태를 함께 써야 한다. 메모리 map만으로는 재시작, 다중 인스턴스, race condition을 견디기 어렵다.

주니어가 자주 하는 오해

  • POST는 절대 retry하면 안 된다고 생각한다.
    • idempotency key와 저장소 계약이 있으면 안전한 retry를 설계할 수 있다.
  • DELETE는 항상 200이어야 한다고 생각한다.
    • 204, 404 등 선택보다 멱등성과 client 계약이 중요하다.
  • GET은 body만 없으면 괜찮다고 생각한다.
    • GET에 상태 변경을 넣으면 cache, crawler, prefetch가 사고를 만든다.
  • PUT과 PATCH를 비슷한 수정 요청으로 본다.
    • PUT은 보통 전체 교체, PATCH는 부분 변경 의미를 가진다.

시니어의 설계 판단 기준

  • retry 가능한 endpoint와 금지 endpoint를 API catalog에 표시한다.
  • idempotency key는 business key, fingerprint, TTL, status transition과 함께 설계한다.
  • command-style POST는 처리 상태 조회 endpoint와 202/303/200 응답 전략을 정한다.
  • 외부 결제/배송 API를 호출하는 endpoint는 내부 상태 전이와 외부 call retry를 분리한다.
  • gateway retry 정책은 method만이 아니라 endpoint별 idempotency metadata와 맞춘다.

인프라 협업 포인트

  • gateway retry는 GET/HEAD/PUT/DELETE처럼 idempotent method 중심으로 제한한다.
  • POST retry를 허용하려면 idempotency key 지원 여부와 safe endpoint 목록을 인프라 팀에 제공한다.
  • WAF나 bot protection이 GET prefetch/crawler를 어떻게 다루는지도 상태 변경 GET 방지와 연결된다.
  • 인프라 담당자는 retry를 켜면 장애 완화가 아니라 중복 처리와 부하 증폭이 될 수 있다는 점을 걱정한다.

실전 팁

  • 주문/결제/포인트 API에는 idempotency key를 처음부터 넣는다.
  • timeout 후 client가 보는 실패와 서버의 실제 처리 결과가 다를 수 있음을 UX에 반영한다.
  • 멱등성 테스트는 같은 요청을 두 번 보내는 자동 테스트로 만든다.
  • retry policy 문서에는 method, status code, max attempts, backoff, idempotency 조건을 함께 둔다.

위험 신호!

  • GET이 DB update나 이벤트 발송을 한다.
  • POST 결제/주문에 idempotency key가 없다.
  • gateway retry 정책이 endpoint별 멱등성을 모른다.
  • timeout 후 “다시 시도” UX만 있고 중복 처리 방어가 없다.
  • PUT/PATCH 의미가 API 문서에 없다.

확인 질문

확인 질문

  • 멱등성이 retry와 연결되는 이유는 무엇인가?
    • 같은 요청을 여러 번 보내도 최종 상태가 중복 변경되지 않아야 장애 중 재시도가 안전하기 때문이다.
  • POST도 안전하게 retry할 수 있는 조건은 무엇인가?
    • idempotency key, payload fingerprint, 저장소 unique constraint, 처리 상태 재사용 계약이 있어야 한다.
  • GET에 상태 변경을 넣으면 왜 위험한가?
    • cache, crawler, prefetch, link preview가 의도치 않게 요청을 실행할 수 있기 때문이다.

참고 문서