요청받은 데이터를 검증하는 이유

  1.  Client 개발자가 깜빡하거나 실수로 값을 안보낼 수도 있다.
  2.  Client bug 로 값이 누락될 수 있다.
  3.  외부에 나쁜 사람이 값을 임의로 조작해서 보낼 수 있다.
  4.  DB 에 값을 저장할 때 의도치 않은 오류가 발생할 수 있다.

위처럼 여러가지 이유로 서버 개발자의 불안한 마음을 편하게 하기 위해서 데이터 검증을 해야한다.

검증 없이는 아래와 같이 title 에 빈 문자열( "" ) 을 넣어도 테스트는 통과한다.

 

PostControllerTest

    @DisplayName("/posts 요청 시 title 값은 필수다.")
    @Test
    void post() throws Exception {
        mockMvc.perform(
                MockMvcRequestBuilders.post("/posts")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content("{\"title\" : \"\", \"content\" : \"글 내용입니다 하하\"}")
            )
            .andExpect(status().isOk())
            .andExpect(content().string("Hello"))
            .andDo(print());

    }

 

하지만 이는 제목에 빈값을 허용하지 않는 개발자의 의도와 다르기 때문에 우리는 데이터 검증을 해야한다.

 


검증은 어떻게 할까?

컨트롤러에 params 객체를 통해 전달된 title 값을 검증하는 로직을 작성해보자. 얼추 아래처럼 될 것이다. 

위와 동일한 테스트를 시행해보자.

 

PostController

@PostMapping("/posts")
public String post(@RequestBody PostCreate params) throws Exception {
    log.info("params = {}", params.toString());
    String title = params.getTitle();
    if (title == null || title.equals("")) {
        throw new Exception("타이틀 값이 없어요!");
    }
    String content = params.getContent();
    if (content == null || title.equals("")) {
        // error
    }
    return "Hello";
}

 

예상대로 예외가 터진다. 하지만 이 방법은 많은 문제점을 가지고 있다.

 

  1.  우선 검증이 필요한 필드마다 노가다로 검증 코드를 작성해야해서 빡세다.
  2.  누락될 염려가 있다. (만약 필드가 100가지라면?)
  3.  생각보다 검증해야할 게 많다 (꼼꼼하지 않을 수 있다)
    ex) 만약 입력된 title 값이 공백이 포함된 빈 문자열(" ") 이라면? 혹은 수십억글자라면?

특히 3번째가 가장 중요한데, 필드가 늘어감에 따라 코드 줄 수가 늘어나면 모든 예외 사항을 확인하기는 매우 힘들다.

무언가 3번 이상 반복 작업을 할 때 내가 뭔가 잘못하고 있는건 아닐지 의심해봐야한다.

 


BeanValidation

검증 로직은 우리만 고민하는게 아닌 많은 개발자들이 꾸준히 고민해오던 문제이다.

이 검증 로직을 모든 프로젝트에 적용할 수 있게 공통화하고 표준화 한 것이 바로 Bean Validation 이다.

BeanValidation 에 관련된 내용은 여기에 정리해두었다.

BeanValidation 을 적용하여 컨트롤러와 테스트를 작성해보자.

 

PostController

@Valid 와 BindingResult 를 적용하여 PostCreate 객체에 바인딩 시 오류가 발생해도 컨트롤러가 호출된다.

BindingResult 에 검증 오류값이 저장되면 해당 필드명에러 메시지를 map 에 넣어서 응답으로 반환한다.

@PostMapping("/posts")
public Map<String, String> post(
			@RequestBody @Valid PostCreate params, 
			BindingResult result
			) throws Exception {
            
    if (result.hasErrors()) {
        List<FieldError> fieldErrors = result.getFieldErrors();
        FieldError fieldError = fieldErrors.get(0);
        String fieldName = fieldError.getField();
        String errorMessage = fieldError.getDefaultMessage();

        Map<String, String> error = new HashMap<>();
        error.put(fieldName, errorMessage);
        return error;
    }
    return Map.of();
}

 

PostCreate

DTO 의 검증하고자 하는 필드에 BeanValidation 에서 제공하는 검증 애노테이션을 사용한다.

여기서는 빈값 / 공백(" ")만 있는 경우를 허용하지 않는 @NotBlank 를 사용하고 defaultMessage 를 설정했다.

@Setter
@Getter
@ToString
public class PostCreate {

    @NotBlank(message = "제목을 입력해주세요.")
    private String title;

    @NotBlank(message = "내용을 입력해주세요.")
    private String content;

    public PostCreate(String title, String content) {
        this.title = title;
        this.content = content;
    }
}

 


PostControllerTest

title 값에 null 을 입력하면 응답 값으로 필드명("title")에러 메시지가 넘어오는지 확인하는 테스트이다.

    @DisplayName("/posts 요청 시 title 값은 필수다.")
    @Test
    void post() throws Exception {
        mockMvc.perform(
                MockMvcRequestBuilders.post("/posts")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(
                    "{\"title\" : null, \"content\" : \"글 내용입니다 하하\"}"
                    )
            )
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.title").value("제목을 입력해주세요."))
            .andDo(print());
    }

 

테스트를 돌려보면 성공한다.

MockMvc 의 jsonPath 를 이용하면 JSON 을 검증하는 더 풍부한 테스트를 작성할 수 있다.

jsonPath 로 검증하는법은 더 공부해봐야겠다.

+ Recent posts