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

상태 코드 계열 — 첫 숫자로 응답 읽기

~12 min · foundations, status-codes, polymorphic, dispatch

Level 0HTTP 입문자
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"표준 상태 코드는 많지만 전부 외울 필요는 없어. 첫 숫자가 응답의 큰 범주를 알려 주고, 구체적인 코드는 그 안에서 다음 행동을 좁혀 줘."

모든 응답을 아우르는 다섯 계열

HTTP 상태 코드는 첫 숫자에 따라 다섯 계열 가운데 하나에 속해. 각 계열은 클라이언트가 응답을 처리할 때 사용할 기본 계약을 제공해. 잘 설계된 클라이언트는 처음 보는 상태 코드를 만나도 계열을 기준으로 안전한 기본 동작을 선택하고, 알고 있는 개별 코드만 더 세밀하게 처리해.

  • 1xx — 정보. 요청 처리가 진행 중임을 알리는 중간 응답이야. 대표적으로 100 Continue(큰 요청 본문을 보내기 전 진행 허용), 101 Switching Protocols(WebSocket 같은 프로토콜로 전환), 103 Early Hints(최종 응답 전 자원 미리 불러오기 힌트)가 있어.
  • 2xx — 성공. 요청이 성공적으로 처리됐다는 뜻이야. 200 OK(일반적인 성공), 201 Created(새 리소스 생성, 대개 Location 헤더로 위치 안내), 202 Accepted(처리를 접수했지만 아직 완료되지 않음), 204 No Content(성공, 본문 없음), 206 Partial Content(범위 요청의 일부 본문)가 대표적이야.
  • 3xx — 리디렉션. 다른 위치를 사용하거나 저장된 표현을 재사용하는 등 추가 처리가 필요하다는 뜻이야. 301 Moved Permanently(영구 이동), 302 Found(임시 이동), 304 Not Modified(캐시된 표현을 재사용, 본문 없음), 307 Temporary Redirect(메서드와 본문을 보존하는 임시 이동), 308 Permanent Redirect(메서드와 본문을 보존하는 영구 이동)가 있어.
  • 4xx — 클라이언트 오류. 현재 요청을 그대로는 처리할 수 없다는 뜻이야. 400 Bad Request(잘못된 요청 구문이나 프레이밍), 401 Unauthorized(유효한 인증 정보 필요), 403 Forbidden(요청은 이해했지만 허용하지 않음), 404 Not Found, 405 Method Not Allowed, 409 Conflict, 410 Gone, 415 Unsupported Media Type, 422 Unprocessable Entity(구문은 이해했지만 지시를 처리할 수 없음), 429 Too Many Requests가 대표적이야.
  • 5xx — 서버 오류. 서버가 유효한 요청을 처리하지 못했다는 뜻이야. 500 Internal Server Error(예상하지 못한 서버 오류), 502 Bad Gateway(게이트웨이가 상위 서버에서 유효하지 않은 응답을 받음), 503 Service Unavailable(일시적 과부하나 점검), 504 Gateway Timeout(상위 서버가 제시간에 응답하지 않음)을 자주 만나.

계열로 크게 나누고, 코드로 세분화해

운영용 HTTP 클라이언트는 계열을 먼저 보고 기본 경로를 정해:

  • 2xx → 코드별 본문 규칙을 확인한 뒤 성공 결과로 처리해.
  • 3xx → Location을 따라갈 리디렉션인지, 304처럼 캐시를 재사용할 응답인지 개별 코드를 확인해.
  • 4xx → 같은 요청을 그대로 반복하기보다 인증 정보, 입력, 충돌 상태, 속도 제한 같은 원인을 먼저 고쳐. 다만 408이나 429처럼 기다린 뒤 재시도할 수 있는 예외도 있어.
  • 5xx → 일시적 장애일 수 있으므로 멱등한 요청에 한해 횟수 제한과 지수 백오프를 적용한 재시도를 검토해. 모든 요청을 무조건 다시 보내면 중복 부작용이 생길 수 있어.
  • 1xx → 대부분의 HTTP 라이브러리가 최종 응답 전에 내부적으로 처리하지만, 스트리밍이나 프로토콜 전환에서는 의미를 알아둘 필요가 있어.

그다음 알고 있는 개별 코드에 맞춰 행동을 좁혀. 401이면 인증 정보를 얻거나 갱신하고, 429이면 Retry-After를 존중하며, 409이면 현재 리소스 상태를 다시 읽고 충돌을 해결할 수 있어. 계열은 출발점이고, 개별 코드와 메서드의 의미가 최종 행동을 결정해.

상태 코드 계열은 확장 가능한 분기점을 제공해. if status == 200: ... elif status == 201: ... elif status == 204: ...처럼 아는 성공 코드만 나열하면 처음 보는 2xx를 놓치기 쉬워. 먼저 if 200 <= status < 300:으로 성공 계열을 처리하고, 본문 유무처럼 동작이 달라지는 코드만 안쪽에서 세분화하면 더 튼튼해.

가장 헷갈리는 세 쌍

401 vs 403 — 401은 요청에 유효한 인증 정보가 없어서 인증이 필요하다는 뜻이고, 403은 서버가 요청을 이해했지만 권한 정책상 허용하지 않는다는 뜻이야. 403은 인증된 요청에 흔히 쓰이지만, 서버는 리소스 존재를 숨기기 위해 404를 선택할 수도 있어. 두 코드를 로그인 여부 하나로만 단순화하지 말고 인증과 권한의 차이로 이해해야 해.

400 vs 422 — 실무에서는 400을 JSON 구문이나 요청 프레이밍처럼 요청 자체를 해석하기 어려운 경우에, 422를 구문은 이해했지만 필드 검증이나 업무 규칙을 통과하지 못한 경우에 자주 사용해. 예를 들어 이메일 필드에 "not-an-email"이 들어간 요청은 422로 표현할 수 있어. FastAPI는 Pydantic 요청 검증 실패에 기본적으로 422를 사용하고, 다른 프레임워크는 API 정책에 따라 400을 선택하기도 해.

301 vs 308 (그리고 302 vs 307) — 301과 302는 역사적인 클라이언트 동작 때문에 리디렉션 과정에서 POST가 GET으로 바뀔 수 있어. 307과 308은 원래 메서드와 본문을 보존하도록 명시해. API에서 메서드가 바뀌면 안 되는 리디렉션이라면 임시는 307, 영구는 308을 선택하는 편이 안전해.

cwkPippa에서 자주 보는 상태 코드

cwkPippa 백엔드에서는 200(일반적인 GET 성공), 201(POST로 리소스 생성), 202(Council 마무리처럼 오래 걸리는 작업 접수), 204(DELETE 또는 본문 없는 PUT 성공), 400(잘못된 JSON), 401(인증 정보 없음), 403(관리자 전용 엔드포인트 접근 거부), 404(없는 대화), 422(Pydantic 검증 실패), 500(처리되지 않은 백엔드 예외)을 자주 만나. 프런트엔드의 fetch 래퍼(frontend/src/lib/api.ts)도 먼저 상태 코드 계열을 보고, 인증 갱신이나 오류 표시처럼 개별 코드에 필요한 동작을 덧붙이는 방식으로 이해하면 돼.

Code

계열 우선 분기 — 처음 보는 코드에도 대응하기·python
# Polymorphic dispatcher 패턴 — 처음 보는 code 에도 안 죽음
import httpx

def handle(resp: httpx.Response):
    family = resp.status_code // 100
    if family == 2:
        return resp.json() if resp.content else None        # 성공 경로
    if family == 3:
        return follow_redirect(resp.headers['location'])    # 3xx 경로
    if family == 4:
        # Client error — 알려진 case refine, retry 마
        if resp.status_code == 401: refresh_auth()
        elif resp.status_code == 429:
            delay = int(resp.headers.get('retry-after', 1))
            return retry_after(delay)
        raise ClientError(resp.status_code, resp.text)
    if family == 5:
        # Server error — backoff 로 retry
        return retry_with_backoff(resp.request)
    if family == 1:
        # 1xx — HTTP 라이브러리가 처리; 보통 invisible
        return None
    raise Exception(f'알 수 없는 status family: {resp.status_code}')
httpx — 계열별 판별 속성과 raise_for_status 도우미·python
# httpx 는 같은 아이디어의 편의 predicate 줘
import httpx
resp = httpx.get('https://example.com/')

print(resp.is_informational)  # 1xx
print(resp.is_success)        # 2xx
print(resp.is_redirect)       # 3xx (httpx 가 기본 자동 follow)
print(resp.is_client_error)   # 4xx
print(resp.is_server_error)   # 5xx
print(resp.is_error)          # 4xx 나 5xx

# 흔한 idiom — non-2xx 에 raise
try:
    resp.raise_for_status()  # 4xx/5xx 에 HTTPStatusError raise
except httpx.HTTPStatusError as e:
    print(f'{e.response.status_code} on {e.request.url}')
FastAPI — 상태 상수로 읽기 쉬운 HTTPException 만들기·python
# Server 쪽 — FastAPI 는 적절한 code 로 HTTPException raise
from fastapi import FastAPI, HTTPException, status

app = FastAPI()

@app.get('/users/{uid}')
async def read_user(uid: str):
    user = await db_find(uid)
    if user is None:
        raise HTTPException(status.HTTP_404_NOT_FOUND, detail='없는 user')
    return user

@app.post('/users')
async def create_user(payload: dict):
    if 'email' not in payload:
        # 422 — request 는 well-formed, validation 만 실패
        raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY,
                            detail='email 필수')
    return {'id': 'new', **payload}  # FastAPI 기본 200; status_code=201 설정 가능

@app.post('/admin/wipe')
async def wipe(user=Depends(current_user)):
    if user.role != 'admin':
        raise HTTPException(status.HTTP_403_FORBIDDEN)  # 인증됐지만 허용 안 됨
    ...

External links

Exercise

작은 Python 함수 classify(code: int) -> str를 작성해 어떤 입력에도 informational, success, 리디렉션, client_error, server_error 가운데 하나를 반환하도록 해. 그런 다음 공개 API(cwkPippa의 /api/health, GitHub의 /users/octocat, 의도적으로 잘못된 URL 등)를 호출해 응답을 분류해 봐. 보너스로 classify 결과에 should_retry 불리언을 추가하되, 5xx와 408/429만 재시도 후보로 표시해.
Hint
수십 개 상태 코드를 switch문에 나열할 필요는 없어. code // 100으로 계열 숫자를 구한 뒤 1은 informational, 2는 success, 3은 리디렉션, 4는 client_error, 5는 server_error로 매핑하면 돼. should_retry는 계열이 5이거나 코드가 408 또는 429일 때 참으로 둘 수 있어. 실제 재시도 단계에서는 메서드의 멱등성과 Retry-After, 최대 시도 횟수도 함께 고려해야 해.

Progress

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

댓글 0

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

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