끄적끄적
[spring] ControllerAdvice로 예외처리하기(+ 메세지 국제화) 본문
- 다이어그램
- ExceptionHandlerAdvice는 스프링의 ResponseEntityExceptionHandler를 상속받아 이미 존재하는 메소드를 활용하여 로직을 작성합니다.
- ApiError는 클라이언트에 예외에 대한 응답을 하는 클래스입니다.
- xxxxxxxContent 클래스들은 해당 에러가 어떤 내용을 갖고 있는지 의미합니다.
- ValidErrorContent는 이 에러의 내용이 데이터를 검증하는 과정에서 발생했음을 의미합니다.
- FieldErrorContent는 에러의 원인이 명확한 필드(DTO의 필드)일때를 의미합니다.
- GlobalErrorContent는 FieldErrorContent가 아닐때를 의미합니다.
예를들어, 서비스 로직에 의한 예외, 에러를 발생시킨 필드가 명확하지 않을때입니다.
- @RestControllerAdvice(ControllerAdvice)
@RestControllerAdvice
public class ExceptionHandlerAdvice extends ResponseEntityExceptionHandler {
private final MessageSource validMessageSource;
/**
* @Valid 어노테이션에 의해 발생한 예외
*/
@Override
protected ResponseEntity<Object> handleBindException(BindException ex, HttpHeaders headers, HttpStatus status, WebRequest req) {
List<ErrorContent> contentList = getErrorContentList(ex, BindException.class);
return responseWithBody(ex, new ApiError(HttpStatus.BAD_REQUEST, contentList), req);
}
/**
* multipart/data-from 요청에서 @Valid 어노테이션에 의해 발생한 예외
*/
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest req) {
List<ErrorContent> contentList = getErrorContentList(ex, MethodArgumentNotValidException.class);
return responseWithBody(ex,new ApiError(HttpStatus.BAD_REQUEST,contentList), req);
}
/**
* Controller에 @Validated가 선언
* @PathVariable, @RequestParam에서 @Valid에 의해 발생한 예외
*/
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Object> constraintViolationException(ConstraintViolationException ex, WebRequest req, Locale locale){
List<ErrorContent> contentList = getErrorContentList(ex, ConstraintViolationException.class);
return responseWithBody(ex,new ApiError(HttpStatus.BAD_REQUEST,contentList),req);
}
/**
* @PreAuthorize에 의해 발생한 예외
*/
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<Object> accessDeniedException(AccessDeniedException ex, WebRequest req, Locale locale){
String message = validMessageSource.getMessage("signin.need", null,locale);
return responseWithBody(ex, new ApiError(HttpStatus.BAD_REQUEST,Arrays.asList(new GlobalErrorContent(message))), req);
}
/**
* 커스텀하게 선언한(FieldException) 예외
*/
@ExceptionHandler(FieldException.class)
public ResponseEntity fieldException(FieldException ex, WebRequest req, Locale locale){
String message = validMessageSource.getMessage(ex.getMessage(), null,locale);
return responseWithBody(ex, new ApiError(HttpStatus.BAD_REQUEST,Arrays.asList(new FieldErrorContent(ex.getFieldName(),message,ex.getRejectedValue()))), req);
}
/**
* 커스텀하게 선언한(CustomException) 예외
*/
@ExceptionHandler(CustomRuntimeException.class)
public ResponseEntity customRuntimeException(CustomRuntimeException ex, WebRequest req, Locale locale){
String message = validMessageSource.getMessage(ex.getMessage(), null,locale);
return responseWithBody(ex, new ApiError(HttpStatus.BAD_REQUEST,Arrays.asList(new GlobalErrorContent(message))), req);
}
/**
* 커스텀하게 선언한(RuntimeException) 예외
*/
@ExceptionHandler(RuntimeException.class)
public ResponseEntity runtimeException(RuntimeException ex, WebRequest req){
log.error("runtimeException ", ex);
String message = ex.getMessage();
return responseWithBody(ex, new ApiError(HttpStatus.BAD_REQUEST,Arrays.asList(new GlobalErrorContent(message))), req);
}
private ResponseEntity<Object> responseWithBody(Exception ex, ApiError body, WebRequest request){
return super.handleExceptionInternal(ex, body, new HttpHeaders(), body.getStatus(), request);
}
/**
* 예외 타입에 따라 Exception을 캐스팅하고, 변환하는 로직
* 아래 addXXXX 메소드들은 상세한 변환로직을 의미함
*/
private List<ErrorContent> getErrorContentList(Exception e,Class aClass){
List<ErrorContent> contentList = new ArrayList<>();
if(MethodArgumentNotValidException.class.isAssignableFrom(aClass)){
MethodArgumentNotValidException ex = (MethodArgumentNotValidException) e;
addFieldErrors(contentList, ex.getBindingResult().getFieldErrors());
addGlobalErrors(contentList, ex.getBindingResult().getGlobalErrors());
}
else if(BindException.class.isAssignableFrom(aClass)){
BindException ex = (BindException) e;
addFieldErrors(contentList, ex.getBindingResult().getFieldErrors());
addGlobalErrors(contentList, ex.getBindingResult().getGlobalErrors());
}
else if(ConstraintViolationException.class.isAssignableFrom(aClass)){
ConstraintViolationException ex = (ConstraintViolationException) e;
addGlobalErrors(contentList, ex.getConstraintViolations());
}
return contentList;
}
private void addFieldErrors(List<ErrorContent> contentList, List<FieldError> fieldErrorList){
for(FieldError error : fieldErrorList){
contentList.add(new FieldErrorContent(
error.getField(),
error.getDefaultMessage(),
error.getRejectedValue()
));
}
}
private void addGlobalErrors(List<ErrorContent> contentList, List<ObjectError> globalErrorList){
for(ObjectError error : globalErrorList){
contentList.add(new GlobalErrorContent(
error.getDefaultMessage()
));
}
}
private void addGlobalErrors(List<ErrorContent> contentList, Set<ConstraintViolation<?>> constraintViolations){
List constraintViolationList = constraintViolations.stream().collect(Collectors.toList());
for(Object object : constraintViolationList){
ConstraintViolation constraintViolation = (ConstraintViolation)object;
contentList.add(new GlobalErrorContent(
constraintViolation.getMessage()
));
}
}
}
- 주석 참조
- ApiError, ErrorContent
@AllArgsConstructor
@Getter
public class ApiError {
private HttpStatus status;
private List<ErrorContent> errorContent;
}
public interface ErrorContent {
}
public interface ValidErrorContent extends ErrorContent {
}
@Getter
@AllArgsConstructor
public class FieldErrorContent implements ValidErrorContent{
private String field;
private String message;
private Object rejectedValue;
}
@Getter
@AllArgsConstructor
public class GlobalErrorContent implements ValidErrorContent{
private String message;
}
- 메세지 국제화
- 커스텀 익셉션
아래와 같이 생성자에 message.properties의 키값을 설정하고, @ControllerAdvice에서 적절히 사용한다.
@Getter
public class DuplicateEmailException extends CustomRuntimeException {
public DuplicateEmailException(){
super("email.duplicate");
}
public DuplicateEmailException(String msg){
super(msg);
}
}
- @Valid
@Configuration에서 LocalValidatorFactoryBean을 설정한다.
(messageSource()에 대한 국제화 처리는 필수)
@Bean
public LocalValidatorFactoryBean validator() {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(messageSource());
return bean;
}
- @ControllerAdvice
아래와 같이 MessageSource를 주입받고, getMeesage() 메소드를 통해 message값을 구하여 적절한 로직으로 사용한다.
String message = validMessageSource.getMessage(ex.getMessage(), null,locale);
'개발' 카테고리의 다른 글
[jpa] @Entity에 default 값을 설정하기 (0) | 2020.03.09 |
---|---|
[jpa] querydsl 사용하기 (0) | 2020.03.09 |
[java] localdatetime에서 milliseconds얻기 (0) | 2020.03.07 |
[정규식] 정규표현식이 탐색을 탐욕적으로 할 때 (0) | 2020.03.06 |
[spring] gmail 발송시 자격인증 관련 에러 발생할때 (0) | 2020.03.06 |
Comments