IllegalArgumentException은 400 Bad Request인가?

12 hours ago 2

들어가며

API 개발을 하다 보면 예외가 발생할 때 어떤 HTTP Status Code를 응답해야 할지 고민하게 됩니다. 스프링 프레임워크를 사용하면 @ExceptionHandler와 @ResponseStatus를 이용해 예외 클래스의 응답을 매핑할 수 있죠.

@RestControllerAdvice public class GlobalDefaultExceptionHandler { @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(IllegalArgumentException.class) public ErrorResponse onException(IllegalArgumentException exception) { ... } }

혹시 IllegalArgumentException을 400 Bad Request로 매핑하시나요? 이 예외는 주로 잘못된 인수로 발생하기 때문에, 많은 분들이 400 응답과 자연스럽게 연결 짓곤 합니다. 그래서 비즈니스 로직을 작성할 때, 클라이언트 요청에 문제가 있다면 명시적으로 IllegalArgumentException을 발생시키고, 이를 400 Bad Request로 매핑하는 것을 실무에서 자주 볼 수 있습니다. 그런데 과연 이 예외는 항상 클라이언트의 잘못으로만 발생할까요? 그렇지 않습니다. 때로는 개발자의 실수나 내부 로직의 결함으로 IllegalArgumentException이 발생하기도 합니다.

이번 글에서는 4xx5xx의 차이를 명확히 짚어보고, IllegalArgumentException과 같은 범용적인 예외 클래스를 400으로 매핑하는 것이 왜 위험할 수 있는지, 그리고 그에 대한 대안은 무엇인지 살펴보겠습니다.

4xx와 5xx의 차이는 무엇이고, 왜 구분해야 하는가?

[HTTP 상태 코드 분류]

HTTP 오류 코드의 이해

  • 4xx(Client Error)

    • 클라이언트가 잘못된 요청을 보냈음을 의미합니다.
    • 일반적으로 요청을 수정하지 않으면 계속 실패하므로, 자동 재시도(backoff) 전략이 무의미할 수 있습니다.
    • 올바르게 요청을 수정한 뒤 재시도하면 정상 처리될 수 있습니다.
    • 대표적인 코드는 400 Bad Request, 401 Unauthorized, 404 Not Found 등이 있습니다.
  • 5xx(Server Error)

    • 서버 내부 문제로 요청을 처리하지 못했음을 나타냅니다.
      • 예) 내부 로직 오류, DB 연결 실패, API 연동 실패, 예기치 않은 서버 오류 등.
    • 일시적 장애일 수 있으므로 클라이언트는 일정 시간 간격으로 재시도(backoff) 전략을 세울 수 있습니다.
    • 대표적인 코드는 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable 등이 있습니다.

결국 “4xx인가, 5xx인가”는 오류 책임이 클라이언트냐 서버냐를 구분하는 기준입니다.

운영·모니터링 관점에서의 중요성

서버 운영 측면에서 4xx5xx를 명확하게 구분하는 것은 매우 중요합니다.

  • 5xx → 즉각 대응

    • 5xx 응답은 서버 내부 문제 신호로 간주되어야 합니다.
    • 모니터링·알림 시스템에서 즉시 감지하여 빠르게 원인 파악 및 조치할 수 있어야 합니다.
  • 4xx 오탐 방지

    • 서버 오류를 4xx로 잘못 처리하면, 운영팀은 클라이언트 문제로 오인해 신속 대응이 어려워집니다.
    • 반대로, 클라이언트 오류를 5xx로 처리하면 불필요한 경보가 증가해 운영팀의 피로도가 높아지고 무감각해져, 진짜 서버 문제를 놓칠 위험이 있습니다.

이처럼 4xx5xx를 명확히 구분하여 응답 코드 전략을 설계해야, 효율적인 장애 대응과 운영 안정성을 모두 확보할 수 있습니다.
이제 실무에서 자주 접하는 IllegalArgumentException과 같은 예외를 어떻게 매핑하는 것이 좋을지 살펴보겠습니다.

IllegalArgumentException을 400 응답으로 매핑하는 것이 위험한 이유

IllegalArgumentException은 Java 표준 라이브러리뿐 아니라 스프링·하이버네이트 같은 프레임워크, 여러 라이브러리에서 흔히 발생합니다. 통상 유효하지 않은 인자가 들어왔을 때 발생하기 때문에 400 Bad Request 응답이 적절한 것으로 보이기도 합니다. 그렇다면 스프링 프레임워크에서 기본 정책으로 이 예외에 대해 400 Bad Request 응답을 매핑하는 것도 가능했을 것 같은데, 왜 그러지 않았을까요? 그 이유는 IllegalArgumentException이 클라이언트의 입력으로 인한 것인지 서버 내부 로직에 의한 것인지 알 수 없기 때문입니다.

예를 들어, 아래와 같은 코드가 실행되는 API가 있다고 가정해 보겠습니다.

// 서버 내부 로직 오류 예시: 클라이언트와 무관하게 IllegalArgumentException 발생 public void execute(Runnable task, PriorityResolver priorityResolver) { Thread thread = new Thread(task); int calculatedPriority = priorityResolver.get(); thread.setPriority(calculatedPriority); thread.start(); }

Thread.setPriority(..) 메서드는 1부터 10까지의 값만 허용하며, 이 범위를 벗어나면 IllegalArgumentException이 발생합니다. 서버 개발자가 이를 인지하지 못하고 PriorityResolver를 개발하거나 수정했다면 클라이언트의 잘못이 아닌 문제로 IllegalArgumentException이 발생할 수 있게 됩니다.

이런 상황에서 만약 IllegalArgumentException을 400 Bad Request로 매핑했다면, 해당 예외는 클라이언트의 잘못으로 오인되어 문제 파악이 늦어질 수 있습니다. 반대로, 커스텀 예외 클래스만을 400 Bad Request로 매핑하고, 클라이언트의 잘못이 명확한 경우에만 사용했다면, 500 Internal Server Error가 발생하여 서버 내부 문제로 인식되고 보다 빠르게 조치할 수 있었을 것입니다.

적절한 매핑을 위해 고려해야 할 사항

클라이언트 요청이 잘못된 경우가 명확하면서, 이를 사전에 예측해 처리할 수 있다면 4xx 응답이 적절합니다. 반면, 예상치 못한 예외서버 내부의 로직 오류로 처리가 중단된 경우는 5xx 응답이 적절합니다.

  1. 예상 못 한 상황은 5xx, 명백한 클라이언트 잘못은 4xx

    • 스프링 프레임워크는 예상 못 한(별도의 매핑이 없는) 예외가 발생할 경우, 500 Internal Server Error를 응답합니다.
    • 문제를 조치하며 원인이 클라이언트의 잘못으로 판명되면, 4xx를 응답하도록 수정하는 것이 좋습니다.
  2. 비즈니스 예외 vs. 시스템 예외

    • 비즈니스 예외: "최소 주문 금액 미달"과 같이 클라이언트의 입력이나 조건이 맞지 않을 때는 400 Bad Request 응답이 적절합니다.
    • 시스템 예외: DB 연결 실패, API 연동 실패,NullPointerException, 라이브러리 내부 오류 등이 발생하면 5xx 응답이 적절합니다.
    • 예측 가능한 시스템 예외: 대체 응답(Fallback Response)을 제공하여 5xx 응답을 회피하고 모니터링할 수 있도록 설계하는 것이 좋습니다.
  3. 커스텀 예외를 정의하여 클라이언트의 잘못을 명확히 전달

    • IllegalArgumentException이나 IllegalStateException 같은 범용 예외 클래스 대신, BusinessException, FieldNameDuplicatedException과 같은 커스텀 예외를 정의하고 400 Bad Request 응답을 매핑합니다.
    • 이처럼 정의된 커스텀 예외는 비즈니스 로직에서 클라이언트의 잘못이 명확할 때 발생시킵니다.

커스텀 예외 작성 예시

비즈니스 예외 상황을 표현하기 위해 아래와 같이 예외 클래스를 정의해 볼 수 있습니다.

@Getter public class BusinessException extends RuntimeException { // 클라이언트 개발자가 예외 상황을 식별할 수 있는 값 private final String code; // 클라이언트 개발자가 예외 상황을 이해하는데 도움이 되는 정보 private final Map<String, Object> arguments; public BusinessException(String code) { this(code, null); } public BusinessException(String code, String message) { super(message); this.code = code; this.arguments = new HashMap<>(); } protected void addArgument(String key, Object value) { arguments.put(key, value); } }

더 구체적인 예외 클래스를 정의하고 싶다면 이를 상속할 수 있습니다.

public class InvalidExpressionException extends BusinessException { public InvalidExpressionException(int position, String hint) { super("expression.Invalid", "표현식이 올바르지 않습니다."); addArgument("position", position); addArgument("hint", hint); } }

어디서든 BusinessException이나 그 하위 예외가 발생하면 400 Bad Request를 응답하도록 매핑합니다.

@RestControllerAdvice public class GlobalDefaultExceptionHandler { @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(BusinessException.class) public ErrorResponse onException(BusinessException exception) { ... } }

요약 및 결론

  • 4xx(Client Error): 클라이언트의 잘못된 요청일 때 사용합니다.
  • 5xx(Server Error): 서버 내부 문제로 요청을 처리하지 못할 때 사용합니다.
  • 서버 문제를 4xx로 잘못 응답하면 "클라이언트 잘못"으로 오인하여 빠른 대응이 어려워질 수 있습니다.
  • IllegalArgumentException은 서버 버그로도 발생할 수 있으므로, 이를 무조건 400 Bad Request로 매핑하면 서버 문제를 뒤늦게 발견할 위험이 있습니다.
  • 400 응답이 필요한 상황에는 BusinessException 같은 커스텀 예외를 정의하여 명확하게 매핑하는 것이 좋습니다.

상태 코드를 명확하게 매핑하면 오류에 빠르게 대응할 수 있고, API 유지보수도 장기적으로 간소화할 수 있습니다. 이 글이 4xx/5xx 구분예외 처리 전략 수립에 실무적 도움을 드렸길 바랍니다.

Read Entire Article