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
또는 다른 핸들러의 예외를 처리하는 데 사용할 수 있습니다.
3.1 기본 사용법
@ControllerAdvice
는@Component
로 메타 어노테이션되어 있어 컴포넌트 스캔을 통해 스프링 빈으로 등록할 수 있습니다.@RestControllerAdvice
는@ControllerAdvice
와@ResponseBody
로 메타 어노테이션되어 있습니다.
ControllerAdvice 예제
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFound(ResourceNotFoundException ex) {
ErrorResponse error = new ErrorResponse("Resource not found", ex.getMessage());
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
ErrorResponse error = new ErrorResponse("Internal server error", ex.getMessage());
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}