"일상 언어의 동사를 HTTP 메서드에 그대로 대입하지 마. '생성'이 언제나 POST인 것도, '수정'이 언제나 PUT인 것도 아니야. 상태를 바꾸는지, 같은 요청을 반복해도 되는지, 어떤 리소스를 대상으로 삼는지를 먼저 봐야 해."
결정 트리
다음 트리는 메서드 선택을 시작하기 위한 실용적인 휴리스틱이야. 위에서 아래로 따라가되, 실제 리소스 모델과 PATCH 문서 형식의 의미론, 서버가 보장할 수 있는 멱등성까지 함께 확인해야 해.
1. 관찰 가능한 server state 바꿔?
NO → GET (혹은 header 만 필요하면 HEAD, method 발견은 OPTIONS)
YES → 2 계속
2. Client 가 정확한 target URI 알아?
NO → POST (server 가 URI 를 할당하고 201 + Location 을 돌려줌)
YES → 3 계속
3. Resource 의 FULL representation 보내?
YES → PUT (idempotent: 같은 payload → 같은 state)
NO → PATCH (부분 update 라 diff/delta 를 운반)
4. Resource 완전 제거?
→ DELETE
5. 명백한 target resource 없는 operation ('command' 스타일)?
→ action 이름 URI 에 POST (예: POST /charges/42/refund)
메서드와 결과 매트릭스
일반적인 리소스 API의 작업은 대체로 다음 패턴 가운데 하나로 설명할 수 있어.
| 의도 | 메서드 | URI 형태 | 성공 시 상태 코드 |
|---|---|---|---|
| 리소스 조회 | GET | /users/42 | 200 OK, 조건부 요청이면 304 |
| 리소스 목록 조회 | GET | /users?role=admin | 200 OK |
| 본문 없이 메타데이터 확인 | HEAD | /users/42 | 200 OK, 본문 없음 |
| 허용된 메서드 확인 | OPTIONS | /users/42 | 200 OK + Allow 헤더 |
| 서버가 할당한 ID로 생성 | POST | /users (컬렉션) | 201 Created + Location |
| 클라이언트가 정한 ID로 생성 | PUT | /users/client-picked-id | 201 Created |
| 기존 리소스 전체 교체 | PUT | /users/42 | 200 OK 또는 204 |
| 리소스 일부 수정 | PATCH | /users/42 | 200 OK 또는 204 |
| 리소스 제거 | DELETE | /users/42 | 200 OK 또는 204 No Content |
| 오래 걸리는 비동기 작업 생성 | POST | /jobs (컬렉션) | 202 Accepted + Location |
| 명령 또는 동작 요청 | POST | /charges/42/refund | 200 OK 또는 202 |
| 업로드 리소스 생성 | POST | /uploads | 201 Created |
세 가지 예로 풀어 보기
"사용자 42에게 환영 이메일을 보낸다." 실제 메일 발송이라는 부작용이 있고, 별도 중복 방지 장치가 없다면 같은 요청을 두 번 보낼 때 메일도 두 통 나가. 발송 요청을 새 하위 리소스로 모델링해 POST /users/42/welcome-emails를 사용하거나, 수신자를 본문에 담아 POST /emails로 보낼 수 있어. 안전하게 재시도해야 한다면 Idempotency-Key를 함께 설계해야 해.
"사용자 42의 이메일 주소를 수정한다." 대상 리소스가 분명하고 일부 필드만 바꾸므로 {"email": "new@example.com"}를 담은 PATCH가 자연스러워. API가 사용자 표현 전체를 교체하는 계약이라면 전체 페이로드를 담은 PUT을 사용할 수 있어.
"결제 42를 환불한다." 환불을 독립적인 하위 리소스로 모델링하면 POST /payments/42/refunds로 생성할 수 있어. 별도 장치가 없다면 중복 환불 위험이 있으므로 Idempotency-Key를 추가하는 편이 안전해. 환불 리소스를 만들었다면 201, 기존 작업을 즉시 수행해 결과만 돌려준다면 200을 사용할 수 있어.
모든 작업을 POST로 터널링하는 안티패턴
모든 엔드포인트를 POST로 만들면 처음에는 단순해 보여. 하지만 HTTP가 제공하는 의미 신호를 잃는 대가는 운영 단계에서 돌아와.
- POST 응답은 명시적인 캐시 지시가 있어야 캐시할 수 있고, 실제 중간 장치의 지원도 제한적이라 GET보다 재사용하기 어려워.
- POST는 멱등성을 보장하지 않으므로 전송 실패 뒤 자동 재시도하기 어렵고, 별도의 Idempotency-Key 같은 장치가 필요해.
- 브라우저와 HTTP 중간 장치가 안전한 조회에 제공하는 미리 가져오기와 조건부 GET 같은 최적화를 활용할 수 없어.
- 로그와 관리 화면에서 작업의 의미가 흐려져 모든 기록이 "POST로 무언가 실행"한 모습만 남아.
- OpenAPI와 Swagger 문서도 리소스별 계약 대신 비슷한 POST 작업의 목록으로 읽히기 쉬워.
HTTP가 여러 표준 메서드를 제공하는 이유는 작업의 의미와 재시도·캐시 계약을 전송 계층에서도 드러내기 위해서야. 모든 작업을 POST로 감추면 구현은 잠깐 단순해질지 몰라도 관찰, 캐시, 복구, 문서화는 더 어려워져.
cwkPippa의 메서드 선택 사례
backend/routes/에는 작업 의미에 맞춘 선택이 곳곳에 있어. POST /api/conversations는 서버가 ID를 할당해 새 대화를 만들고, GET /api/conversations/{id}는 내부 복구 계층을 감춘 채 대화를 조회해. PUT /api/conversations/{id}/title은 같은 제목을 반복해서 보내도 최종 상태가 같도록 이름을 설정하고, PATCH /api/folders/{id}는 색이나 표시 이름 같은 일부 필드만 바꿔. DELETE /api/conversations/{id}는 대화를 제거하고, POST /api/council/{id}/finalize는 부작용이 있는 마무리 명령을 요청해. 각 메서드는 단어를 기계적으로 번역한 결과가 아니라 리소스와 작업의 계약을 표현한 결과야.