@Valid, ExceptionHandling을 어떻게 해 주어야 할까?
/api/resource/1/innerResource/2/innerinnerResource/3 등으로 endpoint를 짜는 도중에
이러다간 끝이 없겠다 싶어서 Pathvariable에 들어가던 resource의 id값을 RequestBody로 받기로 했다.
근데 Resource와 innerResource는 일대다 관계 매핑을 해 주고 있던 차라, InnerResource에서 id값을 받지 못하면 상위 엔티티에 매핑을 하지 못하기 때문에 Resource의 id값에 @NotBlank를 걸어주었다. 왜 NotBlank를 걸어주었는지는 밑에 나온다.
그리고 실행을 돌렸더니 아래와 같은 결과가 나오게 되었다.
2023-03-24 12:58:17.393 WARN 20412 --- [ restartedMain] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'handlerExceptionResolver' defined in class path resource
[org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate
[org.springframework.web.servlet.HandlerExceptionResolver]: Factory method 'handlerExceptionResolver' threw exception; nested exception is java.lang.IllegalStateException: Ambiguous @ExceptionHandler method mapped for [class org.springframework.validation.BindException]: {protected org.springframework.http.ResponseEntity com.ddalggak.finalproject.global.error.GlobalExceptionHandler.handleBindException(org.springframework.validation.BindException), public final org.springframework.http.ResponseEntity org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler.handleException(java.lang.Exception,org.springframework.web.context.request.WebRequest) throws java.lang.Exception}
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'handlerExceptionResolver' defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]:
Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.web.servlet.HandlerExceptionResolver]: Factory method 'handlerExceptionResolver' threw exception; nested exception is java.lang.IllegalStateException:
Ambiguous @ExceptionHandler method mapped for [class org.springframework.validation.BindException]: {protected org.springframework.http.ResponseEntity com.ddalggak.finalproject.global.error.GlobalExceptionHandler.handleBindException(org.springframework.validation.BindException), public final org.springframework.http.ResponseEntity org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler.handleException(java.lang.Exception,org.springframework.web.context.request.WebRequest) throws java.lang.Exception}
keyWord : handlerExceptionResolver, BindException
사유 : @ExceptionHandler에 걸어놓은 BindException이 GlobalExceptionHandler에서 동일한 예외 처리를 하고 있기 때문에 뭐 어떤 핸들러 찾아가야 하는지를 스프링에서 모른다. 그래서 Ambiguous(모호성) 문제가 대두되게 된다. 근데 나는 뭘 해준게 없는 것 같은데....
왜 이런 이유가 터졌는지에 대해 알아보자면 2가지 이유가 있다.
첫 번째로 우리는 exceptionHandler에 ResponseEntityExceptionHandler를 상속받아서 사용하고 있었다.
ResponseEntityExceptionHandler는 Spring MVC에서 발생할 수 있는 예외들에 대해서 미리 Handling을 해 놓은 클래스로써, 다음의 exception들에 대해서 미리 handling을 해 놓고 있다.
BindingException, MethodArgumentNotValidException을 포함한 몇가지들을 핸들링하고 있는데, globalExceptionHandler에서 충분히 다양한 ExceptionHandling을 하지 않을 때 유용하게 사용할 수 있다.
물론 Custom한 Exception 등에 대해서는 잡아주지 않는다.
그런데 나는 다음과 같은 코드를 작성하였다.
@ExceptionHandler(BindException.class)
protected ResponseEntity<ErrorResponse> handleBindException(BindException e) {
log.error("handleBindException throws BindException : {}", e.getMessage());
return ErrorResponse.from(INVALID_REQUEST, e.getBindingResult());
}
그래서 ResponseEntityExceptionHandler도 BindException에 대해 핸들링하고 있고, 나는 거기다 또 @ExceptionHandler로 BindException을 잡아주느라 Ambiguous 문제가 터진 것이었다.
그래서 첫 번째 해결책으로 ResponseEntityExceptionHandler를 상속하고 있으니 BindException.class를 따로 핸들링하는 메소드를 작성하는게 아니라 이미 작성된 메소드를 오버라이딩 해 보았다.
@Override
protected ResponseEntity<Object> handleBindException(BindException ex, HttpHeaders headers,
HttpStatus status,
WebRequest request) {
log.error("handleBindException throws BindException : {}", ex.getMessage());
return ErrorResponse.from(INVALID_REQUEST, ex.getBindingResult());
}
그런데 이상한 결과가 나왔다.No validator could be found for constraint 'javax.validation.constraints.NotBlank
이라는 문구가 출력되었다.
이게 뭐지 하고 한참을 들여다보니 다음과 같은 오류가 있었다.
Bean validation에 대해 여러 어노테이션이 있다는 것은 알았지만, 이 어노테이션들에 대한 차이에 대해서는 모르고 있어서 한번 찾아봤더니 다음과 같은 결과가 있었다.
Bean validation
Dto나 entity에서 null valid를 할 때 다음과 같은 방식이 사용된다.
@Notnull
@NotEmpty
@NotBlank
어떤 차이가 있을까?@Notnullnotnull은 이름 그대로 null만 아니면 된다. " " , "" 이런것도 다 받는다.@Size
,@Min
,@Max
등 이용할 수도 있고,@Email
로 이메일 형식도 받아올 수 있다.@NotEmptynotempty는 notnull에서 +""가 추가된 것이다. 그럼 " "는 어떡하냐고? " "는 받는다.
근데 나 " "도 막고싶어.@NotBlank막아드렸습니다.
나는 당연히도 NotBlank를 차용했는데, 사용하고 난 코드는 다음과 같다.
@NotBlank(message = "projectId값이 필요합니다.")
private Long projectId;
그런데 역시 NotNull, NotEmpty같은 어노테이션들이 괜히 있는게 아니었다.
@NotNull은 Nullable한 타입에 선언해야 해서 primitive 타입에는 선언할 수가 없다.
@NotBlank는 단 하나의 공백 문자열도 들어가면 안 돼서 String이나 CharSequence등에만 사용할 수 있다.
@NotEmpty는 empty 키워드가 들어갈 수 있는 CharSequence, Collection, Map, Array 등만 가능하다.
그런데 나는 Long 타입에 대해 @NotBlank를 주고 있었으니, 오류를 잡으려고 validation을 걸었는데 역설적이게도 validation에 대해 오류가 잡힌 것이었다.
다시 돌아와서, NotBlank를 NotNull로 잡아주고 돌렸다. 그랬더니 다음과 같은 오류를 뱉었다.
2023-03-24 14:02:40.301 WARN 42280 --- [nio-8080-exec-4] .m.m.a.ExceptionHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [1] in public org.springframework.http.ResponseEntity<?> com.ddalggak.finalproject.domain.task.controller.TaskController.createTask(com.ddalggak.finalproject.global.security.UserDetailsImpl,com.ddalggak.finalproject.domain.task.dto.TaskRequestDto): [Field error in object 'taskRequestDto' on field 'projectId': rejected value [null]; codes [NotNull.taskRequestDto.projectId,NotNull.projectId,NotNull.java.lang.Long,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [taskRequestDto.projectId,projectId]; arguments []; default message [projectId]]; default message [project Id is required]] ]
내 validation 표현식은 어딜가고 이런 오류를 뱉는거죠?
그런데 생각해보니까 또 당연한 오류였다. 지금은 Binding이 안되는게 아니라 Null값에 대해 오류를 잡는거라 BindingException이 터진게 아니라 ArgumentNotValid 옵션이 뜨는거였다.
그래서 다시 또 다음과 같은 메소드를 작성하였다.
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers, HttpStatus status, WebRequest request) {
log.error("handleBindException throws BindException : {}", ex.getMessage());
return ErrorResponse.from(INVALID_REQUEST, ex.getBindingResult());
}
결과 : Nice
근데 이쯤되면 고민이 온다. ResponseEntityExceptionHandler를 사용해서 Override로 커스텀 해 줄 것이냐, 아니면 다음과 같이 Exception을 잡아 줄 것이냐
[원래 의도는 이랬다]
@ExceptionHandler(BindException.class)
protected ResponseEntity<ErrorResponse> handleBindException(BindException e) {
log.error("handleBindException throws BindException : {}", e.getMessage());
return ErrorResponse.from(INVALID_REQUEST, e.getBindingResult());
}
bindException을 걸고 나머지 핸들링을 하지 못한 오류에 대해서는
@ExceptionHandler(Exception.class)
private ResponseEntity<ErrorResponse> handleExceptionInternal(ErrorCode errorCode, String message) {
return ResponseEntity.status(errorCode.getHttpStatus())
.body(makeErrorResponse(errorCode, message));
}
다 여기 짬통에 박아두느냐
개인의 편의 차이인 것 같다.
[참조]
https://velog.io/@u-nij/%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-221029
https://itecnote.com/tecnote/java-error-creating-bean-with-name-handlerexceptionresolver-defined-in-class-path-resource/
https://yuja-kong.tistory.com/128
https://sas-study.tistory.com/473
https://blog.naver.com/PostView.naver?blogId=writer0713&logNo=221605253778&parentCategoryNo=&categoryNo=83&viewDate=&isShowPopularPosts=false&from=postView