[스프링 MVC - 2편] 검증2 - Bean Validation

지현·2022년 1월 3일
0

스프링

목록 보기
26/32

Bean Validation - 소개

  • validation 검증의 제약 조건을 애너테이션으로 넣는 것
  • Bean Validation은 특정한 구현체가 아니라 검증 애노테이션과 여러 인터페이스의 모음 > 구현체 따로 존재
  • Bean Validation을 구현한 기술중에 일반적으로 사용하는 구현체는 하이버네이트 Validator > 구현체는 바꿀 수 있음

Bean Validation - 시작

Bean Validation을 사용하려면 의존관계를 추가

build.gradle

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

검증 애노테이션

  • @NotBlank : 빈값 + 공백만 있는 경우 허용 X
  • @NotNull : null을 허용 X
  • @Range(min = 1000, max = 1000000) : 범위 안의 값
  • @Max(9999) : 최대 9999까지만 허용
  1. 검증 대상에 검증 애너테이션을 적용
  2. 검증기를 생성
  3. 검증기에 검증 대상을 넣고 결과를 받음

스프링과 통합하면 검증기를 생성하는 코드를 작성 할 필요는 없음


Bean Validation - 스프링 적용

  1. 스프링 부트가 spring-boot-starter-validation라이브러리를 넣으면 자동으로 Bean Validator를 인지하고 스프링에 통합
  2. 애너테이션을 보고 검증을 수행하는 LocalValidatorFactoryBean을 글로벌 Validator로 등록
  3. 글로벌 Validator가 적용되어 있기 때문에, @Validated @Valid를 사용하면 애너테이션 기반으로 검증하는 검증기가 자동으로 검증하고 결과를 bindingResult에 넣어줌
  • 직접 글로벌 Validator를 직접 등록하면 스프링 부트는 Bean Validator를 글로벌 Validator로 등록하지 않아 애노테이션 기반의 빈 검증기가 동작하지 않음 > 있으면 지워주기 !!
public String addItem(@Validated @ModelAttribute Item item, 
	BindingResult bindingResult, RedirectAttributes redirectAttributes) {
    
   ...
   
   }

검증 순서

  1. @ModelAttribute 각각의 필드에 타입 변환 시도
    • 성공하면 다음으로
    • 실패하면 typeMismatch 로 FieldError 추가
  2. 성공하면 Validator 적용

바인딩에 성공한 필드만 Bean Validation 적용

  • BeanValidator는 바인딩에 실패한 필드는 BeanValidation을 적용하지 않음
  • 일단 모델 객체에 바인딩 받는 값이 정상으로 들어와야 검증도 의미가 있기 때문에
  • @ModelAttribute > 각각의 필드 타입 변환시도 > 변환에 성공한 필드만 BeanValidation 적용

Bean Validation - 에러 코드

  • Bean Validation을 적용하고 bindingResult에 등록된 검증 오류 코드를 보면, 오류 코드가 애노테이션 이름으로 등록됨 > 마치 typeMismatch 와 유사
  • MessageCodesResolver를 통해 다양한 메시지 코드가 순서대로 생성
  • 예) @NotBlank
    • NotBlank.item.itemName
    • NotBlank.itemName
    • NotBlank.java.lang.String
    • NotBlank

기본으로 제공하는 오류 메시지를 좀 더 자세히 변경하고 싶으면,

errors.properties에 메세지를 등록

#Bean Validation 추가
NotBlank={0} 공백X 
Range={0}, {2} ~ {1} 허용
Max={0}, 최대 {1}
  • {0}은 필드 명을 의미

BeanValidation 메시지 찾는 순서

  1. 생성된 메시지 코드 순서대로 messageSource에서 메시지 찾기
  2. 애노테이션의 message 속성 사용 @NotBlank(message = "공백! {0}")
  3. 라이브러리가 제공하는 기본 값 사용 공백일 수 없습니다.

Bean Validation - 오브젝트 오류

  • Bean Validation에서 FieldError가 아닌 ObjectError는 @ScriptAssert() 를 사용해서 처리
@ScriptAssert(lang="javascript",script="_this.price * _this.quantity >= 10000 ", 
	message = "총합이 10000원 넘게 입력해주세요.")
  • 하지만 기능이 약해서 오브젝트 오류(글로벌 오류)의 경우 @ScriptAssert을 억지로 사용하는 것 보다는 오브젝트 오류 관련 부분만 직접 자바 코드로 작성하는 것을 권장
        //특정 필드가 아닌 복합 룰 검증
        if(item.getPrice()!=null&& item.getQuantity()!=null){
            int resultPrice = item.getPrice() * item.getQuantity();
            if (resultPrice<10000){
                bindingResult.reject(
                	"totalPriceMin",new Object[]{10000,resultPrice},null);
            }
        }

Bean Validation - groups

동일한 모델 객체를 등록할 때와 수정할 때 각각 다르게 검증하는 방법

  1. BeanValidation의 groups 기능을 사용
  2. Item을 직접 사용하지 않고, ItemSaveForm, ItemUpdateForm 같은 폼 전송을 위한 별도의 모델 객체를 만들어서 사용

BeanValidation groups 기능 사용

그룹을 체크하기 위한 체크 인터페이스(SaveCheck, UpdateCheck) 생성후 적용

item.class

public class Item {

    @NotNull(groups = UpdateCheck.class)
    private Long id;

    @NotBlank(groups = {SaveCheck.class,UpdateCheck.class})
    private String itemName;

    @NotNull(groups = {SaveCheck.class,UpdateCheck.class})
    @Range(min=1000, max=1000000, groups = {SaveCheck.class,UpdateCheck.class})
    private Integer price;

    @NotNull(groups = {SaveCheck.class,UpdateCheck.class})
    @Max(value = 9999,groups = {SaveCheck.class})
    private Integer quantity;

	...
    
}

ValidationItemControllerV3.class

 public String addItemV2(@Validated(SaveCheck.class) @ModelAttribute Item item,
 		BindingResult bindingResult, RedirectAttributes redirectAttributes) 
  • @Validated가 사용 되어 검증할 때 groups가 SaveCheck인 것만 적용됨
  • @Valid로는 groups 기능 사용 안됨

사실 groups 기능은 실제 잘 사용되지는 않음!!
실무에서는 주로 등록용 폼 객체와 수정용 폼 객체를 분리해서 사용


Form 전송 객체 분리 - 소개

데이터 전달에 Item 도메인 객체 사용
HTML Form -> Item -> Controller -> Item -> Repository

  • 장점 : Item 도메인 객체를 컨트롤러, 리포지토리 까지 직접 전달해서 중간에 Item을 만드는 과정이 없어서 간단
  • 단점 : 간단한 경우에만 적용할 수 있음
    수정시 검증이 중복될 수 있고, groups를 사용해야 함

폼 데이터 전달을 위한 별도의 객체 사용
HTML Form -> ItemSaveForm -> Controller -> Item 생성 -> Repository

  • 장점 : 전송하는 폼 데이터가 복잡해도 거기에 맞춘 별도의 폼 객체를 사용해서 데이터를 전달 받을 수 있음
    보통 검증이 중복되지 않음
  • 단점 : 폼 데이터를 기반으로 컨트롤러에서 Item 객체를 생성하는 변환 과정이 추가됨

  • 예를들어 등록과 수정의 경우 완전히 다른 데이터가 넘어오고 검증 로직도 많이 달라짐 -> 별도의 객체로 데이터를 전달받는 것이 좋음
  • 폼 데이터 전달을 위한 별도의 객체를 사용하고, 등록, 수정용 폼 객체를 나누면 등록, 수정이 완전히 분리되기 때문에 groups를 적용할 일은 거의 없음

Form 전송 객체 분리 - 개발

ItemSaveForm.class

@Data
public class ItemSaveForm {
    @NotBlank
    private String itemName;

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

    @NotNull
    @Max(value = 9999)
    private Integer quantity;
}

ItemUpdateForm.class

@Data
public class ItemUpdateForm {

    @NotNull
    private Long id;

    @NotBlank
    private String itemName;

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

    //수정에서는 수량은 자유롭게 변경할 수 있다.
    private Integer quantity;
}
  • 별도의 폼 객체를 생성

ValidationItemControllerV3.class

 public String addItem(@Validated @ModelAttribute("item") ItemSaveForm form, 
 	BindingResult bindingResult, RedirectAttributes redirectAttributes) {

	...
    
        // 폼 객체를 Item으로 변환
        Item item=new Item();
        item.setItemName(form.getItemName());
        item.setPrice(form.getPrice());
        item.setQuantity(form.getQuantity());

        Item savedItem = itemRepository.save(item);
        
        ...
        
    }
  • Item 대신에 ItemSaveform을 전달 받아 @Validated로 검증도 수행하고, BindingResult로 검증 결과를 받음
  • 폼 객체의 데이터를 기반으로 Item 객체를 생성하여 변환
  • @ModelAttribute("item")에 item을 넣어주지 않으면 규칙에 의해 itemSaveForm 이라는 이름으로 model에 저장됨 > 뷰 템플릿 수정해야함
  • Form 전송 객체를 분리해서 등록과 수정에 딱 맞는 기능을 구성하고, 검증도 명확히 분리

Bean Validation - HTTP 메시지 컨버터

@Valid, @Validated는 HttpMessageConverter(@RequestBody)에도 적용 가능

    @ResponseBody
    public Object addItem(@RequestBody @Validated ItemSaveForm form, 
    						BindingResult bindingResult){
        log.info("API 컨트롤러 호출");

        if(bindingResult.hasErrors()){
            log.info("검증 오류 발생 errors={}",bindingResult);
            return bindingResult.getAllErrors();
            //Field error, Object error 모두 반환
        }

        log.info("성공 로직 실행");
        return form;
    }

API의 3가지 경우

  • 성공 요청 : 성공
  • 실패 요청 : JSON을 객체로 생성하는 것 자체가 실패함 -->
    타입 오류 같은 경우 json차제를 객체로 만드는데 실패해서 컨트롤러 자체가 호출이 되지 않고 예외가 터짐
  • 검증 오류 요청 : JSON을 객체로 생성하는 것은 성공했고, 검증에서 실패함

@ModelAttribute vs @RequestBody

  • @ModelAttribute
    • 필드 단위로 정교하게 바인딩이 적용됨
    • 특정 필드가 바인딩 되지 않아도 나머지 필드는 정상 바인딩 되고, Validator를 사용한 검증도 적용 가능
  • @RequestBody
    • HttpMessageConverter 단계에서 JSON 데이터를 객체로 변경하지 못하면 이후
      단계 자체가 진행되지 않고 예외가 발생
    • 컨트롤러도 호출되지 않고, Validator도 적용할 수 없음



출처
[인프런] 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술

0개의 댓글