[Spring] @valid 발생 예외 controllerAdvise를 통해 custom message 던지기
@vaild 은 @Requestbody의 통해 받아오는 데이터 값에 대한 검증이 가능하게 하는 어노테이션입다.(컨트롤러 메서드의 유효성 검증만 가능)
사용자의 입력 값에 대해서 백,프론트 과연 어디서 검증을 해야 하나 라는 생각이 들긴 합니다. 일단 프런트에서 받아야지라고 말하고 싶지만 지금은 제가 둘 다 하고 있으니까.. 더 익숙한 백엔드에서 받기로 했습니다.
아래에서 보면 MemberLoginReqDto의 데이터 바인딩에 대해 입력값에 대한 검증이 @Email,@NotEmpty을 통해서 이루어지고 있습니다. 굉장히 편리하죵
@PostMapping("/doLogin")
public ResponseEntity<CommonResponse> memberLogin(@Valid @RequestBody MemberLoginReqDto memberLoginReqDto){
Member member = memberService.doLogin(memberLoginReqDto);
// 토큰 생성 로직
String jwtToken = jwtTokenProvider.createToken(member.getEmail(),member.getRole().toString());
Map<String,Object> member_info = new HashMap<>();
member_info.put("member_id",member.getId());
member_info.put("token",jwtToken);
return new ResponseEntity<>(new CommonResponse(HttpStatus.OK,"member success logined",member_info),HttpStatus.OK);
//------------------
@Data
public class MemberLoginReqDto {
@NotEmpty(message = "email is essential")
@Email(message = "email is not valid")
private String email;
@Size(min=4, message = "minimum length is 4")
private String password;
}
검증을 해주는 건 참 좋았지만... 프런트에서 이 예외를 받을 때 적절한 상태코드 과 우리가 입력한 메시지를 받지 못하는 것을 확인했습니다. 그 원인은 @Valid에서 던지는 예외를 잡아주지 못했기 때문이라 생각을 했습니다.
개발자도구로 확인해 보니, 400 상태코드를 받아오고 있습니다.
500 코드가 아닌걸 보니, 잡아서 던지기는 하는데 우리가 원하는 "email is not valid" 문구를 같이 보내주지 않는 것 같습니다.
이를 해결하기 위해서 SpringApplication에서 발생하는 예외를 처리하는 미들웨어로 사용하고 있는 ExceptionHandler class에 Valid가 던지는 예외를 잡아주도록 수정이 필요합니다.
그래서 우선 @Valid 검증 실패 시 어떤 예외를 던지는지 찾아보았습니다.
MethodArgumentNotValidException (Spring Framework 6.1.3 API)
resolveErrorMessages Deprecated, for removal: This API element is subject to removal in a future version. Returns: a Map with errors as keys and resolved messages as values Since: 6.0.3
docs.spring.io
위 MethodArgumentNotValidException 문서에서 보면, ' Exception to be thrown when validation on an argument annotated with @Valid fails.' 으로 @Vaild 어노테이션 이 실패할 때 위의 예외를 던지는 것을 알 수 있습니다.
그럼 MethodArgumentNotValidException 을 ExceptionHandler class에서 처리하도록 코드를 수정해 보았습니다.
간단하게 getMessage 메서드만 쓰면 되겠지..라고 했는데,
getMessage에 정말 뭐를 꾸역꾸역 담아놓은 것을 볼 수 있습니다.
2024-02-09 18:10:07.058 ERROR 25684 --- [nio-8080-exec-1] c.e.o.common.ExceptionHandlerClass : Validation failed for argument [0] in public org.springframework.http.ResponseEntity<cohttp://m.example.ordering_lecture.common.CommonResponse> cohttp://m.example.ordering_lecture.member.controller.MemberController.memberCreate(com.example.ordering_lecture.member.dto.MemberCreateReqDto): [Field error in object 'memberCreateReqDto' on field 'email': rejected value [정말로]; codes [Email.memberCreateReqDto.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [memberCreateReqDto.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@6f561c3,.*]; default message [email is not valid]]
..
흠 다시
MethodArgumentNotValidException의 생성자를 보면,
public MethodArgumentNotValidException(MethodParameter parameter, BindingResult bindingResult) {
super(bindingResult);
this.parameter = parameter;
}
두 개의 매개변수를 받는 것을 확인할 수 있습니다.
BindingResult는 스프링에서 제공하는 검증 오류를 보관하는 객체로, 검증오류 발생 시 이 객체에 저장이 됩니다. 이를 사용하면 에러가 발생 시, 예외를 담아서 컨트롤러를 정상호출합니다. (위에서 서버에서 예외가 터지지 않은 원인)
BindingResult의 검증 오류를 적용하는 방법은 3가지는,
1. @ModelAttribute의 객체에 데이터 바인딩이 실패하면 스프링이 FieldError를 생성해 BindingResult에 넣어줌
2. 개발자가 직접 new FieldError를 만들어 넣어주기
3. Validator(@Valid, @Validated)를 사용
@Valid 사용 시, 바인딩 과정에 검증을 통해 검증이 실패하면 FieldError를 만들어서 BindingResult에 넣어주는 것을 알 수 있습니다. ( 위 로그 Field error in object 'memberCreateReqDto' on field 'email')
그리고 default message에 우라가 입력한 메시지가 담겨 있습니다. ( default message [email is not valid]])
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String,Object>> methodArgumentNotValidException(MethodArgumentNotValidException e){
return ErrorResponseDto.makeMessage(HttpStatus.BAD_REQUEST, e.getBindingResult().getFieldError().getDefaultMessage());
}
이런 식으로 MethodArgumentNotValidException에서 입력한 메시지를 꺼낼 수 있었습니다.
일단 완료는 했지만 BindingResult라는 어려운.. 주제를 만나게 되었습니다.
지금은 BindingResult을 통해서, 데이터 바인딩의 검증의 사용자화가 가능하겠구나..라는 가능성을 기억하고 넘어가 보도록 하겠습니다.
프로젝트를 진행하다 보면 곧 정리할 날이 올 것 같네요.
참조 :