본문 바로가기
공부/프로그래밍

[springboot] @Valid 를 이용해 request 시 필수파라미터 체크하기

by demonic_ 2020. 6. 23.
반응형

spring으로 웹 or API 서버를 구축하다보면 필수 파라미터를 체크해야 하는 부분이 있다.

이전까지만 해도 Controller 나 Service에 파라미터를 직접 체크하는 로직을 구현했었는데, Spring에서 제공하는 Validation을 이용하면 쉽게 유효검증이 가능하다는 것을 알았다.

 

이참에 관련 내용을 정리하고자 한다.

여기서는 gradle, springboot를 이용했으며 테스트는 junit5를 이용했다.

 

 

gradle 설정

dependencies {
    ...
    implementation 'org.springframework.boot:spring-boot-starter-web'
    // validation 추가
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    // test(junit5)
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}

 

 

Request를 오브젝트 생성

변수 위에 @NotBlank를 붙였는데, 저렇게 붙인것에만 필수값을 체크한다.

그밖에 @Size, @Min, @Max등 수치나 갯수를 체크하는 것도 있으니 용도에 맞게 가져다 쓰자.

@ToString
@Getter
public class PostTestRequest {
    @NotBlank(message = "이름은 필수값 입니다.")
    private String name;
    
    @Min(value = 10, message = "10살 이후부터 가입가능합니다.")
    private int age;
    
    private String address;
}

 

 

컨트롤러 생성

Post이기 때문에 @RequestBody 를 붙였으며, 유효성검증을 위해 @Valid를 붙였다.

만약 @Valid를 붙이지 않는다면 유효성 검사를 하지 않는다.

@RestController
public class TestController {
    @PostMapping("/test/post")
    public String testPost(@RequestBody @Valid PostTestRequest request) {
        System.out.println("request = " + request);

        return "testPost";
    }
}

 

 

테스트코드 작성

일부로 이름은 빈값을, 그리고 나이를 10살 미만인 5로 설정해서 전송한다.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class TestControllerTest {
    @Autowired
    TestRestTemplate testRestTemplate;

    @Test
    @DisplayName("post parameter에 필수값 체크")
    void testPostTest() {
        // given
        String url = "/test/post";
        HashMap params = new HashMap();
        params.put("age", 5);
        params.put("address", "서울");

        // when
        ResponseEntity<String> entity = testRestTemplate.postForEntity(url, params, String.class);

        // then
        System.out.println("entity = " + entity);
    }
}

 

결과확인

entity = <400,{"timestamp":"2020-06-22T23:28:28.944+0000","status":400,"error":"Bad Request","errors":[{"codes":["NotBlank.postTestRequest.name","NotBlank.name","NotBlank.java.lang.String","NotBlank"],"arguments":[{"codes":["postTestRequest.name","name"],"arguments":null,"defaultMessage":"name","code":"name"}],"defaultMessage":"이름은 필수값 입니다.","objectName":"postTestRequest","field":"name","rejectedValue":null,"bindingFailure":false,"code":"NotBlank"},{"codes":["Min.postTestRequest.age","Min.age","Min.int","Min"],"arguments":[{"codes":["postTestRequest.age","age"],"arguments":null,"defaultMessage":"age","code":"age"},10],"defaultMessage":"10살 이후부터 가입가능합니다.","objectName":"postTestRequest","field":"age","rejectedValue":5,"bindingFailure":false,"code":"Min"}],"message":"Validation failed for object='postTestRequest'. Error count: 2","path":"/test/post"},[Content-Type:"application/json", Transfer-Encoding:"chunked", Date:"Mon, 22 Jun 2020 23:28:28 GMT", Connection:"close"]>

 

로그를 보면 이름과 나이 둘다 문제가 있다고 보이는데, 이처럼 잘못된 파라미터에 한해서 List에 담아서 보내준다.

 

여기에 ExceptionHandler를 추가등록해서 result포멧을 통일시켜주면 좋을거 같다.

@ControllerAdvice(basePackages = "kr.sample.demo.controller")
public class ControllerAdviceHandler {
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public Map<String, Object> handleValidationExceptions(
            MethodArgumentNotValidException ex) {
        Map<String, Object> result = new HashMap<>();
        result.put("code", "ERROR");

        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        result.put("errors", errors);

        return result;
    }
}

 

테스트를 실행하면 다음과 같은 결과를 확인할 수 있다.

entity = <400,{"code":"ERROR","errors":{"name":"이름은 필수값 입니다.","age":"10살 이후부터 가입가능합니다."}},[Content-Type:"application/json", Transfer-Encoding:"chunked", Date:"Mon, 22 Jun 2020 23:42:49 GMT", Connection:"close"]>

 

이번엔 mock을 이용해 테스트

(junit5 는 @Runwith대신 @ExtendWith를 사용한다.)

@ExtendWith(SpringExtension.class)
@WebMvcTest
@AutoConfigureMockMvc
class TestControllerTestV2 {

    @MockBean
    private TestController testController;

    @Autowired
    private MockMvc mockMvc;

    @Test
    @DisplayName("post 필수값 체크 테스트")
    void testPostTest2() throws Exception {
        // given
        String url = "/test/post";
        String test = "{\"age\":5, \"address\": \"서울\"}";


        // when
        MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post(url)
                .content(test)
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().isBadRequest())
                .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
                .andReturn();

        // 기본 캐릭터셋이 ISO-8859-1 임을 확인
        System.out.println("mvcResult = " + mvcResult.getResponse().getCharacterEncoding());
        // 캐릭터셋 설정
        String content = mvcResult.getResponse().getContentAsString(Charset.forName("UTF8"));
        System.out.println("content = " + content);

        // 테스트 결과 비교
        Assertions.assertThat(content).contains("이름은 필수값 입니다");
    }
}

 

테스트 결과

끝.

 

 

참고:

https://www.baeldung.com/spring-boot-bean-validation

 

Validation in Spring Boot | Baeldung

Learn how to validate domain objects in Spring Boot using Hibernate Validator, the reference implementation of the Bean Validation framework.

www.baeldung.com

https://lemontia.tistory.com/934

 

[springboot] ControllerAdvice 응용해 return 꾸미기(HttpStatus 지정 포함)

WEB으로 구성한 서버에서 Exception을 활용해서 에러코드를 핸들링하는 경우가 있는데, 이럴때 ControllerAdvice를 쓴다. 예를들어 Exception을 날리면 ControllerAdvice에서 캐치하고, 데이터를 가공해 리턴하�

lemontia.tistory.com

 

반응형

댓글