1. 라이브러리
  2. HTTP와 웹
  3. HTTP 상태 코드

업데이트됨 1개월 전

400 Bad Request 오류는 서버가 건네는 말과 같습니다. "듣고 있어요, 요청을 처리하고 싶어요, 하지만 무슨 말인지 이해할 수가 없어요."

5xx 오류(서버 자체의 문제)나 401/403(권한 없음)과 달리, 400은 대화가 시작되기도 전에 실패했다는 뜻입니다. 서버는 요청을 받아 이해하려 했지만 끝내 실패했습니다. 수정은 언제나 클라이언트 측에서 이루어져야 합니다.

400이 발생하는 원인

요청 자체가 잘못되었습니다. 서버는 무엇을 요청하는지조차 파악하지 못합니다.

잘못된 형식의 JSON—가장 흔한 원인:

POST /api/users HTTP/1.1
Content-Type: application/json

{
    "name": "Alice"
    "email": "alice@example.com"
}

"Alice" 뒤에 쉼표가 빠지면 서버는 데이터 대신 알 수 없는 값을 받게 됩니다. 값이 잘못된 게 아니라, 서버가 값 자체를 볼 수 없는 상태입니다.

필수 필드 누락:

POST /api/users HTTP/1.1
Content-Type: application/json

{"name": "Alice"}

API가 이메일 필드를 필요로 하는데 보내지 않으면 서버는 처리를 진행할 수 없습니다. 양식을 작성하다가 서명란을 비워둔 것과 같습니다.

잘못된 타입:

{"age": "twenty-five"}

서버가 숫자를 기대하는데 문자열을 받으면, 의도를 안전하게 추측할 수 없습니다. 25를 뜻한 건지, 20을 뜻한 건지—서버는 함부로 가정하지 않습니다.

유효하지 않은 값:

{"email": "not-an-email"}

구조는 맞지만 내용이 유효성 검사를 통과하지 못합니다. 이메일 주소가 아닌 값이 들어온 이메일 필드는 서버가 처리할 수 없는 요청입니다.

400과 다른 오류 코드의 차이

적절한 오류 코드를 선택하면 클라이언트가 무엇이 잘못됐는지, 어떻게 대처해야 하는지 파악할 수 있습니다.

400 대 401: 요청 구조가 잘못된 경우 400을 사용합니다. 요청은 올바르지만 사용자가 인증되지 않은 경우 401을 사용합니다. "JSON 형식이 잘못되었습니다"는 400입니다. "로그인이 필요합니다"는 401입니다.

400 대 403: 잘못된 형식의 요청에는 400을 사용합니다. 요청은 유효하지만 사용자에게 권한이 없을 때는 403을 사용합니다. "이메일 형식이 올바르지 않습니다"는 400입니다. "다른 사용자의 계정은 삭제할 수 없습니다"는 403입니다.

400 대 404: 요청 자체가 잘못된 경우 400을 사용합니다. 요청은 유효하지만 리소스가 존재하지 않을 때는 404를 사용합니다. "유효하지 않은 사용자 ID 형식"은 400입니다. "사용자 12345가 존재하지 않습니다"는 404입니다.

400 대 422: 이 둘의 구분은 미묘하지만 중요합니다. 400은 "요청을 파싱할 수 없다"는 뜻으로—구문 오류, 잘못된 형식의 JSON, 누락된 헤더가 해당됩니다. 422는 "요청은 이해했지만 의미상 처리할 수 없다"는 뜻으로—이미 존재하는 이메일로 사용자를 생성하려는 경우처럼 논리적으로 불가능한 상황을 말합니다. 형식은 맞지만 내용이 맞지 않는 경우입니다.

실제로는 많은 API가 두 경우 모두에 400을 사용합니다. 그러나 정확히 구분하자면: 400은 해석 자체가 불가능한 요청이고, 422는 논리적 모순이 있는 요청입니다.

유용한 오류 응답 작성법

{"error": "Bad Request"} 한 줄은 아무 도움이 되지 않습니다. 400의 핵심은 클라이언트가 요청을 수정할 수 있도록 돕는 것입니다.

무엇이 잘못됐는지 알려주기:

{
    "error": "Validation failed",
    "details": [
        {"field": "email", "message": "Invalid email format"},
        {"field": "age", "message": "Must be between 0 and 150"}
    ]
}

어떻게 수정하는지 알려주기:

{
    "error": "Invalid date format",
    "expected": "YYYY-MM-DD",
    "received": "10/21/2024",
    "example": "2024-10-21"
}

기계가 읽을 수 있는 코드 포함하기:

{
    "error": "Validation failed",
    "code": "VALIDATION_ERROR",
    "details": [
        {"field": "email", "code": "INVALID_FORMAT"}
    ]
}

오류 코드를 사용하면 클라이언트가 특정 상황을 프로그램 코드로 처리할 수 있습니다. 사람이 읽는 메시지는 디버깅하는 개발자를 위한 것이고, 코드는 애플리케이션 로직을 위한 것입니다.

서버 구현

유효성 검사는 일찍 하세요. 요청이 도착하는 즉시, 다른 작업을 하기 전에 유효한지 먼저 확인하세요:

app.post('/api/users', function(request, response) {
    const errors = [];
    
    if(!request.body.email) {
        errors.push({field: 'email', message: 'Email is required'});
    }
    else if(!isValidEmail(request.body.email)) {
        errors.push({field: 'email', message: 'Invalid email format'});
    }
    
    if(errors.length > 0) {
        return response.status(400).json({
            error: 'Validation failed',
            details: errors
        });
    }
    
    // 유효한 데이터가 있을 때만 여기에 도달합니다
    createUser(request.body);
});

유효성 검사를 직접 작성하는 대신 라이브러리를 활용하세요:

const Joi = require('joi');

const userSchema = Joi.object({
    name: Joi.string().required(),
    email: Joi.string().email().required(),
    age: Joi.number().min(0).max(150)
});

app.post('/api/users', function(request, response) {
    const { error, value } = userSchema.validate(request.body);
    
    if(error) {
        return response.status(400).json({
            error: 'Validation failed',
            details: error.details.map(function(d) {
                return {field: d.path.join('.'), message: d.message};
            })
        });
    }
    
    // value에는 검증되고 정제된 데이터가 들어 있습니다
});

클라이언트 처리

400 오류에서 가장 중요한 점: 자동으로 재시도하지 마세요. 503(서버 일시 과부하)과 달리, 400은 두 번째 시도라고 해서 저절로 해결되지 않습니다. 요청 자체가 잘못된 것입니다. 누군가 수정하기 전까지는 계속 실패합니다.

async function createUser(userData) {
    const response = await fetch('/api/users', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify(userData)
    });
    
    if(response.status === 400) {
        const error = await response.json();
        
        // 사용자에게 무엇이 잘못됐는지 보여주기
        if(error.details) {
            error.details.forEach(function(detail) {
                showFieldError(detail.field, detail.message);
            });
        }
        
        // 재시도하지 말고, 사용자가 폼을 수정하도록 하기
        throw new ValidationError(error);
    }
    
    return response.json();
}

보안 고려 사항

오류 메시지는 일반 사용자가 요청을 수정하는 데 도움이 되어야 하되, 공격자가 시스템을 탐색하는 데 활용되어서는 안 됩니다. "이메일이 이미 존재합니다"는 괜찮습니다. "users.email 유니크 인덱스에서 데이터베이스 제약 조건 위반"은 내부 인프라 정보를 외부에 드러냅니다.

400 Bad Request에 대해 자주 묻는 질문

유효성 검사 오류에 400을 사용해야 할까요, 422를 사용해야 할까요?

둘 다 합리적인 선택입니다. 엄밀히 말하면, 400은 구문적으로 잘못된 요청(잘못된 JSON, 누락된 헤더)에 사용하고, 422는 잘 구성되었지만 비즈니스 로직 유효성 검사에 실패한 요청(중복 이메일, 잘못된 날짜 범위)에 사용합니다. 많은 API는 단순화를 위해 모든 유효성 검사 오류에 400을 사용합니다. 하나의 방식을 정하고 일관성 있게 적용하세요.

JSON이 올바르게 보이는데 왜 API가 400을 반환할까요?

보이지 않는 문자가 원인인 경우가 많습니다. 서식 있는 텍스트 편집기에서 복사-붙여넣기, 특정 유니코드 공백 문자, 또는 BOM(바이트 순서 표시)이 겉으로 드러나지 않게 JSON을 손상시킬 수 있습니다. JSON을 직접 다시 입력하거나, 숨겨진 문자를 표시해 주는 JSON 유효성 검사 도구를 사용해 보세요.

400 응답에 전송된 잘못된 데이터를 포함해야 할까요?

디버깅 목적으로는 포함하는 것이 좋습니다—문제가 있는 값을 그대로 보여주면 개발자가 상황을 파악하는 데 도움이 됩니다. 단, 민감한 데이터에는 주의하세요. 오류 응답에 비밀번호, 토큰, 개인 정보를 다시 포함하지 마세요.

400 오류 메시지는 얼마나 자세해야 할까요?

개발자가 추측 없이 문제를 해결할 수 있을 만큼 자세해야 합니다. "유효하지 않은 요청"은 아무 정보도 주지 않습니다. "'email' 필드는 유효한 이메일 주소여야 하는데 'not-an-email'을 받았습니다"는 바로 조치를 취할 수 있는 정보입니다. 목표는 디버깅 과정에서의 불필요한 왕복을 줄이는 것입니다.

이 페이지가 도움이 되었나요?

😔
🤨
😃