HTTP 상태 코드, 왜 응답 Body만으로는 부족할까?

북금곰·2026년 7월 27일

백엔드 기본 지식

목록 보기
9/29

HTTP 상태 코드는 서버가 요청을 처리한 결과를 클라이언트에게 알려주는 숫자다.

API를 호출했을 때 요청이 성공했는지, 잘못된 요청인지, 로그인이 필요한지, 권한이 없는지, 데이터를 찾지 못했는지 등을 상태 코드로 구분할 수 있다.

예를 들어 클라이언트가 서버에 사용자 정보를 요청했다고 해보자.

GET /users/1

서버는 요청을 처리한 뒤 먼저 상태 코드로 큰 결과를 알려준다.

200: 요청을 정상적으로 처리했습니다.
400: 요청한 값이나 형식이 잘못됐습니다.
401: 로그인이 필요합니다.
403: 로그인은 했지만 권한이 없습니다.
404: 요청한 대상을 찾을 수 없습니다.
500: 서버 내부에서 문제가 발생했습니다.

그리고 응답 Body에는 조회한 데이터나 구체적인 오류 내용을 담는다.

{
  "code": "USER_NOT_FOUND",
  "message": "사용자를 찾을 수 없습니다."
}

정리하면 다음과 같다.

Status Code
요청이 어떤 결과로 끝났는지 알려주는 값

Response Body
결과에 대한 데이터나 자세한 설명

HTTP 상태 코드의 종류

MDN의 HTTP response status codes 문서에서는 상태 코드를 HTTP 요청이 성공적으로 완료되었는지를 알려주는 값으로 설명한다.

상태 코드는 크게 다섯 그룹으로 나뉜다.

100번대: 요청 처리 중 전달하는 정보
200번대: 요청 처리 성공
300번대: 다른 주소나 리소스로 이동
400번대: 클라이언트 요청 문제
500번대: 서버 내부 문제

백엔드 API를 개발할 때는 주로 다음 상태 코드를 자주 만난다.

200 OK
400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
500 Internal Server Error

Spring에서는 ResponseEntity를 사용해 응답 Body뿐만 아니라 상태 코드와 Header도 함께 설정할 수 있다.

return ResponseEntity.ok(response);

백엔드 개발자는 JSON 데이터만 반환하는 것이 아니라, 요청이 어떤 결과로 끝났는지도 상태 코드로 표현해야 한다.

응답 Body에 실패라고 쓰면 충분하지 않을까?

처음에는 다음과 같이 생각할 수 있다.

“응답 Body에 성공 여부를 넣어주면 상태 코드는 항상 200이어도 되지 않을까?”

예를 들어 서버가 다음과 같이 응답했다고 해보자.

HTTP/1.1 200 OK
Content-Type: application/json

{
  "success": false,
  "message": "사용자를 찾을 수 없습니다."
}

응답 Body만 보면 실패다. 하지만 HTTP 상태 코드는 성공을 의미하는 200 OK다.

이런 응답을 받으면 클라이언트는 결과를 판단하기 어려워진다.

HTTP 요청은 성공한 것인가?
사용자 조회는 실패한 것인가?
에러 화면을 보여줘야 하는가?
정상 응답으로 처리해야 하는가?

물론 프로젝트마다 공통 응답 형식이나 별도의 정책을 사용할 수 있다. 하지만 HTTP 상태 코드는 이미 요청 결과를 표현하기 위해 만들어진 표준이다.

따라서 특별한 이유가 없다면 상태 코드와 실제 처리 결과를 맞춰주는 것이 좋다.

사용자를 찾지 못했다면 다음과 같이 응답할 수 있다.

HTTP/1.1 404 Not Found
Content-Type: application/json

{
  "code": "USER_NOT_FOUND",
  "message": "사용자를 찾을 수 없습니다."
}

상태 코드만 보고도 요청이 실패했다는 것을 알 수 있고, Body를 통해 실패한 이유까지 확인할 수 있다.

200 OK

200 OK는 요청이 정상적으로 처리되었다는 뜻이다.

사용자 조회 API를 호출했다고 해보자.

GET /users/1

서버가 사용자를 정상적으로 조회했다면 다음과 같이 응답할 수 있다.

HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 1,
  "name": "junior"
}

Spring Boot에서는 다음과 같이 반환할 수 있다.

@GetMapping("/users/{id}")
public ResponseEntity<UserResponse> getUser(@PathVariable Long id) {
    UserResponse response = userService.getUser(id);
    return ResponseEntity.ok(response);
}

200 OK는 가장 자주 사용하는 성공 응답이지만, 모든 성공 응답을 200으로 처리해야 하는 것은 아니다.

새로운 리소스를 생성했다면 201 Created, 응답 Body 없이 성공 여부만 전달한다면 204 No Content를 사용할 수 있다.

200 OK
요청을 정상적으로 처리했고 응답 데이터가 있음

201 Created
새로운 리소스를 생성함

204 No Content
요청은 성공했지만 반환할 데이터가 없음

400 Bad Request

400 Bad Request는 클라이언트가 보낸 요청의 값이나 형식에 문제가 있을 때 사용한다.

회원가입 요청에서 이메일 형식이 잘못됐다고 해보자.

POST /users
Content-Type: application/json

{
  "name": "junior",
  "email": "not-email"
}

서버는 다음과 같이 응답할 수 있다.

HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "code": "INVALID_EMAIL",
  "message": "이메일 형식이 올바르지 않습니다."
}

Spring Boot에서는 @Valid를 사용해 요청 값을 검증하고, 검증에 실패하면 전역 예외 처리에서 400 Bad Request로 변환하는 방식을 많이 사용한다.

@PostMapping("/users")
public ResponseEntity<UserResponse> createUser(
        @Valid @RequestBody UserCreateRequest request
) {
    UserResponse response = userService.createUser(request);

    return ResponseEntity
            .status(HttpStatus.CREATED)
            .body(response);
}

다음과 같은 경우 400을 고려할 수 있다.

이메일 형식이 잘못됨
필수 값이 빠짐
숫자가 들어와야 할 곳에 문자가 들어옴
페이지 번호가 음수로 전달됨
요청 JSON 형식이 올바르지 않음

이런 문제는 서버가 고장 난 것이 아니라, 클라이언트가 올바른 요청을 보내지 않은 경우다.

401 Unauthorized

401 Unauthorized는 이름만 보면 권한이 없다는 뜻처럼 보이지만, 실제로는 유효한 인증 정보가 필요하다는 의미에 가깝다.

쉽게 말하면 서버가 다음과 같이 말하는 상황이다.

“먼저 누구인지 확인해야 합니다.”

로그인하지 않은 사용자가 내 정보 API를 호출했다고 해보자.

GET /me

서버는 다음과 같이 응답할 수 있다.

HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
  "code": "AUTHENTICATION_REQUIRED",
  "message": "로그인이 필요합니다."
}

JWT나 Session 기반 인증에서는 다음과 같은 경우 401을 사용할 수 있다.

인증 토큰이 없음
토큰이 만료됨
토큰 형식이 잘못됨
서명이 올바르지 않은 토큰을 전달함
로그인 세션이 만료됨

즉, 서버가 현재 요청을 보낸 사용자를 신뢰할 수 없는 상태다.

403 Forbidden

403 Forbidden은 사용자가 누구인지는 확인됐지만, 해당 요청을 수행할 권한이 없을 때 사용한다.

일반 사용자가 관리자 전용 API를 호출했다고 해보자.

DELETE /admin/users/1
Authorization: Bearer valid-user-token

토큰은 정상적이기 때문에 서버는 사용자가 누구인지 알고 있다. 하지만 해당 사용자는 관리자가 아니다.

HTTP/1.1 403 Forbidden
Content-Type: application/json

{
  "code": "ACCESS_DENIED",
  "message": "이 기능을 사용할 권한이 없습니다."
}

401과 403은 다음처럼 구분하면 이해하기 쉽다.

401 Unauthorized
누구인지 확인되지 않음

403 Forbidden
누구인지는 확인됐지만 권한이 없음

조금 더 쉽게 표현하면 다음과 같다.

401: 당신이 누구인지 확인할 수 없습니다.
403: 당신이 누구인지는 알지만, 이 요청은 허용할 수 없습니다.

404 Not Found

404 Not Found는 요청한 리소스를 찾을 수 없을 때 사용한다.

존재하지 않는 사용자 ID를 조회했다고 해보자.

GET /users/999

서버에 999번 사용자가 없다면 다음과 같이 응답할 수 있다.

HTTP/1.1 404 Not Found
Content-Type: application/json

{
  "code": "USER_NOT_FOUND",
  "message": "사용자를 찾을 수 없습니다."
}

404는 URL 자체가 존재하지 않을 때만 사용하는 코드가 아니다.

API 주소는 올바르지만, 해당 API에서 요청한 데이터를 찾지 못한 경우에도 사용할 수 있다.

GET /users/999

/users/{id} API는 존재함
하지만 ID가 999인 사용자는 존재하지 않음
따라서 404 Not Found 응답

다만 목록 조회 결과가 비어 있는 경우에는 상황이 조금 다르다.

GET /users?name=unknown

검색 결과가 없더라도 API 요청 자체는 정상적으로 처리됐기 때문에 빈 배열과 함께 200 OK를 반환하는 경우가 많다.

HTTP/1.1 200 OK
Content-Type: application/json

[]

단일 리소스를 찾지 못한 것과 검색 결과가 비어 있는 것은 구분해서 생각할 필요가 있다.

500 Internal Server Error

500 Internal Server Error는 서버가 요청을 처리하는 도중 예상하지 못한 문제가 발생했을 때 사용한다.

예를 들어 코드에서 처리하지 못한 예외가 발생했다고 해보자.

String value = null;
value.length();

이 예외를 적절하게 처리하지 못하면 서버는 500 응답을 반환할 수 있다.

HTTP/1.1 500 Internal Server Error
Content-Type: application/json

{
  "code": "INTERNAL_SERVER_ERROR",
  "message": "서버 내부 오류가 발생했습니다."
}

500은 클라이언트가 요청을 잘못 보냈다는 의미가 아니다. 서버가 정상적인 응답을 만들지 못했다는 의미다.

500이 발생했다면 백엔드 개발자는 서버 로그를 확인해야 한다.

어떤 API에서 발생했는가?
어떤 요청 값이 전달되었는가?
어떤 예외가 발생했는가?
DB 연결에 실패했는가?
외부 API 호출이 실패했는가?
최근 배포된 코드와 관련이 있는가?

여기서 주의할 점이 있다.

실제 예외 메시지나 Stack Trace를 응답 Body에 그대로 담아서는 안 된다.

{
  "message": "NullPointerException at UserService.java:42 ..."
}

이런 정보에는 내부 클래스 구조, 패키지 이름, 데이터베이스 정보처럼 외부에 노출하면 안 되는 내용이 포함될 수 있다.

클라이언트에는 일반적인 오류 메시지만 전달하고, 자세한 내용은 서버 로그에 남기는 것이 좋다.

자주 헷갈리는 상황

모든 실패를 500으로 처리하면 안 된다

다음과 같은 요청은 서버 내부 오류가 아니다.

필수 입력값이 없음
이메일 형식이 잘못됨
존재하지 않는 사용자를 조회함
로그인하지 않고 인증 API를 호출함
일반 사용자가 관리자 기능을 호출함

상황에 따라 각각 400, 404, 401, 403으로 구분할 수 있다.

잘못된 요청 값: 400
인증 정보 없음: 401
권한 없음: 403
대상 데이터 없음: 404
예상하지 못한 서버 오류: 500

없는 데이터와 숨겨야 하는 데이터는 다를 수 있다

권한이 없는 사용자에게 403 Forbidden을 반환하면 해당 리소스가 존재한다는 사실을 알려줄 수 있다.

예를 들어 다음 주소가 있다고 해보자.

GET /private-documents/100

권한이 없는 요청에 403을 반환하면 사용자는 100번 문서가 실제로 존재한다고 추측할 수 있다.

이런 정보까지 숨겨야 하는 서비스에서는 리소스가 존재하더라도 404를 반환하는 전략을 사용하기도 한다.

다만 이것은 모든 API에 무조건 적용하는 규칙은 아니다. 서비스의 보안 정책과 팀의 API 설계 기준에 따라 결정해야 한다.

에러 응답 형식은 일관되게 유지하는 것이 좋다

에러마다 응답 형식이 달라지면 클라이언트에서 처리하기 어려워진다.

{
  "errorMessage": "사용자가 없습니다."
}
{
  "result": false,
  "reason": "권한이 없습니다."
}
{
  "code": 1004,
  "msg": "잘못된 요청입니다."
}

각 API가 서로 다른 형태로 응답하면 프론트엔드는 에러마다 별도의 처리 코드를 작성해야 한다.

따라서 프로젝트에서는 공통된 에러 응답 구조를 정해두는 것이 좋다.

{
  "code": "USER_NOT_FOUND",
  "message": "사용자를 찾을 수 없습니다."
}

필요하다면 요청을 추적하기 위한 값이나 상세 오류 목록을 추가할 수도 있다.

{
  "code": "VALIDATION_FAILED",
  "message": "입력값을 확인해주세요.",
  "errors": [
    {
      "field": "email",
      "message": "이메일 형식이 올바르지 않습니다."
    }
  ]
}

정리

HTTP 상태 코드는 서버가 클라이언트에게 이번 요청이 어떤 결과로 끝났는지를 알려주는 값이다.

자주 사용하는 상태 코드는 다음과 같다.

200 OK
요청을 정상적으로 처리함

400 Bad Request
요청 값이나 형식이 잘못됨

401 Unauthorized
로그인이 필요하거나 인증 정보가 유효하지 않음

403 Forbidden
인증은 됐지만 요청을 수행할 권한이 없음

404 Not Found
요청한 리소스를 찾을 수 없음

500 Internal Server Error
서버 내부에서 예상하지 못한 문제가 발생함

상태 코드는 요청 결과를 크게 구분하고, 응답 Body는 그 결과에 대한 자세한 내용을 전달한다.

Status Code
이번 요청이 성공했는지 실패했는지 알려준다.

Response Body
성공한 데이터나 실패한 이유를 자세히 알려준다.

API를 만들 때 응답 JSON만 설계해서는 부족하다.

상태 코드까지 함께 설계해야 클라이언트가 정상 처리, 잘못된 요청, 로그인 필요, 권한 부족, 데이터 없음, 서버 장애를 정확하게 구분할 수 있다.

profile
주니어개발자

0개의 댓글