스프링에서 API를 만들때 어떤형태로 요청이 들어올지 우리는 모른다. 그렇게 때문에 들어오는 요청의 에대해서 유효성체크(validation)을 할필요성이 있다.

선언적인 방식의 유효성 검사기

  • Spring validator
  • Hibernate validator

  • @Valid : JSR-303 에 의해 만들어진 어노테이션 (현재는 JSR-380까지 나온상태)
  • @Validated : spring framework 의 만들어진 어노테이션

Package Tree

패키지 구조는 아래와 같다
자료는 저장소 바로가기 에서 다운받을수 있다.

STAR도 한번씩 눌러주시길 (굽신..)

├── main
│   ├── java
│   │   └── com
│   │       └── example
│   │           └── demo
│   │               ├── Controller.java
│   │               ├── DemoApplication.java
│   │               ├── domain
│   │               │   └── Person.java
│   │               ├── enums
│   │               │   └── CategoryType.java
│   │               ├── params
│   │               │   └── PersonParam.java
│   │               ├── request
│   │               │   └── PersonRequest.java
│   │               ├── service
│   │               │   └── PersonService.java
│   │               └── validator
│   │                   └── PersonValidator.java
│   └── resources
│       ├── application.properties
│       ├── static
│       └── templates
└── test
    └── java
        └── com
            └── example
                └── demo
                    ├── DemoApplicationTests.java
                    ├── PersonApiTest.java
                    └── PersonTest.java

Controller

/src/main/java/com/example/demo/Controller.java를 살펴보면 POST method로 지정되어있으며 json형태로 입력을 받고 있다. 또한 @Valid를 이용하여 요청이 들어오는 입력값에 대해서 검증을 실시하고 BindingResult필드를 통하여 error에대한 결과가 컨트롤러로 넘겨받는다. 필드를 이용하여 유효성실패에대한 적절한 분기처리를 할수있다.

@RestController
public class Controller {

    @RequestMapping(value = "/", method = RequestMethod.POST)
    @ResponseStatus(code = HttpStatus.CREATED)
    public PersonRequest getPerson(@RequestBody @Valid PersonRequest personRequest, BindingResult bindingResult){

        new PersonValidator().validate(personRequest, bindingResult);
        if(bindingResult.hasErrors()){
            bindingResult.getAllErrors()
                    .stream()
                    .forEach(s -> {
                        log.error("[{}] {}", s.getCode(), s.getDefaultMessage());
                    });

            return null;
        }

        return personRequest;
    }
}

PersonRequest

Person.class의 형태는 아래처럼 되어있으며 annotation으로 값이 없을때 그리고 최소값을 가져야하는 유효성 및 메세지를 지정하였다.

@Data
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Person {

    @NotEmpty(message = "이름을 입력해주세요.")
    private String name;

    @Min(value = 1, message = "나이는 0 이상입니다. ")
    private int age;

    @Builder
    public Person(@NotEmpty(message = "이름을 입력해주세요.") String name, @Min(value = 1, message = "나이는 0 이상입니다. ") int age) {
        this.name = name;
        this.age = age;
    }
}
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class PersonParam {

    private Long testLong;

    @NotNull(message = "정확한 카테고리값을 입력해주세요.")
    private CategoryType categoryType;

    @Builder
    public PersonParam(Long testLong, @NotNull(message = "정확한 카테고리값을 입력해주세요.") CategoryType categoryType) {
        this.testLong = testLong;
        this.categoryType = categoryType;
    }
}

Request 최종

위의 객체들을 합쳐서 PersonRequest에 몰아 넣어 각각 필드에 @Valid를 적용하여 각 객체속의 검증도 하도록 적용하였다.

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class PersonRequest {

    @Valid
    private PersonParam personParam;

    @Valid
    private Person person;

    @Builder
    public PersonRequest(@Valid PersonParam personParam, @Valid Person person) {
        this.personParam = personParam;
        this.person = person;
    }
}

Request Json Body

일단 정상적인 케이스를 시도해보겠다.

{
	"personParam":{
		"categoryType": "CAR"
	},
	"person": {
		"name": "dasda",
		"age": 2
	}
}

실행결과

여기서 주목해봐야할 값은 Respon Status 201로 성공했다는거 그리고 테스를 보다 쉽게 하기 위해서 들어온값 그대로 응답하였다. 실행결과

Validator Class

좀더 세밀하게 유효성검사를 하고 싶을땐 따로 클래스를 만들어 Validator를 상속받아 구현해줄수 있다.
파일위치 : /src/main/java/com/example/demo/validator/PersonValidator.java

public class PersonValidator implements Validator {
    @Override
    public boolean supports(Class<?> clazz) {
        return PersonRequest.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        PersonRequest personRequest = (PersonRequest) target;

        // TODO: 유효성검사

        //errors.reject("arthur", "이곳에걸렷구나");
    }
}

boolean supports(Class<?> clazz)는 유효성을 체크할려는 클래스에 대한 허용여부 True로 될시 아래의 void validate(Object target, Errors errors)로 진입하게 된다.
또한 유효성 체크시 적절하게 errors.reject()를 이용하여 컨트롤러에게 에러사실을 전달할수있다.

Fail Request Json

일부러 categoryType을 enum 범위에 없는 값을 입력하여 유효성 체크를 해보자

{
	"personParam":{
		"categoryType": "BIKE"
	},
	"person": {
		"name": "dasda",
		"age": 2
	}
}

Alt text 컨트롤러의 유효성 bindingResult.hasErrors()에 걸려 에러로그를 콘솔에 출력하고 있다.

나의 Validator Class 컨트롤러에 적용하기

Alt text 내가만든 유효성클래스를 컨트롤러에 적용하기 위해선 new PersonValidator().validate(personRequest, bindingResult); 호출해주면된다.

API Test Case

자 이제 최종적으로 테스트케이스를 작성해보자.
MockMvc 테스트를 하기위해서 의존성 주입을 한후 request객체를 만들어 build해준다.

@RunWith(JUnitPlatform.class)
@WebMvcTest(Controller.class)
public class PersonApiTest {

    @Autowired
    private MockMvc mvc;

    @Test
    @DisplayName("API MOCK Test")
    public void personAPI테스트() throws Exception {

        Person person = Person.builder()
                .age(32)
                .name("arthur")
                .build();

        PersonParam personParam = PersonParam.builder()
                .categoryType(CategoryType.CAR)
                .testLong(1L)
                .build();

        PersonRequest personRequest = PersonRequest.builder()
                .person(person)
                .personParam(personParam)
                .build();

        ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter();
        String json = ow.writeValueAsString(personRequest);

        mvc.perform(
                post("/")
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        .content(json)
        ).andExpect(status().isCreated())
        .andDo(print());
    }
}

Alt text 정상적으로 pass 된것을 확인했다. 응답결과값도 우리가 원하는 대로 넘어온걸 확인했다.

앞으로 controller, service등 validation을 적절하게 이용하여 잘 쓸수 있을거 같다.
또한 추가로 요청할때 enum에 없는 값들이 넘어왔을때 처리방법을 알아보겠다.