1. 스프링 MVC 예외 처리 개요
- 스프링 MVC는 애플리케이션에서 발생하는 예외를 우아하게 처리할 수 있는 다양한 메커니즘을 제공합니다.
- 예외 처리는 애플리케이션의 안정성과 사용자 경험 향상에 중요한 역할을 합니다.
- 스프링의 예외 처리 방식은 크게 다음과 같이 나뉩니다:
- 컨트롤러 레벨 의
@ExceptionHandler - 글로벌 레벨의
@ControllerAdvice또는@RestControllerAdvice - HTTP 상태 코드 기반의 오류 페이지
- 컨트롤러 레벨 의
2. @ExceptionHandler 애노테이션
@ExceptionHandler애노테이션은 컨트롤러 또는@ControllerAdvice클래스 내에서 예외를 처리하는 메서드를 지정하는 데 사용됩니다.- 이 애노테이션이 적용된 메서드는 지정된 예외 타입이 발생했을 때 자동으로 호출됩니다.
2.1 기본 사용법
@Controller나@RestController클래스 내에서 사용할 수 있습니다.- 처리하고자 하는 예외 타입을 지정하여 해당 예외가 발생했을 때 특정 응답을 반환할 수 있습니다.
기본 예제 코드
@Controller
public class SimpleController {
@ExceptionHandler(IOException.class)
public ResponseEntity<String> handle() {
return ResponseEntity.internalServerError().body("Could not read file storage");
}
}
- 이 예제는
IOException이 발생했을 때 500 상태 코드와 함께 에러 메시지를 반환합니다.
2.2 예외 매핑 방식
- 예외 매핑은 발생 한 최상위 예외 또는 중첩된 원인 예외와 일치시킬 수 있습니다.
- 스프링 5.3부터는 임의의 깊이에 있는 원인 예외까지 매칭할 수 있습니다(이전 버전에서는 직접적인 원인만 고려했습니다).
- 여러 예외 메서드가 매칭될 때는 일반적으로 최상위 예외 매치가 원인 예외 매치보다 우선시됩니다.
예외 매핑 예시
ServiceException → DataAccessException → SQLException → IOException
- 위와 같은 예외 계층 구조에서 5.3 이전에는 IOException을 처리하는 핸들러가 있어도 ServiceException이나 그 직접적인 원인인 DataAccessException만 확인했기 때문에 매칭되지 않았습니다
- 하지만 5.3 부터는 예외 체인을 깊게 탐색해서 IOException까지 확인하므로 해당 핸들러가 작동할 수 있게 되었습니다.
2.3 예외 매개변수 선언
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handleIoException(IOException ex) {
return ResponseEntity.internalServerError().body(ex.getMessage());
}
- 이 예제에서는
FileSystemException과RemoteException을 처리하며, 이 두 예외는 모두IOException을 확장합니다.
2.4 일반 예외 타입 사용 예제
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handleExceptions(Exception ex) {
return ResponseEntity.internalServerError().body(ex.getMessage());
}
- 매개변수로
Exception을 사용하여 더 일반적인 시그니처를 가질 수 있습니다.
예외 매칭 동작에 주의하세요. 최상위 예외와 원인 예외 매칭은 의외의 결과를 가져올 수 있습니다.
- 최상위 예외와 원인 예외 매칭의 차이를 이해하는 것이 중요합니다:
IOException변형에서는 메서드가 일반적으로 실제FileSystemException또는RemoteException인스턴스를 인자로 받습니다.- 그러나 이러한 예외가
IOException래퍼 내에 전파되는 경우, 전달된 예외 인스턴스는 래퍼 예외입니다. handle(Exception)변형에서는 래핑 시나리오에서 항상 래퍼 예외와 함께 호출됩니다.
- 가능한 한 메서드 시그니처에서 구체적인 예외 타입을 사용하는 것이 좋습니다.
- 여러 예외를 처리하는 메서드를 각 특정 예외 타입에 대한 개별
@ExceptionHandler메서드로 분리하는 것이 좋습니다.
2.4 미디어 타입 매핑
@ExceptionHandler메서드는 생성 가능한 미디어 타입을 선언할 수도 있습니다.- 이를 통해 HTTP 클라이언트가 요청한 미디어 타입에 따라 오류 응답을 세분화할 수 있습니다.
- 일반적으로 "Accept" HTTP 요청 헤더를 기반으로 합니다.
미디어 타입 매핑 예제
@ExceptionHandler(produces = "application/json")
public ResponseEntity<ErrorMessage> handleJson(IllegalArgumentException exc) {
return ResponseEntity.badRequest().body(new ErrorMessage(exc.getMessage(), 42));
}
@ExceptionHandler(produces = "text/html")
public String handle(IllegalArgumentException exc, Model model) {
model.addAttribute("error", new ErrorMessage(exc.getMessage(), 42));
return "errorView";
}
여기서는 동일한 예외 타입을 처리하지만 다른 미디어 타입으로 응답합니다. JSON을 요청하는 클라이언트에는 JSON 오류가 제공되고 브라우저에는 HTML 오류 뷰가 제공됩니다.
2.5 메서드 매개변수
@ExceptionHandler 메서드는 다음과 같은 매개변수를 지원합니다:
- 예외 타입: 발생한 예외에 접근하기 위해 사용
- HandlerMethod: 예외를 발생시킨 컨트롤러 메서드에 접근하기 위해 사용
- WebRequest, NativeWebRequest: Servlet API 직접 사용 없이 요청 파라미터와 속성에 접근
- ServletRequest, ServletResponse: 요청이나 응답의 특정 타입에 접근
- HttpSession: 세션 존재 보장(null이 아님)
- Principal: 현재 인증된 사용자
- HttpMethod: 요청의 HTTP 메서드
- Locale: 현재 요청의 로케일
- TimeZone, ZoneId: 현재 요청과 관련된 시간대
- OutputStream, Writer: 원시 응답 본문에 접근
- Map, Model, ModelMap: 오류 응답 모델에 접근
- RedirectAttributes: 리디렉션 시 사용할 속성 지정
- @SessionAttribute: 세션 속성에 접근
- @RequestAttribute: 요청 속성에 접근
세션 접근은 스레드 안전하지 않습니다. 여러 요청이 세션에 동시에 접근하는 경우
RequestMappingHandlerAdapter 인스턴스의 synchronizeOnSession 플래그를true로 설정하는 것을 고려하세요.
2.6 반환 값
@ExceptionHandler 메서드는 다음과 같은 반환 값을 지원합니다:
- @ResponseBody: 반 환값이
HttpMessageConverter를 통해 변환되고 응답에 작성됩니다. HttpEntity<B>,ResponseEntity<B>: 반환값이 헤더와 본문을 포함한 전체 응답을 지정합니다.- ErrorResponse: RFC 9457 오류 응답을 본문의 세부 정보와 함께 렌더링합니다.
- ProblemDetail: RFC 9457 오류 응답을 본문의 세부 정보와 함께 렌더링합니다.
- String:
ViewResolver를 통해 해석될 뷰 이름 - View: 렌더링에 사용할
View인스턴스 - Map, Model: 암시적 모델에 추가될 속성
- @ModelAttribute: 암시적 모델에 추가될 속성
- ModelAndView: 사용할 뷰와 모델 속성, 선택적으로 응답 상태
- void: 메서드가 응답을 완전히 처리한 것으로 간주됩니다.
- 기타 반환값: 단순 타입이 아닌 경우 모델 속성으로 처리됩니다.
ResponseEntity 예제
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException ex) {
ErrorResponse error = new ErrorResponse(ex.getMessage());
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
3. @ControllerAdvice를 활용한 글로벌 예외 처리
@ControllerAdvice또는@RestControllerAdvice클래스에 선언된@ExceptionHandler,@InitBinder,@ModelAttribute메서드는 모든 컨트롤러에 적용됩니다.- 스프링 5.3부터
@ControllerAdvice의@ExceptionHandler메서드는 모든@Controller또는 다른 핸들러의 예외를 처리하는 데 사용할 수 있습니다.