Spring - 데이터 검증 @Valid

Seongjin Jo·2023년 1월 2일
0

REST API

목록 보기
3/4

💥 @Valid


@Valid 데이터 검증 동작원리

디스패처 서블릿 동작 중, 컨트롤러 메소드를 호출하는 과정에서 메소드의 값을 처리해주는 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은 체크하지 않습니다.

@Validated

이 어노테이션을 검증할 객체의 프로퍼티에 달고 컨트롤러에서 @Valid 어노테이션을 붙여주면 유효성 검증이 진행된다. @Valid는 컨드롤러에서만 동작하는데, 불가피한 상황으로 다른 레이어에서 파라미터를 검증해야 할 수도 있다

@Service
@Validated
public class UserService {
	public void signup(@Valid UserDto userDto) {
    	...
    }
}

@Validated 그룹 지정

객체를 검증하는 방법이 달라질 수도 있다. 예를들어, 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) {
    ...
}

@Validated 동작원리

@Valid와 달리 @Valudated는 스프링의 AOP기반(관점 기반 모듈화)으로 메소드 요청을 인터셉터하여 처리한다. @Validated가 클래스 레벨에 선언되면(Controller에서는 생략 가능) 해당 클래스에 유효성 검증을 위한 인터셉터인 MethodValidationInterceptor가 등록되고, 해당 클래스의 메소드들이 호출 될 때 요청을 가로채 인터셉터를 통해 유효성 검증을 한다.

예외처리

@Valid -> MethodArgumentNotValidException이 발생한다.
@Validated -> ConstraintViolationException이 발생한다.

💥 @Valid를 이용한 예외처리 예제


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응답을 보낼수 있다.

💥 @Valid를 이용한 예외처리 TestCode


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 응답

0개의 댓글