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

2025. 6. 18. 20:58Framework/Spring

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 인증 흐름 완전 정복 -> 로그인 인증부터 토큰 발급, 필터 구조, 실무 적용까지 알아보고자 한다.