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
public class GlobalExceptionHandler {
// 모든 컨트롤러에서 사용할 공통 데이터
@ModelAttribute
public void addAttributes(Model model) {
model.addAttribute("serverTime", new Date());
}
}
@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 상태 코드 매핑, 일관된 에러 응답 형식 제공
@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
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는 @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);
}
}