
안녕하세요, 자바파커입니다.
"API에서 에러가 났는데 500만 던지고 있습니다. 어떤 코드를 써야 하나요?"
솔직히 개발 초반에는 200(성공)과 500(에러), 이 두 개만 쓰는 경우가 많습니다. 하지만 적절한 상태 코드를 사용하면 프론트엔드가 에러를 자동으로 처리할 수 있고, 디버깅 시간이 절반으로 줄어듭니다.
결론부터 말씀드리면 — 실무에서 자주 쓰는 코드는 15개 정도입니다. 오늘은 전체 구조를 훑은 뒤, 실무에서 꼭 알아야 할 코드를 중심으로 정리하겠습니다.
HTTP 응답 코드 구조 — 첫 숫자가 핵심
HTTP 상태 코드는 3자리 숫자이고, 첫 번째 숫자가 응답의 종류를 결정합니다.
| 범위 | 분류 | 의미 | 비유 |
|---|---|---|---|
| 1xx | 정보 | "알겠습니다, 계속하세요" | 전화 연결 중 "잠시만요" |
| 2xx | 성공 | "요청을 정상 처리했습니다" | "주문 완료되었습니다" |
| 3xx | 리다이렉션 | "다른 곳으로 가세요" | "매장이 이전했습니다" |
| 4xx | 클라이언트 에러 | "당신의 요청에 문제가 있습니다" | "주문서를 잘못 작성하셨습니다" |
| 5xx | 서버 에러 | "서버에서 문제가 발생했습니다" | "주방에서 불이 났습니다" |
핵심: 4xx는 클라이언트(요청자)의 잘못, 5xx는 서버의 잘못입니다. 이 구분을 명확히 하는 것만으로도 디버깅 시간이 크게 줄어듭니다.
1xx — 정보 응답
실무에서 직접 다룰 일은 거의 없지만, 알아두면 로그 분석 시 도움이 됩니다.
| 코드 | 이름 | 설명 |
|---|---|---|
| 100 | Continue | 요청 헤더를 받았으니 본문을 보내도 됩니다 |
| 101 | Switching Protocols | WebSocket 전환 시 사용 |
| 103 | Early Hints | 브라우저가 리소스를 미리 로드할 수 있도록 힌트 제공 |
실무에서 만나는 경우
# WebSocket 연결 시 101 응답
GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade2xx — 성공
가장 많이 보고 싶은 응답 코드입니다.
| 코드 | 이름 | 설명 | 사용 시점 |
|---|---|---|---|
| 200 | OK | 요청 성공 | GET 조회, PUT 수정 후 결과 반환 |
| 201 | Created | 리소스 생성 완료 | POST로 새 데이터 생성 |
| 202 | Accepted | 요청 접수됨 (아직 처리 중) | 비동기 작업 요청 |
| 204 | No Content | 성공했지만 응답 본문 없음 | DELETE 완료 |
실무 포인트
# 200 — 조회 성공
GET /api/users/1
→ 200 OK
→ {"id": 1, "name": "김철수", "role": "개발자"}
# 201 — 생성 성공 (Location 헤더에 새 리소스 경로)
POST /api/users
→ 201 Created
→ Location: /api/users/42
→ {"id": 42, "name": "이영희"}
# 202 — 비동기 작업 접수 (이메일 발송, 대용량 처리 등)
POST /api/reports/generate
→ 202 Accepted
→ {"jobId": "abc123", "status": "processing"}
# 204 — 삭제 성공 (본문 없이 응답)
DELETE /api/users/1
→ 204 No Content흔한 실수: 리소스를 생성(POST)하고 200을 반환하는 경우가 많습니다. 201을 쓰는 게 표준이고, 프론트엔드가 "새로 만들어졌구나"라고 인지할 수 있습니다.
3xx — 리다이렉션
URL이 바뀌었거나, 다른 곳으로 안내할 때 사용합니다.
| 코드 | 이름 | 설명 | 사용 시점 |
|---|---|---|---|
| 301 | Moved Permanently | 영구 이동 | 도메인 변경, URL 구조 변경 |
| 302 | Found | 임시 이동 | 로그인 후 원래 페이지로 |
| 304 | Not Modified | 변경 없음 (캐시 사용) | 브라우저 캐싱 |
| 307 | Temporary Redirect | 임시 이동 (메서드 유지) | HTTPS 강제 리다이렉트 |
| 308 | Permanent Redirect | 영구 이동 (메서드 유지) | API URL 영구 변경 |
301 vs 302 — 실무에서 가장 중요한 차이
# 301 — 영구 이동: 검색 엔진이 새 URL을 인덱싱
# SEO에서 매우 중요. 잘못 쓰면 검색 순위가 사라질 수 있음
HTTP/1.1 301 Moved Permanently
Location: https://new-domain.com/page
# 302 — 임시 이동: 검색 엔진이 기존 URL 유지
# 점검 페이지, 로그인 후 리다이렉트 등
HTTP/1.1 302 Found
Location: /login?redirect=/dashboard304 — 캐시의 핵심
# 1차 요청
GET /api/data
→ 200 OK
→ ETag: "abc123"
# 2차 요청 (브라우저가 ETag를 보냄)
GET /api/data
If-None-Match: "abc123"
→ 304 Not Modified ← 본문 없이 "캐시 쓰세요" 응답301과 308의 차이: 301은 리다이렉트 시 POST → GET으로 바뀔 수 있습니다. 308은 원래 메서드를 유지합니다. API에서는 308이 더 안전합니다.
4xx — 클라이언트 에러
"당신이 잘못한 겁니다" — 요청 자체에 문제가 있을 때 사용합니다.
| 코드 | 이름 | 설명 | 사용 시점 |
|---|---|---|---|
| 400 | Bad Request | 요청 형식 오류 | 필수 파라미터 누락, JSON 파싱 실패 |
| 401 | Unauthorized | 인증 필요 | 로그인 안 됨, 토큰 만료 |
| 403 | Forbidden | 권한 없음 | 로그인은 됐지만 접근 권한 부족 |
| 404 | Not Found | 리소스 없음 | 존재하지 않는 URL/ID |
| 405 | Method Not Allowed | 메서드 불허 | GET만 허용되는 URL에 POST |
| 408 | Request Timeout | 요청 시간 초과 | 클라이언트 응답이 너무 늦음 |
| 409 | Conflict | 충돌 | 중복 데이터, 동시 수정 충돌 |
| 413 | Payload Too Large | 요청 본문 초과 | 파일 업로드 크기 제한 |
| 415 | Unsupported Media Type | 미지원 타입 | JSON 기대했는데 XML이 옴 |
| 422 | Unprocessable Entity | 처리 불가 | 형식은 맞지만 비즈니스 로직 위반 |
| 429 | Too Many Requests | 요청 과다 | Rate Limit 초과 |
401 vs 403 — 가장 많이 혼동하는 코드
# 401 — "누구세요?" (인증 실패)
# 로그인하지 않았거나 토큰이 만료됨
GET /api/profile
→ 401 Unauthorized
→ {"error": "토큰이 만료되었습니다. 다시 로그인해주세요."}
# 403 — "당신은 안 됩니다" (인가 실패)
# 로그인은 했지만 해당 리소스에 접근 권한이 없음
GET /api/admin/settings
→ 403 Forbidden
→ {"error": "관리자 권한이 필요합니다."}| 401 Unauthorized | 403 Forbidden | |
|---|---|---|
| 인증(Authentication) | 실패 | 성공 |
| 인가(Authorization) | 확인 불가 | 실패 |
| 해결법 | 로그인/토큰 갱신 | 권한 요청 |
400 vs 422 — 미묘한 차이
# 400 — 요청 자체가 깨짐 (파싱 불가)
POST /api/users
Content-Type: application/json
Body: {invalid json...
→ 400 Bad Request
→ {"error": "JSON 파싱에 실패했습니다."}
# 422 — 형식은 맞지만 내용이 유효하지 않음
POST /api/users
Body: {"email": "not-an-email", "age": -5}
→ 422 Unprocessable Entity
→ {"errors": [
{"field": "email", "message": "올바른 이메일 형식이 아닙니다"},
{"field": "age", "message": "0 이상이어야 합니다"}
]}429 — Rate Limiting
# API 호출 제한 초과
GET /api/search?q=test
→ 429 Too Many Requests
→ Retry-After: 30 ← 30초 후 재시도
→ X-RateLimit-Limit: 100
→ X-RateLimit-Remaining: 0
→ {"error": "요청 제한을 초과했습니다. 30초 후 다시 시도해주세요."}5xx — 서버 에러
"우리가 잘못한 겁니다" — 서버 측 문제로 요청을 처리하지 못할 때 사용합니다.
| 코드 | 이름 | 설명 | 사용 시점 |
|---|---|---|---|
| 500 | Internal Server Error | 서버 내부 오류 | 예기치 않은 예외, 버그 |
| 502 | Bad Gateway | 게이트웨이 오류 | 업스트림 서버 연결 실패 |
| 503 | Service Unavailable | 서비스 불가 | 서버 점검, 과부하 |
| 504 | Gateway Timeout | 게이트웨이 시간 초과 | 업스트림 서버 응답 지연 |
502 vs 503 vs 504 — 어디서 문제인지 파악하기
클라이언트 → [Nginx/ALB] → [애플리케이션 서버] → [DB]
│
├── 502: 앱 서버가 죽었거나 잘못된 응답 반환
├── 503: 앱 서버가 점검 중이거나 과부하
└── 504: 앱 서버가 응답을 너무 늦게 줌# 502 — 앱 서버 프로세스가 죽은 경우
→ 502 Bad Gateway
→ Nginx 로그: "upstream prematurely closed connection"
# 503 — 점검 모드 (Retry-After 헤더 포함)
→ 503 Service Unavailable
→ Retry-After: 3600
→ {"error": "서버 점검 중입니다. 1시간 후 다시 시도해주세요."}
# 504 — DB 쿼리가 30초 넘게 걸린 경우
→ 504 Gateway Timeout
→ Nginx 로그: "upstream timed out (110: Connection timed out)"중요: 500 에러에 내부 스택 트레이스를 노출하지 마세요. 보안 취약점이 됩니다. 로그에는 상세 정보를 남기되, 응답에는 일반적인 메시지만 반환하세요.
REST API 설계 시 상태 코드 선택 가이드
어떤 상황에 어떤 코드를 써야 하는지 빠르게 참고할 수 있는 가이드입니다.
CRUD별 권장 코드
| 메서드 | 성공 | 실패 (클라이언트) | 실패 (서버) |
|---|---|---|---|
| GET (조회) | 200 | 404 (없음), 400 (파라미터 오류) | 500 |
| POST (생성) | 201 | 400 (형식 오류), 409 (중복), 422 (유효성) | 500 |
| PUT (전체 수정) | 200 | 404 (없음), 400, 422 | 500 |
| PATCH (부분 수정) | 200 | 404, 400, 422 | 500 |
| DELETE (삭제) | 204 | 404 (없음) | 500 |
인증·인가 흐름
요청 → 토큰 없음? → 401 Unauthorized
→ 토큰 만료? → 401 Unauthorized
→ 토큰 유효, 권한 없음? → 403 Forbidden
→ 토큰 유효, 권한 있음 → 200/201/204...에러 응답 본문 표준 형식
{
"status": 422,
"error": "Unprocessable Entity",
"message": "입력 데이터가 유효하지 않습니다.",
"details": [
{ "field": "email", "message": "올바른 이메일 형식이 아닙니다" },
{ "field": "age", "message": "0 이상이어야 합니다" }
],
"timestamp": "2026-04-04T09:30:00Z",
"path": "/api/users"
}실무에서 꼭 기억할 코드 TOP 10
모든 코드를 외울 필요는 없습니다. 이 10개만 정확히 사용해도 대부분의 상황을 커버합니다.
| 순위 | 코드 | 한 줄 요약 |
|---|---|---|
| 1 | 200 | 성공 |
| 2 | 201 | 생성 성공 |
| 3 | 204 | 성공, 본문 없음 (삭제) |
| 4 | 400 | 잘못된 요청 |
| 5 | 401 | 인증 필요 (로그인 안 됨) |
| 6 | 403 | 권한 없음 (로그인은 됨) |
| 7 | 404 | 찾을 수 없음 |
| 8 | 409 | 충돌 (중복) |
| 9 | 422 | 유효성 검증 실패 |
| 10 | 500 | 서버 에러 |
FAQ — 자주 묻는 질문
Q. 200과 204 중 어떤 걸 써야 하나요?
응답 본문이 있으면 200, 없으면 204입니다. DELETE 후 삭제된 데이터를 반환하고 싶으면 200, 그냥 "삭제됨"만 알리려면 204를 사용합니다. 팀 내 컨벤션을 정하고 일관되게 사용하는 게 가장 중요합니다.
Q. 에러 시 200 + 에러 메시지를 반환하면 안 되나요?
기술적으로는 가능하지만 안티패턴입니다. HTTP 클라이언트, 프록시, 모니터링 도구는 모두 상태 코드를 기준으로 성공/실패를 판단합니다. 200으로 에러를 반환하면 모니터링에서 에러가 잡히지 않고, 프론트엔드도 매번 본문을 파싱해야 합니다.
Q. 404와 410의 차이는?
404는 "없음 (있었는지도 모름)", 410 Gone은 "있었는데 삭제됨 (다시 안 돌아옴)"입니다. 검색 엔진에 "이 URL은 영구 삭제됐으니 인덱스에서 빼라"고 알리고 싶을 때 410을 사용합니다. 일반 API에서는 404면 충분합니다.
마무리
HTTP 상태 코드는 서버와 클라이언트가 대화하는 공통 언어입니다. 적절한 코드를 사용하면 프론트엔드 개발자가 별도 문서 없이도 에러를 처리할 수 있고, 모니터링 도구가 자동으로 문제를 감지할 수 있습니다.
처음에는 TOP 10만 정확히 사용하는 것부터 시작하세요. 그것만으로도 API 품질이 크게 올라갑니다.
여러분의 API에서는 어떤 상태 코드를 주로 사용하고 계신가요? 실무에서 겪은 재미있는 에러 코드 경험이 있다면 댓글로 공유해주세요!