Spring Interceptor, @ControllerAdvice, @ExceptionHandler

szlee·2025년 1월 12일

Spring Interceptor

  • HandlerInterceptor 인터페이스를 구현하여 컨트롤러의 요청 처리 전/후에 추가 로직을 실행할 수 있게 해주는 컴포넌트
public class CustomInterceptor implements HandlerInterceptor {
    // 컨트롤러 실행 전
    @Override
    public boolean preHandle(HttpServletRequest request, 
        HttpServletResponse response, Object handler) {
        // 인증, 로깅 등의 공통 작업 수행
        return true; // false 반환 시 요청 처리 중단
    }
    
    // 컨트롤러 실행 후, 뷰 렌더링 전
    @Override
    public void postHandle(HttpServletRequest request, 
        HttpServletResponse response, Object handler, ModelAndView modelAndView) {
        // 뷰에 추가적인 데이터를 담는 등의 작업
    }
    
    // 뷰 렌더링 후
    @Override
    public void afterCompletion(HttpServletRequest request, 
        HttpServletResponse response, Object handler, Exception ex) {
        // 리소스 해제 등의 작업
    }
}

@ControllerAdvice

  • 전역적으로 예외를 처리하고 공통 데이터를 바인딩하기 위한 어노테이션
  • 여러 컨트롤러에서 발생할 수 있는 예외를 한 곳에서 일관되게 처리할 수 있다.
@ControllerAdvice
public class GlobalExceptionHandler {
    // 모든 컨트롤러에서 사용할 공통 데이터
    @ModelAttribute
    public void addAttributes(Model model) {
        model.addAttribute("serverTime", new Date());
    }
}

@ExceptionHandler

  • 특정 예외가 발생했을 때 이를 처리하는 메서드를 지정하는 어노테이션
  • @ControllerAdvice와 함께 사용하면 전역적인 예외 처리가 가능
@ControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<ErrorResponse> handleRuntimeException(RuntimeException ex) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.INTERNAL_SERVER_ERROR.value(),
            ex.getMessage()
        );
        return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
    
    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleUserNotFoundException(UserNotFoundException ex) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.NOT_FOUND.value(),
            ex.getMessage()
        );
        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }
}
  • Interceptor: 로깅, 인증, 권한 체크, API 요청/응답 감시
  • ControllerAdvice: 전역 예외 처리, 공통 데이터 바인딩
  • ExceptionHandler: 비즈니스 예외 처리, HTTP 상태 코드 매핑, 일관된 에러 응답 형식 제공

실사용예시

로그인 체크를 위한 Interceptor

@Component
public class AuthInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession();
        String requestURI = request.getRequestURI();
        
        // 세션에서 로그인 정보 확인
        if (session.getAttribute("loginUser") == null) {
            // 로그인되지 않은 경우
            response.sendRedirect("/login?redirectURL=" + requestURI);
            return false;
        }
        return true;
    }
}

// Interceptor 등록
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    
    @Autowired
    private AuthInterceptor authInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor)
                .addPathPatterns("/**")                // 모든 경로에 적용
                .excludePathPatterns("/login", "/register", "/css/**", "/js/**");  // 제외할 경로
    }
}

전역 예외 처리와 응답 형식 표준화

// 표준 응답 형식
@Getter @Setter
public class ApiResponse<T> {
    private int status;
    private String message;
    private T data;
    
    public ApiResponse(int status, String message, T data) {
        this.status = status;
        this.message = message;
        this.data = data;
    }
}

// 커스텀 예외 클래스
public class CustomException extends RuntimeException {
    private final int errorCode;
    
    public CustomException(String message, int errorCode) {
        super(message);
        this.errorCode = errorCode;
    }
    
    public int getErrorCode() {
        return errorCode;
    }
}

// 전역 예외 처리
@ControllerAdvice
@RestController
public class GlobalExceptionHandler {
    
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    
    // 커스텀 예외 처리
    @ExceptionHandler(CustomException.class)
    public ResponseEntity<ApiResponse<Void>> handleCustomException(CustomException ex) {
        logger.error("Custom error occurred: {}", ex.getMessage());
        ApiResponse<Void> response = new ApiResponse<>(
            ex.getErrorCode(),
            ex.getMessage(),
            null
        );
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }
    
    // validation 예외 처리
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ApiResponse<Map<String, String>>> handleValidationExceptions(
            MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        
        ApiResponse<Map<String, String>> response = new ApiResponse<>(
            400,
            "Validation failed",
            errors
        );
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }
    
    // 기타 모든 예외 처리
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<Void>> handleAllExceptions(Exception ex) {
        logger.error("Unexpected error occurred: ", ex);
        ApiResponse<Void> response = new ApiResponse<>(
            500,
            "Internal server error",
            null
        );
        return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

컨트롤러와 실제 사용 예시

@Validated
@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @Autowired
    private UserService userService;
    
    @GetMapping("/{id}")
    public ResponseEntity<ApiResponse<UserDto>> getUser(@PathVariable Long id) {
        UserDto user = userService.getUser(id);
        if (user == null) {
            throw new CustomException("User not found", 404);
        }
        return ResponseEntity.ok(new ApiResponse<>(200, "Success", user));
    }
    
    @PostMapping
    public ResponseEntity<ApiResponse<UserDto>> createUser(@Valid @RequestBody UserDto userDto) {
        UserDto createdUser = userService.createUser(userDto);
        return ResponseEntity.ok(new ApiResponse<>(200, "User created successfully", createdUser));
    }
}

// DTO with validation
@Getter @Setter
public class UserDto {
    private Long id;
    
    @NotBlank(message = "Username is required")
    @Size(min = 4, max = 20, message = "Username must be between 4 and 20 characters")
    private String username;
    
    @Email(message = "Invalid email format")
    @NotBlank(message = "Email is required")
    private String email;
    
    @NotBlank(message = "Password is required")
    @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}$", 
             message = "Password must be minimum 8 characters, at least one letter and one number")
    private String password;
}

공통 데이터를 위한 ControllerAdvice 사용

@ControllerAdvice
public class GlobalControllerAdvice {
    
    @Autowired
    private UserService userService;
    
    // 모든 컨트롤러에서 공통으로 사용할 데이터
    @ModelAttribute("commonData")
    public Map<String, Object> commonData(HttpSession session) {
        Map<String, Object> data = new HashMap<>();
        
        // 현재 로그인한 사용자 정보
        Long userId = (Long) session.getAttribute("userId");
        if (userId != null) {
            UserDto user = userService.getUser(userId);
            data.put("currentUser", user);
        }
        
        // 사이트 공통 설정
        data.put("siteName", "My Application");
        data.put("currentYear", Year.now().getValue());
        
        return data;
    }
    
    // 특정 HTTP 상태에 대한 처리
    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ExceptionHandler(NoHandlerFoundException.class)
    public String handle404(NoHandlerFoundException ex) {
        return "error/404";
    }
}

표준화된 API 응답 형식
체계적인 예외 처리
입력 값 검증
로깅
보안 처리
공통 데이터 관리

@ControllerAdvice와 @RestControllerAdvice 차이

  • @ControllerAdvice: 전통적인 Spring MVC 애플리케이션에서 사용, 뷰 반환
  • @RestControllerAdvice: REST API 애플리케이션에서 사용, JSON 응답 반환
  • @RestControllerAdvice = @ControllerAdvice + @ResponseBody
  • 두 어노테이션 모두 예외 처리(@ExceptionHandler)와 공통 데이터 바인딩(@ModelAttribute) 기능 제공

기본 동작 방식

// @ControllerAdvice는 @ResponseBody가 없어서 뷰 리졸버를 통해 뷰를 찾음
@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)
    public String handleException() {
        return "error/500";  // View 이름 반환
    }
}

// @RestControllerAdvice는 내부적으로 @ResponseBody를 포함하여 객체를 JSON으로 직접 반환
@RestControllerAdvice
public class GlobalRestExceptionHandler {
    @ExceptionHandler(Exception.class)
    public ErrorResponse handleException() {
        return new ErrorResponse("에러 발생");  // JSON 응답
    }
}

적용 대상

// @Controller에 대한 예외 처리
@ControllerAdvice
public class MvcExceptionHandler {
    @ExceptionHandler(Exception.class)
    public String handleException() {
        return "error/500";
    }
}

// @RestController에 대한 예외 처리
@RestControllerAdvice
public class ApiExceptionHandler {
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleException() {
        return ResponseEntity
                .status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(new ErrorResponse("에러 발생"));
    }
}

// 특정 패키지나 클래스에만 적용
@RestControllerAdvice(
    basePackages = "com.example.api",
    annotations = RestController.class
)
public class SpecificApiExceptionHandler {
    // ...
}

실사용 예시

// MVC 애플리케이션의 예외 처리
@ControllerAdvice
public class GlobalMvcExceptionHandler {
    
    // 뷰 이름 반환
    @ExceptionHandler(Exception.class)
    public String handleException(Exception e, Model model) {
        model.addAttribute("errorMessage", e.getMessage());
        return "error/500";
    }
    
    // 모든 컨트롤러에서 사용할 공통 데이터
    @ModelAttribute
    public void globalAttributes(Model model) {
        model.addAttribute("siteName", "My Website");
    }
}

// REST API의 예외 처리
@RestControllerAdvice
public class GlobalRestExceptionHandler {
    
    // JSON 응답 반환
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleException(Exception e) {
        ErrorResponse error = ErrorResponse.builder()
            .status(HttpStatus.INTERNAL_SERVER_ERROR.value())
            .message(e.getMessage())
            .timestamp(LocalDateTime.now())
            .build();
            
        return ResponseEntity
            .status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(error);
    }
    
    // Validation 예외 처리
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationException(
            MethodArgumentNotValidException e) {
        Map<String, String> errors = e.getBindingResult()
            .getFieldErrors()
            .stream()
            .collect(Collectors.toMap(
                FieldError::getField,
                FieldError::getDefaultMessage
            ));
            
        ErrorResponse error = ErrorResponse.builder()
            .status(HttpStatus.BAD_REQUEST.value())
            .message("Validation failed")
            .errors(errors)
            .build();
            
        return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(error);
    }
}
profile
🌱

0개의 댓글