디스패처 서블릿 동작 중, 컨트롤러 메소드를 호출하는 과정에서 메소드의 값을 처리해주는 ArgumentResolver가 동작하는데, 이때 @Valid 역시 ArgumentResolver에 의해 처리된다. 이러한 이유로, @Valid 어노테이션은 컨트롤러에서만 동작하며, 다른 계층에서 파라미터를 검증하기 위해서는 @Validated와 결합되어야한다.
spring boot 2.3버전 부터는 validation 의존성을 따로 추가해줘야 한다.2.3 버전 이전에는 spring-boot-starter-web 의존성 내부에 함께 있었지만, 2.3 버전부터 spring-boot-starter-validation으로 분리되었다고 한다. 주의하자.
// 필드 위에 작성한다.
@Null // null만 혀용합니다.
@NotNull // null을 허용하지 않습니다. "", " "는 허용합니다.
@NotEmpty // null, ""을 허용하지 않습니다. " "는 허용합니다.
@NotBlank // null, "", " " 모두 허용하지 않습니다.
@Email // 이메일 형식을 검사합니다. 다만 ""의 경우를 통과 시킵니다. @Email 보다 아래 나올 @Patten을 통한 정규식 검사를 더 많이 사용합니다.
@Pattern(regexp = ) // 정규식을 검사할 때 사용됩니다.
@Size(min=, max=) // 길이를 제한할 때 사용됩니다.
@Max(value = ) // value 이하의 값을 받을 때 사용됩니다.
@Min(value = ) // value 이상의 값을 받을 때 사용됩니다.
@Positive // 값을 양수로 제한합니다.
@PositiveOrZero // 값을 양수와 0만 가능하도록 제한합니다.
@Negative // 값을 음수로 제한합니다.
@NegativeOrZero // 값을 음수와 0만 가능하도록 제한합니다.
@Future // 현재보다 미래
@Past // 현재보다 과거
@AssertFalse // false 여부, null은 체크하지 않습니다.
@AssertTrue // true 여부, null은 체크하지 않습니다.
이 어노테이션을 검증할 객체의 프로퍼티에 달고 컨트롤러에서 @Valid 어노테이션을 붙여주면 유효성 검증이 진행된다. @Valid는 컨드롤러에서만 동작하는데, 불가피한 상황으로 다른 레이어에서 파라미터를 검증해야 할 수도 있다
@Service
@Validated
public class UserService {
public void signup(@Valid UserDto userDto) {
...
}
}
객체를 검증하는 방법이 달라질 수도 있다. 예를들어, UserDto로 오는 요청들을 다른 방식으로 검증해야 될 경우가 생길 수도 있다. 이를 해결하기 위한 Grouping 기능을 사용할 수도 있다.
// 그룹을 지정하기 위한 마커 인터페이스들
public interface SignUpMarker {}
public interface SignInMarker {}
// Dto에서 그룹 지정
public class UserDto {
@NotBlank(groups = {SignUpMarker.class, SignInMarker.class}) //회원가입, 로그인 시
private String accountId;
@NotBlank(groups = {SignUpMarker.class, SignInMarker.class}) //회원가입, 로그인 시
private String password;
@NotBlank(groups = SignUpMarker.class) // 회원가입만 검증
private String nickname;
@NotBlank // 없으면 기본값(Default)
private String email
}
// Controller에서 검증할 클래스 지정
@PostMapping("/signup")
public ResponseEntity signUp(@Validated(SignUpMarker.class) @RequestBody UserDto user) {
...
}
@PostMapping("/signin")
public ResponseEntity signIn(@Validated(SignInMarker.class) @RequestBody UserDto user) {
...
}
@Valid와 달리 @Valudated는 스프링의 AOP기반(관점 기반 모듈화)으로 메소드 요청을 인터셉터하여 처리한다. @Validated가 클래스 레벨에 선언되면(Controller에서는 생략 가능) 해당 클래스에 유효성 검증을 위한 인터셉터인 MethodValidationInterceptor가 등록되고, 해당 클래스의 메소드들이 호출 될 때 요청을 가로채 인터셉터를 통해 유효성 검증을 한다.
@Valid -> MethodArgumentNotValidException이 발생한다.
@Validated -> ConstraintViolationException이 발생한다.
entity 객체인 Post가 있고, request 객체인 PostCreate.class 가 있다고 가정하자. 각 프로퍼티에 @NotBlank를 붙여서 null,""," " 등 다 안되게 한다.
public class PostCreate {
@NotBlank(message = "타이틀을 입력해주세요.")
private String title;
@NotBlank(message = "내용을 입력해주세요.")
private String content;
}
이 request 객체를 이용해서 컨트롤러에서 @PostMapping 방식으로
Post를 작성한다. 이때 컨트롤러에서 @Valid를 붙여주고 이 컨트롤러를 호출하면, 디스패처 서블릿 동작 중, 컨트롤러 메소드를 호출하는 과정에서 메소드의 값을 처리해주는 ArgumentResolver가 동작한다.그렇게 되면 해당 클래스의 유효성 검증을 위한 인터셉터인 MethodValidationInterceptor가 등록되고, 해당 컨트롤러의 메소드가 호출 될 때 요청을 가로채 인터셉터를 통해 유효성 검증을 한다.
@PostMapping("/posts")
public void post(@RequestBody @Valid PostCreate request) throws Exception {
request.validate();
postService.write(request);
}
스프링에서는 예외처리를 @ControllerAdvice를 통해서 따로 클래스로 관리할 수 있다. @ControllerAdvice는 모든 @Controller 즉, 전역에서 발생할 수 있는 예외를 잡아 처리해주는 annotation이다.
ExceptionController.class를 만들고 @ControllerAdvice어노테이션을 이용해서 @Valid -> MethodArgumentNotValidException 예외처리를 해보자.
@ControllerAdvice
public class ExceptionController {
//@Valid 예외처리
@ResponseStatus(HttpStatus.BAD_REQUEST)
//@Valid로 인한 메소드 에러가 발생했을때만 이 컨트롤러가 에러를 잡아주게 끔
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public ErrorResponse invalidRequestHandler(MethodArgumentNotValidException e){
ErrorResponse response = ErrorResponse.builder()
.code("400")
.message("잘못된 요청입니다.")
.build();
//ErrorResponse에 MethodArgumentNotValidException에러(@NotBlank 필드에러)를 추가해서 리턴한다.
for (FieldError fieldError : e.getFieldErrors()) {
response.addValidation(fieldError.getField(), fieldError.getDefaultMessage());
}
return response;
}
}
예외처리로 하여금 클라이언트에 response 응답 값을 보낸다.
@ResponseStatus(HttpStatus.BAD_REQUEST) 400에러를 보낸다는 뜻이다. @ExceptionHandler(MethodArgumentNotValidException.class)를 붙여서 @Valid로 인해 발생한 MethodArgumentNotValidException.class를 잡는다. 그 후에@ResponseBody를 붙여서 responseBody에 보낼 response객체를 만들고,
MethodArgumentNotValidException의 에러가 발생한 필드를 가지고 와서
addValidation을 통해서 해당하는 필드에 대한 직접 설정한 메시지를 response에 담아서 클라이언트에 response응답을 보낼수 있다.
request요청으로 PostCreate를 이용해서 Post를 생성한다고 가정하자.
이 때 제목을 뺀다. 그 후에 request를 json타입으로 바꿔서 mockMvc의 content에 담는다.
@Test
@DisplayName("/posts 요청시 title값은 필수다.")
void test2() throws Exception {
//given
PostCreate request = PostCreate.builder()
.content("내용입니다.")
.build();
String json = objectMapper.writeValueAsString(request);
mockMvc.perform(post("/posts")
.contentType(MediaType.APPLICATION_JSON)
.content(json)
)
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("400"))
.andExpect(jsonPath("$.message").value("잘못된 요청입니다."))
.andExpect(jsonPath("$.validation.title").value("타이틀을 입력해주세요."))
.andDo(print());
}
.andExpect()를 이용해서 서버에서 받은 status() 상태와 response.code , response.message , response.addValidation 을 확인한다.
request 요청
response 응답