HTTP 200인데 실패한 요청? 상태 코드 설계가 API 운영에 미치는 영향
API를 개발하다 보면 한 번쯤 이런 상황을 마주치게 된다. 서버는 HTTP 200 OK를 반환하고 있는데, 실제로는 요청이 처리되지 않은 것이다. 응답 본문을 열어 보면 {"success": false, "message": "사용자를 찾을 수 없습니다"}와 같은 내용이 담겨 있다. 외형상으로는 성공한 것처럼 보이지만, 실질적으로는 실패한 요청이다.
이런 패턴이 왜 생겨나는지, 그리고 운영 환경에서 어떤 문제를 일으키는지 이해하면 더 나은 API 설계 결정을 내릴 수 있다.
HTTP 상태 코드가 존재하는 이유
HTTP 상태 코드는 요청의 결과를 간결하게 표현하기 위해 설계된 표준 체계다. 1xx는 정보 응답, 2xx는 성공, 3xx는 리다이렉션, 4xx는 클라이언트 오류, 5xx는 서버 오류를 나타낸다. 이 분류 체계는 단순히 개발자 간의 약속이 아니라, HTTP를 기반으로 동작하는 수많은 인프라 도구들이 공통으로 이해하는 언어다.
로드 밸런서, API 게이트웨이, 모니터링 시스템, 로그 집계 도구, CDN, 브라우저 캐시 등은 모두 상태 코드를 보고 동작 방식을 결정한다. 상태 코드가 의도한 의미대로 사용되지 않으면, 이 도구들은 잘못된 판단을 내리게 된다.
200 OK로 오류를 감추면 생기는 문제
모니터링과 알림이 작동하지 않는다
대부분의 모니터링 도구는 4xx나 5xx 응답이 증가할 때 알림을 보내도록 설정된다. 오류를 200 응답으로 반환하면 이 알림 체계가 전혀 작동하지 않는다. 시스템 입장에서는 모든 요청이 성공한 것처럼 보이기 때문이다.
실제로 사용자들이 오류를 겪고 있어도 운영자는 대시보드에서 정상적인 수치만 보게 된다. 문제를 인지하는 시점이 늦어지고, 그만큼 대응도 늦어진다.
클라이언트 재시도 로직이 오작동한다
HTTP 클라이언트 라이브러리와 SDK는 상태 코드를 기준으로 재시도 여부를 판단한다. 503 Service Unavailable이나 429 Too Many Requests가 반환되면 지수 백오프를 적용해 재시도하는 것이 일반적인 패턴이다.
하지만 오류가 200으로 반환된다면 클라이언트는 요청이 성공했다고 판단하고 재시도를 시도하지 않는다. 반대로, 응답 본문을 직접 파싱해서 오류를 감지한 클라이언트가 재시도를 구현하면, 이번에는 과도한 재시도가 발생할 수도 있다. 상태 코드가 아닌 응답 본문으로 흐름을 제어할 때 생기는 혼란이다.
로그 분석과 디버깅이 어려워진다
서버 접근 로그에는 상태 코드가 기록된다. 문제가 발생했을 때 로그에서 4xx나 5xx를 검색해 오류 요청을 걸러내는 것이 일반적인 디버깅 방법이다.
오류가 200으로 기록되면 이 방법이 통하지 않는다. 수백만 건의 로그 중에서 실패한 요청만 골라내려면 응답 본문을 파싱해야 하는데, 이는 훨씬 복잡하고 느린 작업이다. 장애 상황에서 신속하게 원인을 파악하기 어려워진다.
캐시와 프록시가 잘못 동작한다
CDN이나 리버스 프록시는 200 응답을 캐시하도록 설정되는 경우가 많다. 만약 오류 응답이 200으로 반환된다면, 이 오류 응답이 캐시될 수 있다. 이후 정상적인 요청을 보내도 캐시된 오류 응답이 반환되는 상황이 발생할 수 있다.
왜 이런 설계가 생겨나는가
이 문제는 단순한 무지에서 비롯되지 않는다. 몇 가지 현실적인 이유가 있다.
비즈니스 로직과 HTTP 오류의 혼동이 가장 흔한 원인이다. “사용자를 찾을 수 없음”이나 “잔액 부족”같은 비즈니스 규칙에 의한 실패를 HTTP 오류와 다르게 취급하고 싶은 경우, 모든 HTTP 응답을 200으로 맞추고 본문에서 성공 여부를 전달하려는 접근이 나타난다.
방화벽이나 보안 장치 우회가 목적인 경우도 있다. 일부 환경에서는 4xx나 5xx 응답이 특정 프록시나 방화벽에 의해 차단되거나 변형된다. 이를 피하기 위해 의도적으로 모든 응답을 200으로 반환하는 경우가 있다.
레거시 시스템과의 호환성 때문에 어쩔 수 없이 유지되는 경우도 있다. 초기에 잘못 설계된 API를 여러 클라이언트가 의존하고 있어, 상태 코드를 바꾸면 기존 클라이언트가 깨지는 상황이 발생할 수 있다.
올바른 상태 코드 설계 원칙
의미에 맞는 코드를 사용한다
요청이 실패했다면 그에 맞는 상태 코드를 반환하는 것이 기본이다. 자주 사용되는 상태 코드와 적절한 사용 맥락을 정리하면 다음과 같다.
| 상태 코드 | 의미 | 사용 예 |
|---|---|---|
| 200 OK | 성공 | 데이터 조회 성공 |
| 201 Created | 생성 성공 | 리소스 생성 완료 |
| 204 No Content | 성공, 반환 데이터 없음 | 삭제 완료 |
| 400 Bad Request | 잘못된 요청 | 필수 파라미터 누락 |
| 401 Unauthorized | 인증 필요 | 로그인되지 않은 접근 |
| 403 Forbidden | 권한 없음 | 접근 권한 부족 |
| 404 Not Found | 리소스 없음 | 존재하지 않는 ID 조회 |
| 409 Conflict | 충돌 | 중복 데이터 생성 시도 |
| 422 Unprocessable Entity | 처리 불가 | 비즈니스 로직 실패 |
| 429 Too Many Requests | 요청 초과 | 속도 제한 초과 |
| 500 Internal Server Error | 서버 오류 | 예상치 못한 예외 발생 |
| 503 Service Unavailable | 서비스 불가 | 점검 중 또는 과부하 |
비즈니스 오류도 적절한 코드로 표현할 수 있다
“사용자를 찾을 수 없다”는 404로, “권한이 없다”는 403으로, “입력값이 비즈니스 규칙에 맞지 않는다”는 422로 표현할 수 있다. HTTP 상태 코드가 비즈니스 로직을 충분히 표현하지 못한다고 느껴지는 경우, 응답 본문에 더 구체적인 오류 코드와 메시지를 함께 제공하면 된다.
상태 코드는 “이 요청이 HTTP 계층에서 어떻게 처리됐는가”를 나타내고, 응답 본문의 오류 코드는 “비즈니스 관점에서 구체적으로 무슨 일이 있었는가”를 나타내는 방식으로 역할을 나누는 것이 좋다.
예를 들어, 결제 금액이 잔액을 초과했을 때 이렇게 응답할 수 있다.
HTTP/1.1 422 Unprocessable Entity
{
"error_code": "INSUFFICIENT_BALANCE",
"message": "잔액이 부족합니다.",
"detail": {
"required": 50000,
"available": 30000
}
}
상태 코드로 실패를 명확히 알리고, 본문으로 클라이언트가 조치를 취할 수 있는 정보를 제공하는 구조다.
일관된 오류 응답 형식을 유지한다
상태 코드만큼 중요한 것이 오류 응답의 구조적 일관성이다. 어떤 엔드포인트에서는 {"error": "메시지"}를 반환하고, 다른 엔드포인트에서는 {"message": "메시지", "code": 1234}를 반환한다면, 클라이언트는 각 엔드포인트마다 오류 처리 로직을 따로 작성해야 한다.
오류 응답 구조를 통일하면 클라이언트 개발이 단순해지고, 공통 오류 처리 미들웨어를 작성하기도 쉬워진다.
API 운영 품질과의 연결
상태 코드 설계는 개발 단계의 문제처럼 보이지만, 실제로는 운영 단계에서 그 영향이 더 크게 드러난다.
SLI와 SLO 측정이 상태 코드에 의존한다. 서비스 수준 지표를 정의할 때 성공 요청 비율을 측정하는데, 이 측정값이 HTTP 상태 코드를 기준으로 계산된다. 오류가 200으로 기록된다면 실제 성공률을 측정하는 것이 불가능해진다.
인프라 자동화도 상태 코드에 반응한다. 쿠버네티스의 헬스 체크, 로드 밸런서의 헬시 체크, 서킷 브레이커 패턴 모두 상태 코드를 기준으로 동작한다. 서버가 오류를 200으로 반환하면 이 자동화 체계들이 제대로 반응하지 못한다.
개발자 경험에도 차이가 있다. 새로운 팀원이 API를 처음 사용할 때, 또는 외부 파트너사가 API를 연동할 때, 상태 코드가 의미 그대로 동작하는 API는 별도의 문서 없이도 직관적으로 이해할 수 있다. 반면 모든 응답이 200이고 본문을 파싱해야 하는 API는 연동 비용이 높아진다.
레거시 API를 개선할 때
이미 잘못된 상태 코드 체계로 운영 중인 API를 개선해야 한다면, 하위 호환성을 고려해야 한다. 기존 클라이언트를 갑자기 깨뜨리지 않으면서 점진적으로 개선하는 방법이 필요하다.
새로운 버전의 API 엔드포인트를 /v2/ 경로로 제공하고, 기존 /v1/ 엔드포인트는 유지하되 deprecated 상태로 명시하는 방식이 일반적이다. 클라이언트에게 마이그레이션 기간을 충분히 주고, API 버전별 일몰 계획을 명확하게 공지하는 것이 중요하다.
또한 개선 과정에서 상태 코드 변경이 클라이언트에 미치는 영향을 사전에 분석하고, 주요 클라이언트 팀과 충분히 소통하는 것이 필요하다.
설계 단계에서 시작하는 것이 최선이다
상태 코드 문제는 운영 중에 발견하면 수정 비용이 크다. 여러 클라이언트가 기존 동작에 의존하고 있기 때문이다. 처음 API를 설계할 때부터 HTTP 상태 코드의 의미를 정확히 이해하고 올바르게 적용하는 것이 가장 효율적인 접근이다.
API 설계 리뷰 단계에서 상태 코드 사용이 적절한지 확인하는 체크리스트를 갖추는 것도 좋은 방법이다. 팀 내에서 상태 코드 사용 기준을 문서화하고 공유하면, 여러 개발자가 작업하는 환경에서도 일관성을 유지하기 쉽다.
단순해 보이는 상태 코드 하나가 모니터링, 디버깅, 자동화, 클라이언트 경험 전반에 걸쳐 영향을 미친다. HTTP가 제공하는 표준 체계를 제대로 활용하는 것이 API 운영 품질을 높이는 가장 기본적인 출발점이다.