이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- 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의 대표 성질은 다음과 같다.
| Method | Safe | Idempotent | 주의점 |
|---|---|---|---|
| GET | 예 | 예 | 상태 변경 금지, cache 가능성 고려 |
| HEAD | 예 | 예 | body 없이 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가 의도치 않게 요청을 실행할 수 있기 때문이다.