본문 바로가기
C.W.K.
Stream
Lesson 06 of 06 · published

HTTP 메서드 결정표 — 작업의 의미에서 메서드까지

~10 min · semantics, methods, decision, synthesis

Level 0HTTP 입문자
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"일상 언어의 동사를 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/42200 OK, 조건부 요청이면 304
리소스 목록 조회GET/users?role=admin200 OK
본문 없이 메타데이터 확인HEAD/users/42200 OK, 본문 없음
허용된 메서드 확인OPTIONS/users/42200 OK + Allow 헤더
서버가 할당한 ID로 생성POST/users (컬렉션)201 Created + Location
클라이언트가 정한 ID로 생성PUT/users/client-picked-id201 Created
기존 리소스 전체 교체PUT/users/42200 OK 또는 204
리소스 일부 수정PATCH/users/42200 OK 또는 204
리소스 제거DELETE/users/42200 OK 또는 204 No Content
오래 걸리는 비동기 작업 생성POST/jobs (컬렉션)202 Accepted + Location
명령 또는 동작 요청POST/charges/42/refund200 OK 또는 202
업로드 리소스 생성POST/uploads201 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을 사용할 수 있어.

가장 비슷한 일상 동사가 아니라 HTTP 계약에 맞는 메서드를 골라. 메서드 선택에는 안전성·멱등성·캐시 가능성뿐 아니라 대상 리소스의 URI, 전체 교체인지 부분 수정인지, 서버가 새 리소스를 할당하는지 같은 리소스 모델도 필요해. 결정 트리는 답을 대신하는 암기표가 아니라, 그 질문들을 빠뜨리지 않게 하는 도구야.

모든 작업을 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는 부작용이 있는 마무리 명령을 요청해. 각 메서드는 단어를 기계적으로 번역한 결과가 아니라 리소스와 작업의 계약을 표현한 결과야.

Code

설계 검토에 활용하는 메서드 선택 도우미·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식 라우트로 보는 메서드별 계약·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}'}

External links

Exercise

다음 열 가지 작업에 맞는 HTTP 메서드를 각각 고른다. 선택한 메서드의 안전성, 멱등성, 캐시 가능성을 밝히고 리소스 모델까지 포함해 한 문장으로 근거를 설명해. (1) 사용자 42 profile 조회, (2) 사용자 42의 이메일 필드 수정, (3) 사용자 42의 전체 profile 교체, (4) 사용자 42 제거, (5) 본문을 다운로드하지 않고 사용자 42의 존재 확인, (6) 사용자 42에게 비밀번호 재설정 이메일 발송, (7) 폴더에 새 대화 생성, (8) 대화 X를 폴더 Y로 이동, (9) Council 라운드 마무리, (10) /users/42에 허용된 메서드 확인.
Hint
결정 트리를 출발점으로 사용해. (1) GET — 안전하고 멱등해. (2) PATCH — 부분 수정이며 사용하는 patch 형식에 따라 멱등성이 달라질 수 있어. (3) PUT — 전체 교체이며 멱등해. (4) DELETE. (5) HEAD. (6) POST + Idempotency-Key — 부작용이 있고 기본적으로 멱등하지 않아. (7) POST — 서버가 대화 ID를 할당해. (8) 대화 리소스의 parent_folder를 PATCH하거나, /conversations/{id}/folder 관계 리소스를 PUT으로 설정할 수 있어. (9) POST + Idempotency-Key — 부작용이 있는 명령이야. (10) OPTIONS. 보너스로 이 계약이 나중에 호환되지 않게 바뀔 경우 어떤 작업이 date-based 버전 관리의 이점을 가장 크게 받을지 설명해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 피파warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.