[SpringBoot] @Valid를 이용한 유효성 검증

애이용·2021년 2월 22일
1

springboot

목록 보기
11/20
post-thumbnail

@Valid

RestController를 이용해 @RequestBody 객체를 사용자로부터 가져올 때, 데이터가 유효한지(ex) 누락, 최대 크기 초과 등) 검증할 수 있다. (@Valid 또는 @Validated 어노테이션)

  • 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-validation'

TEST API를 작성해보자

@PostMapping("/user")
public ResponseEntity<ResponseDto> signIn(@Valid @RequestBody SignInDto SignInDto){
    // ...
}

파라미터에 @RequestBody와 함께 @Valid를 사용하면, RequestBody로 들어오는 객체에 대한 검증을 수행한다.
검증의 세부적인 사항은 객체 안에 정의해둬야 한다.

@AllArgsConstructor
@Getter
public class SignInDto {
    @NotBlank
    @Email(message = NOT_VALID_EMAIL)
    private String email;
    @NotBlank
    private String password;
}

✔ 문자열 유무 검증

  • @NotNull
    null이 아닌 값
    인자로 들어온 필드 값에 null을 허용하지 않는다.
  • @NotBlank
    null이 아닌 값(공백이 아닌 문자를 하나 이상 포함해야 한다.)
  • @Null
    null 값
  • @NotEmpty
    null이거나 empty(빈 문자열)가 아니어야 한다.
  • @Email
    입력한 문자열이 이메일 형식이어야 한다.
    이것만 쓰인다면 email이 null이거나 빈문자열("")인 경우, 유효성 검사가 통과하게 된다.
    @NotBlank와 같이 쓰면 유효성 검사를 확실히 할 수 있다.

✔ 숫자 범위 검증

@Min(value = 2, message = NOT_VALID_STATE)
@Max(value = 4, message = NOT_VALID_STATE)
private int state;

state가 2보다 작거나 4보다 크면 NOT_VALID_STATE(상수) 메세지가 응답되도록 한다.

응답 예시

{
  "message": " Invalid Input Value",
  "status": 400,
  "errors": [
    {
      "field": "bucketState",
      "value": "1",
      "reason": "유효한 버킷 state가 아닙니다."
    }
  ],
  "code": "C001"
}

✔ 응답 메세지

위에서 작성한 것처럼 message 를 이용해 응답 메세지를 설정할 수 있다.

따로 설정하지 않으면 default message가 응답된다.
ex) Email : 올바른 형식의 이메일 주소여야 합니다

✔ 예외 처리

    /**
     * javax.validation.Valid or @Validated 으로 binding error 발생 시 발생
     * HttpMessageConverter 에서 등록한 HttpMessageConverter binding 못할 경우 발생
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    protected ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error("MethodArgumentNotValidException : " + e.getMessage());
        final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult());
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

✔ 테스트 코드 작성

DTO 테스트

class SignInDtoTest {
    private static ValidatorFactory factory;
    private static Validator validator;

    @BeforeAll
    public static void init() {
        factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
    }

    @AfterAll
    public static void close() {
        factory.close();
    }

    @DisplayName("email에 빈문자열 전송 시 에러 발생")
    @Test
    void 빈문자열_유효성_실패_테스트() {
        // given
        SignInDto signInDto = new SignInDto("", "test");

        // when
        Set<ConstraintViolation<SignInDto>> violations = validator.validate(signInDto); // 유효하지 않은 경우 violations 값을 가지고 있다.

        // then
        assertThat(violations).isNotEmpty();
        violations
                .forEach(error -> {
                    assertThat(error.getMessage()).isEqualTo("공백일 수 없습니다");
                });
    }

    @DisplayName("이메일 형식 아닌 경우 에러 발생")
    @Test
    void 이메일_형식_유효성_실패_테스트() {
        // given
        SignInDto signInDto = new SignInDto("test", "test");

        // when
        Set<ConstraintViolation<SignInDto>> violations = validator.validate(signInDto);

        // then
        assertThat(violations).isNotEmpty();
        violations
                .forEach(error -> {
                    assertThat(error.getMessage()).isEqualTo(NOT_VALID_EMAIL);
                });
    }

    @DisplayName("비밀번호 빈문자열인 경우 에러 발생")
    @Test
    void 비밀번호_유효성_실패_테스트() {
        // given
        SignInDto signInDto = new SignInDto("test@gmail.com", "");

        // when
        Set<ConstraintViolation<SignInDto>> violations = validator.validate(signInDto);

        // then
        assertThat(violations).isNotEmpty();
        violations
                .forEach(error -> {
                    assertThat(error.getMessage()).isEqualTo("공백일 수 없습니다");
                });
    }


    @DisplayName("유효성 성공")
    @Test
    void 유효성_성공_테스트() {
        // given
        SignInDto signInDto = new SignInDto("test@gmail.com", "test");

        // when
        Set<ConstraintViolation<SignInDto>> violations = validator.validate(signInDto);

        // then
        assertThat(violations).isEmpty(); // 유효한 경우
    }
}

컨트롤러 테스트 (이전에 작성한 코드)

@RunWith(SpringRunner.class) 
@WebMvcTest(controllers = TestController.class)
public class TestControllerTest{

    @Autowired 
    private MockMvc mvc; 

    @Autowired
    private ObjectMapper objectMapper;
    
    @Test // NotNull 테스트(bad request)
    public void isValidName() throws Exception {
        UserRequestDto user = UserRequestDto.builder()
                .email("ayong703@gmail.com")
                .build();

        String userRequestDtoJsonString = objectMapper.writeValueAsString(user);

        mvc.perform(post("/user")
                .contentType(MediaType.APPLICATION_JSON)
                .content(userRequestDtoJsonString)) // Request Body
                .andExpect(status().isBadRequest());
    }

    @Test // Email 테스트(bad request)
    public void isValidEmail() throws Exception {
        UserRequestDto user = UserRequestDto.builder()
                .name("문아영")
                .email("ayong703@")
                .build();

        String userRequestDtoJsonString = objectMapper.writeValueAsString(user);

        mvc.perform(post("/user")
                .contentType(MediaType.APPLICATION_JSON)
                .content(userRequestDtoJsonString))
                .andExpect(status().isBadRequest());
    }

    @Test // 성공 테스트
    public void isValidData() throws Exception {
        UserRequestDto user = UserRequestDto.builder()
                .name("문아영")
                .email("ayong703@gmail.com")
                .build();

        String userRequestDtoJsonString = objectMapper.writeValueAsString(user);

        mvc.perform(post("/user")
                .contentType(MediaType.APPLICATION_JSON)
                .content(userRequestDtoJsonString))
                .andExpect(status().isOk());
    }
}

참고 링크

profile
로그를 남기자 〰️

0개의 댓글