ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Framework 시리즈 15화 – 스프링 MVC의 예외 처리 전략과 @ExceptionHandler 사용법
    기술과 산업/언어 및 프레임워크 2025. 7. 18. 12:21
    728x90

    Spring MVC의 예외 처리 흐름과 함께 @ExceptionHandler, @ControllerAdvice, ResponseEntityExceptionHandler를 실제 사례와 함께 정리합니다. 실무에서 API 오류 응답을 체계적으로 처리하는 전략을 배웁니다.

     

     

    예외는 반드시 발생한다 – 그래서 설계가 중요하다

     

    API를 만들다 보면 잘못된 요청, 인증 실패, 리소스 없음 등 다양한 예외가 발생합니다.

    문제는 예외가 발생했을 때 클라이언트에게 어떻게 응답할지 전략이 없으면 다음과 같은 일이 생긴다는 거죠.

     

    • 500 Internal Server Error가 그대로 노출됨
    • HTML 에러 페이지가 REST 응답에 노출됨
    • 오류 메시지에 스택트레이스가 그대로 출력됨

     

    이런 건 단순히 보기 안 좋을 뿐 아니라 보안 문제로도 이어질 수 있습니다.

     

    그래서 스프링은 예외 처리 방식을 세분화해서 제공하고 있고, 개발자는 상황에 맞게 정리해둘 필요가 있습니다.

     


     

    스프링 MVC 예외 처리 방식 – 크게 3단계로 나뉩니다

    처리 방법적용 대상특징

    @ExceptionHandler 개별 컨트롤러 특정 예외만 별도 처리
    @ControllerAdvice 전역 컨트롤러 전역 예외 처리에 적합
    ResponseEntityExceptionHandler 스프링 제공 추상 클래스 복잡한 구조에서 확장성 있게 처리 가능

     


     

    1. @ExceptionHandler – 가장 직관적인 방법

    @RestController
    public class HelloController {
    
        @GetMapping("/hello")
        public String hello(@RequestParam(required = false) String name) {
            if (name == null) {
                throw new IllegalArgumentException("이름은 필수입니다.");
            }
            return "Hello " + name;
        }
    
        @ExceptionHandler(IllegalArgumentException.class)
        public ResponseEntity<String> handleIllegalArgument(IllegalArgumentException e) {
            return ResponseEntity
                    .badRequest()
                    .body("요청 오류: " + e.getMessage());
        }
    }

     

    • 같은 클래스 내에서 발생한 예외만 처리 가능
    • 응답 형식도 직접 지정할 수 있어 REST 응답에 유용

     


     

    2. @ControllerAdvice – 전역 예외 처리

    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler(IllegalArgumentException.class)
        public ResponseEntity<String> handleIllegalArgument(IllegalArgumentException e) {
            return ResponseEntity
                    .badRequest()
                    .body("잘못된 요청입니다: " + e.getMessage());
        }
    
        @ExceptionHandler(Exception.class)
        public ResponseEntity<String> handleAll(Exception e) {
            return ResponseEntity
                    .status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body("서버 내부 오류가 발생했습니다.");
        }
    }

     

    • 모든 컨트롤러에서 발생한 예외를 한 곳에서 처리
    • 공통 에러 포맷이 필요한 경우 유용

     

    📌 @ControllerAdvice vs @RestControllerAdvice

    → 후자는 @ResponseBody가 기본 적용되어 JSON 응답용으로 주로 사용

     


     

    3. ResponseEntityExceptionHandler 확장 – 복잡한 구조에 유리

     

    Spring은 이미 ResponseEntityExceptionHandler라는 기본 클래스에서 몇 가지 예외를 처리하고 있습니다. 예를 들어:

     

    • MethodArgumentNotValidException
    • HttpRequestMethodNotSupportedException

     

    이걸 상속받아 내 스타일대로 확장할 수 있습니다.

    @ControllerAdvice
    public class ApiExceptionHandler extends ResponseEntityExceptionHandler {
    
        @Override
        protected ResponseEntity<Object> handleMethodArgumentNotValid(
                MethodArgumentNotValidException ex,
                HttpHeaders headers, HttpStatus status, WebRequest request) {
    
            String message = ex.getBindingResult()
                    .getFieldErrors()
                    .stream()
                    .map(e -> e.getField() + ": " + e.getDefaultMessage())
                    .collect(Collectors.joining(", "));
    
            return ResponseEntity
                    .status(HttpStatus.BAD_REQUEST)
                    .body("검증 실패: " + message);
        }
    }

     

    • Bean Validation 실패 시 자동 호출
    • 응답 포맷을 커스터마이징할 수 있음
    • 여러 컨트롤러에 걸쳐 적용 가능

     


     

    실무 팁 – 에러 응답 포맷 정형화하자

     

    많은 프로젝트에서는 아래와 같은 형태의 JSON 응답을 사용합니다.

    {
      "timestamp": "2025-07-18T12:34:56",
      "status": 400,
      "error": "Bad Request",
      "message": "필수 파라미터가 없습니다",
      "path": "/api/hello"
    }

    이런 포맷을 공통으로 만들려면 ErrorResponse 클래스를 만들어 @ControllerAdvice에서 일괄 반환하는 방식이 좋습니다.

    @Data
    @AllArgsConstructor
    public class ErrorResponse {
        private LocalDateTime timestamp;
        private int status;
        private String error;
        private String message;
        private String path;
    }
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleAll(Exception e, HttpServletRequest request) {
        ErrorResponse body = new ErrorResponse(
            LocalDateTime.now(),
            500,
            "Internal Server Error",
            e.getMessage(),
            request.getRequestURI()
        );
        return ResponseEntity.status(500).body(body);
    }

     


     

    예외도 전략이다

     

    • 예외는 언제나 발생합니다
    • 사용자에게 예외 상황을 명확히 알릴 수 있어야 합니다
    • 스택 트레이스를 그대로 노출하면 위험합니다
    • 전역 처리 → 포맷 통일 → 유지보수 편리

     

    예외를 잘 다루는 시스템은 결국 신뢰받는 시스템으로 이어집니다.

     

    728x90
Designed by Tistory.