Spring Boot Validation 순서 정하기 & 테스트 코드

프론트에서 유효성 검사를 수행하지만, 백단에서도 유효성 검사를 수행해야 안전한 애플리케이션을 만들 수 있습니다.

Spring Boot의 Validation은 가장 많이 쓰이는 유효성 검사 라이브러리 입니다. 적용하는 방법은 구글링하면 수많은 포스팅이 존재하니 생략하고 바로 이슈 및 해결을 설명드리겠습니다.

 

public class UrlDto {

    public static class Request{

        @Data
        @Builder
        @NoArgsConstructor
        @AllArgsConstructor
        public static class Save{
            @NotBlank(message = "Link 필드는 필수입니다.")
            @Pattern(regexp="^((http(s?))\\:\\/\\/)([0-9a-zA-Z\\-]+\\.)+[a-zA-Z]{2,6}(\\:[0-9]+)?(\\/\\S*)?$",
                message = "Link 형식이 유효하지 않습니다.")
            private String url;
        }
    }

url을 검사하는 코드입니다. url의 Validation에는 '@NotBlank'(빈값 조사)와 '@Pattern'(http(s) url 정규표현식)이 적용되어 있습니다. 

이 상태로 Postman에서 테스트 해보면 아래의 결과가 뜹니다.

왼쪽 : RequestBody, 오른쪽 : ResponseBody

원했던 것은, 우선순위를 적용해서 하나의 검증이 실패하면 그 메시지만 리턴하고 검증을 통과하면 그 다음 항목을 검증하는 식으로 진행하고 싶었습니다. 그래서 1순위로 NotBlank를 검사하고, 2순위로 정규표현식을 검사해서 그에 맞는 메시지만을 리턴하게 만들어야 합니다. 수행한 방법은 아래순서와 같습니다.

 

 

1. 먼저 인터페이스를 생성해줍니다. 따로 만들면 파일이 늘어나니, 클래스 하나를 만들고 그안에 정의하는 방식을 선택했습니다.

public class ValidationGroups {
    public interface NotEmptyGroup {};
    public interface PatternCheckGroup {};
}

 

2. @GroupSequence를 사용하여 그룹별 인터페이스를 정의해줍니다.

왼쪽부터 유효성 검사를 체크해서 오류가 없으면 다음 유효성 검사를 하는 순서로 진행이 됩니다

Default.class -> NotEmptyGroup.class -> PatternCheckGroup.class

import foo.study.url.dto.ValidationGroups.NotEmptyGroup;
import foo.study.url.dto.ValidationGroups.PatternCheckGroup;
import javax.validation.GroupSequence;
import javax.validation.groups.Default;

@GroupSequence({Default.class, NotEmptyGroup.class, PatternCheckGroup.class, })
public interface ValidationSequence {
}

 

3. Validaton 어노테이션에서 각각 groups = "인터페이스명"을 추가해줍니다.

public class UrlDto {

    public static class Request{

        @Data
        @Builder
        @NoArgsConstructor
        @AllArgsConstructor
        public static class Save{
            @NotBlank(message = "Link 필드는 필수입니다.", groups = ValidationGroups.NotEmptyGroup.class)
            @Pattern(regexp="^((http(s?))\\:\\/\\/)([0-9a-zA-Z\\-]+\\.)+[a-zA-Z]{2,6}(\\:[0-9]+)?(\\/\\S*)?$",
                message = "Link 형식이 유효하지 않습니다.", groups = ValidationGroups.PatternCheckGroup.class)
            private String url;
        }

 

 

4. 기존에는 @Valid를 사용했지만, 그룹 시퀀스를 지정하기 위해 @Validated 어노테이션을 사용합니다.

    @PostMapping("/save")
    public Response.FindOne<String> save( @Validated(ValidationSequence.class) @RequestBody UrlDto.Request.Save dto){
        return FindOne.<String>builder()
            .data(
                urlService.save(dto.getUrl()
            )
        ).build();
    }

 

 

5. 테스트

url 빈값

 

 

http 정규표현식 불일치

 

 

테스트 코드는 다음과 같습니다.

class UrlDtoTest {

    private static ValidatorFactory factory;
    private static Validator validator;

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

    @DisplayName("url에 빈값 전송시 에러발생")
    @Test
    void save_notblank_validation(){
        Request.Save saveDto = Request.Save.builder().url("").build();

        // 유효하다면 violations는 빈 값, 유효하지 않다면 값을 가지고 있음.
        Set<ConstraintViolation<Request.Save>> violations = validator.validate(saveDto, ValidationGroups.NotEmptyGroup.class);

        violations
            .forEach(error -> {
                assertThat(error.getMessage()).isEqualTo("Link 필드는 필수입니다.");
            });
    }

    @DisplayName("url에 http 표현식이 아닌 값 전송시 에러발생")
    @Test
    void save_pattern_validation(){
        Request.Save saveDto = Request.Save.builder().url("not http expressions").build();

        Set<ConstraintViolation<Request.Save>> violations = validator.validate(saveDto, ValidationGroups.PatternCheckGroup.class);

        violations
            .forEach(error -> {
                assertThat(error.getMessage()).isEqualTo("Link 형식이 유효하지 않습니다.");
            });
    }

}

 

 

적용한 프로젝트는 깃허브를 확인해주세요.

 

참고

okky.kr/article/381626

okky.kr/article/435590

stackoverflow.com/questions/11804879/error-messages-are-not-in-the-correct-order

velog.io/@damiano1027/Spring-Valid-Validated%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%9C%A0%ED%9A%A8%EC%84%B1-%EA%B2%80%EC%A6%9D

javapointers.com/spring/spring-mvc/spring-mvc-validation-order-example/

www.notion.so/lightblog/Bean-Validation-2f70a3f0aae94621886487477097abfa

 

테스트 코드

discourse.hibernate.org/t/groupsequence-combined-with-valid-doest-work-in-all-cases/840

velog.io/@tigger/DTO-%EA%B2%80%EC%A6%9D-%EB%B0%8F-%ED%85%8C%EC%8A%A4%ED%8A%B8

m.blog.naver.com/varkiry05/222058714706