목차

개발 노트/Spring Boot

Spring boot - bean validation

천만일 2024. 1. 21. 15:55

Validation이란?

Controller는 클라이언트가 보낸 요청의 데이터를 검증할 책임을 갖습니다.

예를 들어 회원가입 로직을 처리하려면 다음과 같은 정보가 필요합니다.

{
    "email": String,
    "password": String,
    "nickname": String,
    "job": String
}

 

만약 클라이언트에서 email을 보내지 않고 다음과 같은 데이터를 보냈다면 어떨까요?

{
    "password": "password",
    "nickname": "닉네임",
    "job": "대학생"
}

이 경우 email 필드는 null로 처리될 것이고, 회원가입 로직을 처리할 수 없겠죠.

이와 같은 상황을 미리 방지하기 위해 요청의 데이터를 검증해야 합니다.

 

검증할 객체를 맵핑하자

먼저 검증을 하기 위해서는 요청의 담긴 데이터를 객체로 변환해야합니다.

 

일반적으로 많이 사용하는 방식은 @ModelAttribute, @RequestBody입니다.

둘 다 역할을 알아보겠습니다.

 

@ModelAttribute

요청 파라미터로 전달된 데이터들을 객체로 맵핑되도록 도와주는 어노테이션입니다.

요청 파라미터에 정의된 key 값을 객체의 멤버 변수와 맵핑하여 반환합니다.

유의할 점은 각 필드가 독립적으로 맵핑된다는 점입니다.

// /sign-up?email={이메일}&password={비밀번호}&nickname={닉네임}&job={직업}

@PostMapping("/sign-up")
public ResponseEntity<?> signUp(@ModelAttribute SignUpRequest signUpRequest)

 

@RequestBody

 

HttpMessageConverter에 의해 요청 바디의 JSON을 객체로 변환해 주는 어노테이션입니다.

JSON을 객체로 변환할 때는 MappingJackson2 HttpMessageConverter라는 HttpMessageConverter의 구현체가 작동합니다.

@PostMapping("/sign-up")
public ResponseEntity<?> signUp(@RequestBody SignUpRequest signUpRequest)

 

Bean Validation 활용

build.gradle에 다음 라이브러리를 추가합니다.

implementation 'org.springframework.boot:spring-boot-starter-validation’

 

이제 bean validation의 built-in constraint를 사용할 수 있습니다.

어떤 종류가 있는지는 공식 문서 링크를 통해 확인할 수 있습니다.

 

다음은 제가 임의로 생성한 회원가입 DTO입니다.

public class SignUpRequest {

  @NotEmpty
  private String email;
  @NotEmpty
  private String password;
    @NotEmpty
  @Size(min = 2, max = 20)
  private String nickname;
  private String job;

  public SignUpRequest(
      String email,
      String password,
      String nickname,
            String job
  ) {
    this.email = email;
    this.password = password;
    this.nickname = nickname;
    this.job = job;
  }
}

 

다음과 같이 맵핑하고자 하는 매개변수 앞에 @Valid 혹은 @Validated 어노테이션을 작성하면, 맵핑 후에 검사를 유효성 검사를 진행합니다.

@PostMapping("/sign-up")
public ResponseEntity<?> signUp(@Valid @RequestBody SignUpRequest signUpRequest) {

  // 로직 생략

  return ResponseEntity.ok(null);
}

 

유효성 검사를 실패하면 400 에러가 발생합니다.

// Spring Default Error Response
{
    "timestamp": "2024-01-18T04:37:38.666+00:00",
    "status": 400,
    "error": "Bad Request",
    "path": "/user/sign-up"
}

 

BindingResult

위와 같은 방식으로 유효성 검사를 하면, 400 에러가 발생한 원인을 파악할 수 없습니다.

 

Controller의 메서드에 BindingResult 매개변수를 추가하면 유효성 검사 도중 발견된 에러를 확인할 수 있습니다.

BindingResult 매개변수는 @ModelAttribute, @RequestBody, @RequestPart와 같은 어노테이션을 가진 매개변수와 함께 해야 합니다.

 

발생한 에러는 BindingResult.getAllErrors() 메서드를 통해 확인할 수 있습니다.

@PostMapping("/sign-up")
public ResponseEntity<?> signUp(@RequestBody @Valid SignUpRequest signUpRequest, BindingResult bindingResult) {
  if (bindingResult.hasErrors()) {
    return ResponseEntity.badRequest()
        .body(
            new ExceptionResponse("잘못된 요청입니다.")
        );
  }

  return ResponseEntity.ok(null);
}

 

유의 사항

DTO의 getter 혹은 setter를 열어두자.

MappingJackson2HttpMessageConverter는 변환할 객체의 타입에서 getter 혹은 setter를 찾아서 활용합니다. 따라서 getter, setter가 없는 필드는 맵핑이 되지 않고 null로 세팅되는 점을 유의해야 합니다.

 

 

@RequestBody 작동 중에 예외가 발생하면 controller 로직은 작동하지 않는다.

@RequestBody를 통해 JSON을 Java 객체로 변환하다가 실패하면 바로 예외를 던지고 DispatchServlet에서 처리됩니다.

따라서 controller 로직에는 도달하지 않습니다. (int → string과 같이 자동 형변환이 되는 것들은 허용해 줍니다.)

다음은 JSON → Java 객체로 변경해 주는 Jackson2HttpMessageConverter의 일부입니다.

HttpMessageConverter가 던진 예외는 DispatchServlet이 받아서 처리합니다.

// org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter
private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
    MediaType contentType = inputMessage.getHeaders().getContentType();
    Charset charset = getCharset(contentType);

    ObjectMapper objectMapper = selectObjectMapper(javaType.getRawClass(), contentType);
    Assert.state(objectMapper != null, () -> "No ObjectMapper for " + javaType);

    boolean isUnicode = ENCODINGS.containsKey(charset.name()) ||
            "UTF-16".equals(charset.name()) ||
            "UTF-32".equals(charset.name());
    try {
        // 생략
    }
    **catch (InvalidDefinitionException ex) {
        throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
    }**
    **catch (JsonProcessingException ex) {
        throw new HttpMessageNotReadableException("JSON parse error: " + ex.getOriginalMessage(), ex, inputMessage);
    }**
}

 

참고

 

@RequestBody vs @ModelAttribute

1. @RequestBody와 @ModelAttribute Controller.java @RequestBody와 @ModelAttribute는 클라이언트 측에서 보낸 데이터를 Java…

tecoble.techcourse.co.kr

 

Java Bean Validation 제대로 알고 쓰자

개발하면서 제일 중요하게 생각하는 것 중에 하나가 validation입니다. 개발하고 운영하다 보면 클라이언트로부터 입력받은 값의 오류로 발생하는 장애가 꽤 많습니다. 잘못된 값을 전달받아 즉시

kapentaz.github.io