-> 블로그 이전

[Spring] 검증(Validation) 2. Bean Validation

2022. 7. 3. 21:13Language`/Spring

 

[Spring] 검증(Validation) 1. 직접 구현하기

MVC 패턴에서 Controller는 request로 들어온 URL에 대한 처리도 하지만, HTTP Request 자체가 정상인지 "검증"하는 역할도 수행한다 물론 Front쪽에서 검증을 할 수 있지만, 이는 Client가 의도적인 조작을 통

cs-ssupport.tistory.com

앞에서 검증 기능을 직접 작성함으로써 객체에 대한 검증을 하였다

그런데 검증 기능을 매번 코드로 작성하려면 엄청난 시간과 노력을 투자해야 한다

 

그리고 대부분의 검증 로직은 {빈 값인지 아닌지 / 특정 크기를 넘는지 아닌지 / ...} 등 매우 일반적인 로직이다

 

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

>> Bean Validation을 활용하면 "애노테이션" 하나로 검증 로직을 매우 편리하게 적용할 수 있다

 

Bean Validation을 구현한 기술중에서 일반적으로 사용하는 구현체는 Hibernate Validator이다
 

Hibernate Validator 6.2.3.Final - Jakarta Bean Validation Reference Implementation: Reference Guide

Validating data is a common task that occurs throughout all application layers, from the presentation to the persistence layer. Often the same validation logic is implemented in each layer which is time consuming and error-prone. To avoid duplication of th

docs.jboss.org

 


@Data
@NoArgsConstructor
@AllArgsConstructor
public class ItemDTO {
    @NotBlank
    private String itemName;

    @NotNull
    @Range(min = 1000, max = 1000000000)
    private Integer price;

    @NotNull
    @Min(5)
    private Integer quantity;
}
@NotBlank : 빈값 + 공백의 경우 에러처리
@NotNull : null일 경우 에러처리
@Range : {min ≤ "value" ≤ max}
@Min : 최소 값
@Max : 최댓값
....

 

Bean Validation 검증 순서

1. @ModelAttribute를 통해서 각 Request Data를 객체에 매핑시켜서 넣어주기

  • 성공 : next level
  • 실패 : typeMissmatch로 FieldError 추가

2. Validator 적용

과정 (1)에서 typeMissmatch가 발생하게되면 애초에 타입이 맞지 않는 data가 들어온것이기 때문에 Validator가 동작하지 않는다

 

 

Bean Validation 에러 메시지 찾는 순서

1. messageSource에서 메시지 찾기

  • ~~~.properties에 설정한 메세지들

2. Annotation에 설정한 message

3. 라이브러리가 제공하는 기본 값

 

 


Bean Validation - groups

일반적으로 회원 등록/회원 수정의 경우 검증을 다르게 해야 한다

  • 회원 등록은 모든 form data에 대해서 검증을 해야하지만, 회원 수정은 일반적으로 특정 data를 변경하기 때문에

 

이 때 groups라는 속성을 사용하면 서로 다른 타입의 검증을 시도할 수 있다

public interface ItemSaveForm {
}
public interface ItemEditForm {
}

일단 groups를 지정하는데 사용할 2개의 인터페이스를 만들어주자

 

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Item {
    private Long id;

    @NotBlank(groups = {ItemSaveForm.class, ItemEditForm.class})
    private String itemName;

    @NotNull(groups = {ItemSaveForm.class, ItemEditForm.class})
    @Range(min = 1000, max = 1000000000, groups = {ItemSaveForm.class, ItemEditForm.class})
    private Integer price;

    @NotNull(groups = {ItemSaveForm.class, ItemEditForm.class})
    @Min(value = 5, groups = ItemSaveForm.class)
    private Integer quantity;
}
<itemName>
@NotBlank : 등록/수정 둘다 동일하게 검증

<price>
@NotNull : 등록/수정 둘다 동일하게 검증
@Range : 등록/수정 둘다 동일하게 검증

<quantity>
@NotNull : 둥록/수정 둘다 동일하게 검증
@Min : 등록할 때만 검증 & 수정할 때는 검증 X

 

@PostMapping("/add")
public String addItem(
        @Validated(ItemSaveForm.class) @ModelAttribute Item item,
        BindingResult bindingResult
)

@PostMapping("/{itemId}/edit")
public String editItem(
        @PathVariable Long itemId,
        @Validated(ItemEditForm.class) @ModelAttribute Item item,
        BindingResult bindingResult
)

이처럼 검증을 서로 다르게 수행할 수 있다

 


Bean Validation - 검증용 DTO

실무에서는 거의 원래 도메인이 아닌 검증용 DTO를 따로 만들어서 검증 로직을 작성한다

왜냐하면 각 상황마다 검증할 data가 다르고 이를 도메인에 그대로 검증을 시켜버리면 원하는 결과를 얻기 힘들기 때문이다

 

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Item {
    private Long id;

    private String itemName;

    private Integer price;

    private Integer quantity;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ItemDTO {
    @NotBlank
    private String itemName;

    @NotNull
    @Range(min = 1000, max = 1000000000)
    private Integer price;

    @NotNull
    @Min(5)
    private Integer quantity;
}

@RequestMapping("/items")
public String addItem(
        @Validated @ModelAttribute ItemDTO item,
        BindingResult bindingResult

이처럼 아예 따로 만들어서 검증할 대상 객체도 검증용 DTO로 설정하면 된다

 

<Domain 객체 그대로 사용>
장점 : 중간에 따로 parsing을 해서 만들 필요가 없이 그대로 사용하면 된다
단점 : 간단한 경우에만 적용이 가능하고, 검증이 중복되면 groups를 사용해야 한다

<검증용 객체 따로 생성>
장점 : 전송받는 폼 데이터가 복잡해도 거기에 맞는 별도의 폼 객체를 사용해서 데이터를 받을 수 있다
단점 : parsing과정이 추가된다

 


Bean Validation - HTTP Message Converter

@Valid, @Validate는 @RequestBody & HttpEntity에도 적용할 수 있다

 

HTTP API가 들어오는 경우는 크게 3가지로 분류할 수 있다

 

1. 요청 : 실패

이 경우는 JSON → 객체로 변환하는 자체가 실패한 것이다

  • 데이터 type 불일치로 인해서 Client가 JSON Data를 잘못 보낸 것이다

 

2. 요청 : 검증 오류

이 경우는 JSON → 객체로 변환은 성공했지만, 내부의 값들에 대한 검증이 실패한 경우이다

 

3. 요청 : 성공

이 경우 JSON → 객체로 변환도 성공하고 내부 값들에 대한 검증도 성공한 경우이다

 

 

@ModelAttribute vs @RequestBody

@ModelAttribute는 field단위로 검증이 적용된다. 따라서 어느 field의 type이 맞지 않는 경우에도 다른 field에게 영향을 주지 않는다

 

반면에 @RequestBody는 HTTP API(JSON)으로 데이터가 들어오는 경우이고 일단 검증을 하려면 먼저 "JSON → 객체"로 변환이 먼저 되어야 한다

근데 여기서 어느 field의 type이 맞지 않는 경우는 애초에 객체로 변환이 실패하기 때문에 검증은 들어갈 수도 없다

 

따라서 API 요청에 대한 검증을 하려면 일단 먼저 JSON → 객체로의 변환이 성공됨을 전제로 해야 한다

만약 변환이 실패하였다면 애초에 Client가 Data를 잘못보낸것이기 떄문에 4xx Error를 return해주면 된다