"영어 verb 를 HTTP method 로 번역 그만해. 'Create' 가 항상 POST 아냐; 'update' 가 항상 PUT 아냐. 선택은 세 invariant — safety, idempotency, cacheability — 가 이끌어, 사전 아냐."
결정 tree
설계하는 모든 operation 에 대해, 이 tree 를 위에서 아래로 걸어:
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)
Method/Outcome 매트릭스
대부분 operation 이 이 패턴 중 하나에 맞아:
의도
Method
URI shape
성공 시 status code
Resource 읽기
GET
/users/42
200 OK (혹은 cached 면 304)
Resource 나열
GET
/users?role=admin
200 OK
존재 싸게 테스트
HEAD
/users/42
200 OK (body 없음)
허용 method 발견
OPTIONS
/users/42
200 OK + Allow header
Server 할당 ID 로 생성
POST
/users (컬렉션)
201 Created + Location
Client 할당 ID 로 생성
PUT
/users/client-picked-id
201 Created
기존 resource 교체
PUT
/users/42
200 OK (혹은 204)
부분 update
PATCH
/users/42
200 OK
제거
DELETE
/users/42
204 No Content
Async / long-running
POST
/jobs (컬렉션)
202 Accepted + Location
Command / action
POST
/charges/42/refund
200 OK or 202
파일 upload
POST
/uploads
201 Created
풀어쓴 예 셋
"User 42 한테 환영 이메일 보내." 이메일 자체엔 명백한 target resource 없음; operation 에 side effect 있음 (이메일 나감); idempotent 아님 (두 번 보내면 이메일 두 개). Action URI 에 POST: POST /users/42/welcome-emails 혹은 수신자 명시한 body 가진 POST /emails. 안전한 retry 원하면 Idempotency-Key header 추가.
"User 42 의 이메일 주소 update." Target URI 있음; client 가 resource 알음; 부분 update. {"email": "new@example.com"} 로 PATCH. 혹은 API 가 PUT-everything 스타일이면 (어떤 건 그래) 전체 user payload 로 PUT.
"Payment 42 환불." "환불 operation" 에 명백한 자연 resource 없음; operation 에 side effect 있음; 협조 없이 거의 확실히 idempotent 아님. Action URI 에 POST: POST /payments/42/refunds. Idempotency-Key 추가. Response 는 201 (Refund resource 생성됨) 이나 200 (operation 성공) 될 수 있음.
가장 가까운 영어 단어 아닌 계약 맞는 verb 골라. 'Update' 가 PUT 의미 아님; 'create' 가 POST 의미 아님; 'remove' 가 DELETE 의미 아님. Method 는 세 invariant 가 골라 — operation 이 state 변경, retry 안전성, cacheability 에 대해 뭘 약속해? Tree 걸어; verb 가 떨어져 나와.
'모든 거 POST 로 터널' 안티패턴
어떤 팀은 모든 endpoint 를 POST 로 기본. "항상 동작. 생각할 필요 없어." 비용 실제:
CDN 이 response 캐시 못 함 (POST 기본 cacheable 아님).
Client 가 transport 실패에 안전 retry 못 함 (POST not idempotent).
브라우저와 HTTP intermediary 가 최적화 못 함 (speculative prefetch 없음, conditional GET 없음).
로그와 dashboard 가 진단 신호 잃음 — 모든 operation 이 "POST 뭔가 일어남" 처럼 보임.
OpenAPI / Swagger 문서가 shape 없는 POST 벽이 됨.
Protocol 이 verb 일곱 주는 이유가 정확히 이 정보가 wire 에 있게 하려고. 버리면 system 이 덜 관찰 가능, 덜 cacheable, 덜 retryable. 하지 마.
cwkPippa 의 method 선택
backend/routes/ 걸으면 의도적 선택 곳곳에 있어. POST /api/conversations 는 server 할당 ID 로 생성; GET /api/conversations/{id} 는 읽기 (healing 층이 client 한텐 투명); PUT /api/conversations/{id}/title 는 이름 변경 (idempotent — 같은 이름 → 같은 state); PATCH /api/folders/{id} 는 색이나 display name 변경; DELETE /api/conversations/{id} 는 제거; POST /api/council/{id}/finalize 는 command (not idempotent — 두 번 finalize 면 버그). 이 모든 선택이 tree 걸어 나옴.
Code
결정 helper 함수 — 설계 review 때 써·python
# Code review 나 설계 논의용 'method picker' helper
from dataclasses import dataclass
@dataclass
class Operation:
changes_state: bool
target_uri_known: bool
sends_full_resource: bool
is_removal: bool
has_natural_target: bool
def pick_method(op: Operation) -> str:
if not op.changes_state:
return 'GET (존재 체크엔 HEAD, 발견엔 OPTIONS)'
if op.is_removal:
return 'DELETE'
if not op.has_natural_target:
return 'Action URI 에 POST (예: POST /resource/{id}/action)'
if not op.target_uri_known:
return 'Collection 에 POST (server 가 ID 할당, 201 + Location 돌려줌)'
if op.sends_full_resource:
return 'PUT (idempotent — 전체 교체)'
return 'PATCH (patch format 이 보장할 때만 idempotent)'
# 예 걸어보기
print(pick_method(Operation(changes_state=False, target_uri_known=True,
sends_full_resource=False, is_removal=False,
has_natural_target=True)))
# GET (존재 체크엔 HEAD, 발견엔 OPTIONS)
print(pick_method(Operation(changes_state=True, target_uri_known=False,
sends_full_resource=False, is_removal=False,
has_natural_target=True)))
# Collection 에 POST (server 가 ID 할당, 201 + Location 돌려줌)
cwkPippa-스타일 route: 각 method 가 선택된 계약·python
# cwkPippa-스타일 FastAPI route — 각 method 선택이 의도적
from fastapi import FastAPI, APIRouter, status
app = FastAPI()
conversations = APIRouter(prefix='/api/conversations')
@conversations.get('/{cid}')
async def read_conversation(cid: str):
# State 변경 없음, target URI 알려짐 → GET
return await load_from_db(cid)
@conversations.post('', status_code=status.HTTP_201_CREATED)
async def create_conversation():
# Server 가 ID 할당; 201 + Location 돌려줌
new_id = await create_in_db()
return {'id': new_id, 'location': f'/api/conversations/{new_id}'}
@conversations.put('/{cid}/title')
async def rename_conversation(cid: str, payload: dict):
# Idempotent: 같은 title → 같은 state. PUT 이 맞음.
await update_title(cid, payload['title'])
return {'id': cid, 'title': payload['title']}
@conversations.patch('/{cid}')
async def update_conversation(cid: str, payload: dict):
# 부분 update: payload 가 필드의 어느 부분집합이든 가질 수 있음. PATCH.
return await apply_patch(cid, payload)
@conversations.delete('/{cid}', status_code=status.HTTP_204_NO_CONTENT)
async def delete_conversation(cid: str):
await delete_from_db(cid) # DELETE — idempotent
@conversations.post('/{cid}/finalize', status_code=status.HTTP_202_ACCEPTED)
async def finalize_council(cid: str):
# Side effect 있는 command/action, idempotent 아님 — POST
job_id = await enqueue_finalize(cid)
return {'job_id': job_id, 'poll': f'/api/jobs/{job_id}'}
이 operation 열 개 받고 각각에 맞는 HTTP method 골라. 각각에 세 invariant (safe / idempotent / cacheable) 써서 한 문장 정당화: (1) user 42 profile fetch, (2) user 42 email 필드 update, (3) user 42 전체 profile 교체, (4) user 42 제거, (5) body 안 download 하고 user 42 존재 확인, (6) user 42 한테 비밀번호 재설정 이메일, (7) 폴더에 새 대화 생성, (8) 대화 X 를 폴더 Y 로 이동, (9) council round finalize, (10) /users/42 에 허용된 method 발견.
Hint
Lesson 의 결정 tree 써. (1) GET — safe + idempotent. (2) PATCH — 부분 update, 꼭 idempotent 아님. (3) PUT — 전체 교체, idempotent. (4) DELETE. (5) HEAD. (6) POST + Idempotency-Key — side effect, not idempotent. (7) POST — server 가 대화 ID 할당. (8) PATCH 의 'parent_folder' 필드에 PUT, 혹은 /conversations/{id}/folder 에 PUT. (9) POST + Idempotency-Key — command, side effect. (10) OPTIONS. 보너스: 이 중 어떤 게 date-based versioning 전략 으로부터 가장 이득 볼지 식별.
Progress
Progress is local-only — sign in to sync across devices.