HTTP 상태 코드와 에러 응답 설계
코드 선택 기준
| 코드 | 언제 쓰나 | 흔한 오용 |
|---|
| 200, 201, 204 | 성공, 생성, 본문 없음 | 실패인데 200 + 본문 에러 |
| 400 | 형식과 문법 오류 | 검증 실패를 전부 400으로 |
| 401, 403 | 미인증과 권한 없음 | 둘을 뒤섞음 |
| 404, 409 | 없음과 상태 충돌 | 중복 생성에 400 |
| 422 | 형식은 맞지만 처리 불가 | 400과 혼용 |
| 429 | 요청 한도 초과 | 503으로 대체 |
| 500, 503 | 서버 결함과 일시 과부하 | 클라이언트 잘못을 500으로 |
에러 본문 표준화
{
"type": "/errors/insufficient-balance",
"title": "잔액이 부족합니다",
"status": 409,
"detail": "요청 12000원, 잔액 3500원"
}
기계가 분기할 값(type)과 사람이 읽을 문장(title, detail)을 분리한다. 필드 검증 오류는 어느 필드가 왜 틀렸는지 배열로 함께 준다.
실무 포인트
- 200 고정 + 본문 에러코드 방식은 프록시와 모니터링이 전부 성공으로 집계해 장애 감지를 늦춘다
- 5xx는 재시도 대상, 4xx는 재시도 무의미하다. 상태 코드가 클라이언트 재시도 정책의 입력값이다
- 권한 없는 리소스의 존재 여부를 숨기려면 403 대신 404를 주는 선택도 있다
- 내부 예외 메시지와 스택트레이스를 응답에 넣지 않는다
Q.모든 응답을 200으로 주고 본문에 에러 코드를 담는 API의 문제는 무엇인가요?
HTTP 를 이해하는 모든 중간 계층이 성공으로 판단하기 때문입니다.
| 영향받는 것 | 결과 |
|---|
| 로드밸런서 | 죽은 서버를 정상으로 보고 계속 요청을 보낸다 |
| 모니터링 | 오류율이 항상 0이라 장애를 감지하지 못한다 |
| CDN 과 캐시 | 오류 응답을 캐시해서 계속 내보낸다 |
| 클라이언트 라이브러리 | 예외를 던지지 않아 오류 처리가 누락된다 |
| 재시도 | 재시도해야 할지 판단할 근거가 없다 |
두 번째가 특히 위험합니다. 대시보드의 오류율이 0인데 사용자는 아무것도 못 하는 상태가 됩니다. 문제를 알려면 응답 본문을 파싱하는 별도 계측을 만들어야 하고, 그것은 이미 있는 표준을 다시 만드는 일입니다.
세 번째도 실제 사고를 만듭니다. 오류 응답이 캐시되면 원인이 사라진 뒤에도 계속 오류가 나갑니다.
이 방식을 택하는 이유는 대개 클라이언트 처리를 단순하게 하려는 것인데, 그러면 오류 처리가 없는 클라이언트가 만들어지기 쉽습니다.
흔한 실수: 상태 코드와 본문 코드를 양자택일로 보는 것. 함께 씁니다. 상태 코드로 종류를 알리고, 본문에 세부 코드와 메시지를 담습니다.
Q.400과 422, 401과 403을 각각 어떤 기준으로 구분하나요?
| 코드 | 기준 |
|---|
| 400 | 요청 자체를 해석할 수 없다. JSON 형식 오류, 타입 불일치 |
| 422 | 형식은 맞지만 값이 규칙에 어긋난다. 이메일 형식, 필수 값 누락 |
| 401 | 신원을 모른다. 토큰이 없거나 잘못됐거나 만료됐다 |
| 403 | 신원은 알지만 권한이 없다 |
앞의 둘은 파싱 단계에서 실패했는지, 검증 단계에서 실패했는지로 갈립니다. 구분하는 값은 클라이언트가 대응을 달리할 수 있다는 것입니다. 400 은 코드 버그이고, 422 는 사용자에게 입력을 고쳐 달라고 안내할 수 있습니다.
다만 422 를 안 쓰고 400 으로 통일하는 팀도 많습니다. 어느 쪽이든 팀 안에서 일관되면 됩니다.
뒤의 둘은 실무에서 자주 틀립니다.
| 상황 | 코드 |
|---|
| 토큰 만료 | 401. 갱신하면 되므로 클라이언트가 재발급을 시도할 수 있다 |
| 권한 부족 | 403. 재로그인해도 달라지지 않는다 |
이 구분이 실제 동작을 바꿉니다. 401 을 받으면 클라이언트가 토큰을 갱신하고 재시도하고, 403 이면 하지 않습니다. 권한 부족에 401 을 주면 갱신과 재시도를 무한히 반복합니다.
흔한 실수: 403 을 주어 자원의 존재를 노출하는 것. 민감한 자원은 404 로 존재 자체를 숨기는 편이 안전합니다.
Q.클라이언트가 자동 재시도를 판단하려면 서버가 무엇을 제공해야 하나요?
다시 시도해도 되는지, 언제 하면 되는지를 서버가 알려줘야 합니다.
| 제공할 것 | 내용 |
|---|
| 적절한 상태 코드 | 503 과 429 는 재시도 가능, 400 은 아니다 |
| Retry-After 헤더 | 몇 초 뒤에 오라고 알려준다 |
| 멱등성 보장 | 재시도해도 중복되지 않게 멱등 키를 받는다 |
| 오류 코드 구분 | 본문에 일시적인지 영구적인지 분류를 담는다 |
| 요청 식별자 | 같은 요청인지 서버가 판별할 수 있게 |
상태 코드만으로는 부족한 경우가 있습니다. 500 은 일시적일 수도, 코드 버그일 수도 있습니다. 그래서 본문에 세부 분류를 담아 주면 클라이언트가 판단할 수 있습니다.
| 코드 | 재시도 |
|---|
| 408, 429, 503, 504 | 한다 |
| 500, 502 | 조건부로. 멱등한 요청만 |
| 400, 401, 403, 404, 422 | 하지 않는다 |
| 409 | 상황에 따라. 충돌 원인을 봐야 한다 |
두 번째 줄이 판단이 필요한 자리입니다. POST 로 결제를 만들다 500 을 받으면 실제로 처리됐는지 알 수 없습니다. 멱등 키가 없으면 재시도는 이중 결제 위험이고, 안 하면 누락 위험입니다. 그래서 서버가 멱등 키를 받아 주는 것이 재시도의 전제 조건입니다.
흔한 실수: Retry-After 를 무시하고 고정 간격으로 재시도하는 것. 서버가 알려준 시간보다 빨리 오면 상대를 더 밀어붙입니다.
Q.검증 오류가 여러 필드에서 났을 때 응답을 어떻게 설계하겠습니까?
전부 모아 한 번에 돌려줍니다. 첫 오류에서 멈추면 사용자가 여러 번 왕복해야 합니다.
응답 구조는 이런 형태가 됩니다.
| 항목 | 내용 |
|---|
| 최상위 | 오류 종류와 요청 식별자 |
| errors 배열 | 필드별 오류 목록 |
| 각 항목 | 필드 경로, 오류 코드, 사람이 읽을 메시지 |
| 필드 | 담는 것 | 예 |
|---|
| field | 중첩까지 표현하는 경로 | items[0].quantity |
| code | 기계가 판단할 코드 | TOO_SMALL |
| message | 화면에 보일 문구 | 수량은 1 이상이어야 합니다 |
code 와 message 를 나누는 이유가 있습니다. 클라이언트가 문구를 자기 언어로 바꾸거나 필드별 표시 위치를 정하려면 기계가 읽을 코드가 필요합니다. 메시지만 주면 문자열 비교로 분기하게 됩니다.
필드 경로를 정확히 주는 것도 중요합니다. 배열 안 몇 번째 항목의 어느 필드인지 알려주지 않으면 화면에서 어디에 오류를 표시할지 알 수 없습니다.
흔한 실수: 검증 실패에 첫 오류만 담는 것. 사용자가 다섯 번 저장을 눌러야 다섯 개를 다 고칩니다. 서버 부하보다 왕복 횟수가 사용자 경험에 훨씬 크게 작용합니다.
먼저 스스로 답해보고 아래 답변과 견줘보세요. 막히는 부분은 문제로 확인할 수 있어요.
읽었으면 문제로 확인해보세요
API 설계 문제를 풀면 틀린 문제가 자동으로 노트에 쌓입니다. 가입 없이 5문제를 먼저 풀어볼 수도 있어요.