Notice
Recent Posts
Recent Comments
Link
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

끄적끄적

[spring] ControllerAdvice로 예외처리하기(+ 메세지 국제화) 본문

개발

[spring] ControllerAdvice로 예외처리하기(+ 메세지 국제화)

으아아아앜 2020. 3. 8. 00:22

- 다이어그램

  • 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);

 

Comments