티스토리 뷰

IT/Spring

⚠️Spring 예외 처리 완전 정복 - @ControllerAdvice, @ExceptionHandler, 그리고 실무 예외 전략

Stv 2025. 6. 18. 20:58

목차


    1. 예외 처리의 필요성

    소프트웨어는 실패하지 않는 것이 아니라, 실패를 어떻게 다루느냐가 중요하다.
    Spring 애플리케이션에서는 예외를 제대로 처리하지 않으면
    • 클라이언트에게 500 에러가 그대로 노출되거나
    • 사용자 친화적인 메시지를 전달하지 못하고
    • 서비스 전체가 중단될 수 있음

    -> 그래서 예외 처리를 계층화하고 구조화하는 전략이 필요함 


    2. 예외 처리 전략의 계층화

    계층 처리 방식
    Controller @ExceptionHandler, @ControllerAdivce
    Service 커스텀 예외 발생 (throw new..)
    Global 필터/인터셉터에서의 처리, 예외 로깅

    🎯3. Spring MVC에서 예외가 발생했을 때 흐름

    1. 컨트롤러/서비스에서 예외 발생
    2. HandlerExceptionResolver가 예외를 가로채 처리
    3. 등록된 @ExceptionHandler, @ControllerAdivce 우선 탐색
    4. 적젉한 HTTP 상태코드 + 메시지 변환

    4. 핵심 어노테이션 : @ExceptionHandler

    ✅ 특정 예외 처리 전용 메서드 정의

    @RestController
    public class UserController {
    
        @GetMapping("/users/{id}")
        public User getUser(@PathVariable Long id) {
            return userService.findById(id); // NotFoundException 발생 가능
        }
    
        @ExceptionHandler(NotFoundException.class)
        public ResponseEntity<String> handleNotFound(NotFoundException e) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
        }
    }
    • 컨트롤러 내에서 발생한 예외 중 NotFoundException이 잡힘
    • 응답 코드와 메시지를 커스터마이징 가능

    5. 전역 예외 처리기 : @ControllerAdivce

    ✅ 모든 컨트롤러의 예외를 한 곳에서 처리

    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler(NotFoundException.class)
        public ResponseEntity<ErrorResponse> handleNotFound(NotFoundException e) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND)
                    .body(new ErrorResponse("NOT_FOUND", e.getMessage()));
        }
    
        @ExceptionHandler(Exception.class)
        public ResponseEntity<ErrorResponse> handleGlobal(Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(new ErrorResponse("INTERNAL_ERROR", "서버 오류 발생"));
        }
    }
    • @RestControllerAdvice = @ControllerAdvice + @Responsebody
    • 공통 예외 응답 구조 ErrorResponse 설계로 일관된 반환 가능

    6. 커스텀 예외 설계

    public class NotFoundException extends RuntimeException {
        public NotFoundException(String message) {
            super(message);
        }
    }
    • 비즈니스 규칙에 따라 예외를 세분화하면 가독성과 유지보수성 향상

    7. 실무에서 사용하는 공통 에러 응답 DTO

    public class ErrorResponse {
        private String code;
        private String message;
        private LocalDateTime timestamp;
    
        public ErrorResponse(String code, String message) {
            this.code = code;
            this.message = message;
            this.timestamp = LocalDateTime.now();
        }
    }
    • 클라이언트에 일관된 응답 제공
    • Swagger/OpenAPI 문서화에도 유리

    8. 실무 트러블슈팅 팁

    문제 원인 해결책
    500 Internal Server Error만 뜸 예외 잡히지 않음 @ExceptionHandler(Exception.class) 추가
    커스텀 예외인데 200 OK 전달됨 ResponseEntity 없이 return 항상 ResponseEntity<> 사용
    예외 메시지가 노출됨 디버그용 메시지 그대로 노출 ErrorResponse에 사용자 메시지 구분
    Validation 실패 시 JSON 응답 없음 MethodArgumentNotValidException 처리 누락 @Valid 예외 처리 추가 필요

    9. @Valid 유효성 검사와 연계

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException e) {
        String errorMsg = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
        return ResponseEntity.badRequest().body(new ErrorResponse("VALIDATION_ERROR", errorMsg));
    }
    • @Valid + BindingResult 없이 바로 예외 잡는 방식

    ✅ 마무리 요약

    항목 핵심 정리
    처리 방식 @ExceptionHandler, @ControllerAdvice 활용
    설계 원칙 커스텀 예외 + 공통 응답 DTO 구성
    실무 전략 계층화된 예외 분리 + 글로벌 핸들러 구성

    🔜 다음 포스팅에서는 Spring Security & JWT 인증 흐름 완전 정복 -> 로그인 인증부터 토큰 발급, 필터 구조, 실무 적용까지 알아보고자 한다.