5. 검증2 - Bean Validation

ys·2024년 1월 9일

Spring-mvc2

목록 보기
5/10

김영한 강사님의 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술을 듣고 정리한 내용입니다. 자세한 내용은 강의를 참고해주세요

  • 저번시간에 배운대로 오류처리를 하면 코드가 많아서 번거롭다
  • 스프링에서는 이런 검증 로직을 모든 프로젝트에서 적용할 수 있게 공통화Bean Validation이 있다
  • 구현체: Validator
  • 인테페이스 : Jakarta

Bean Validation 의존관계 추가

  • build.gradle에 추가!
implementation 'org.springframework.boot:spring-boot-starter-validation'
  • spring-boot-starter-validation 의존관계를 추가하면 라이브러리가 추가 된다.
  • jakarta.validation-api : Bean Validation 인터페이스
  • hibernate-validator 구현체

Bean-Validation(V3)

  • 이때, global영역에 Validator을 구현해놨으면 Bean Validator가 등록되지 않으므로 삭제해준다
  • 스프링 부트는 spring-boot-starter-validation로 인해 자동으로 @Bean Validator을 인지하고 스프링에 통합한다
  • 스프링은 LocalValidaotrFactoryBean글로벌 Validator로 등록해 놨는데
  • Validator가 @NotNull같은 에노테이션을 보고 검증을 수행한다
  • 컨트롤러에서 @Valid, @Validated가 적용된 객체를 검증하는데
  • 이때, 검증 오류가 발생하면, rejectValue에서 자동으로 FieldError, ObjectError을 생성해서 BindingResult에 담아준다

과정을 자세히 봐보면...

  1. 컨트롤러에서 @ModelAttribute 에노테이션으로 인해, request parameters를 객체의 필드에 넣어준다
  • 이때, 바인딩이 성공하면 -> 다음 단계
  • 바인딩 실패시 typeMismatch(스프링 꺼) -> field Error에 추가
  1. @Validator 적용
  • 바인딩이 성공된 객체@Validator을 적용해, 검증사항을 확인하는데
  • 만약 검증 오류가 발생하면 rejectValue에서 자동으로 FieldError, ObjectError을 생성해서 BindingResult에 담아준다

이제 코드를 확인해보자

Item

@Data
public class Item {
	private Long id;
    
	@NotBlank
	private String itemName;
 
	@NotNull
	@Range(min = 1000, max = 1000000)
	private Integer price;
 
	@NotNull
	@Max(9999)
	private Integer quantity;
	public Item() {
	}
	public Item(String itemName, Integer price, Integer quantity) {
	this.itemName = itemName;
	this.price = price;
	this.quantity = quantity;
	}
}
  • 이렇게 Bean Validation 에노테이션을 추가해준다
  • 이 에노테이션들은 -> 컨트롤러의 @Validate에서 확인돼, 검증된다

자세한 관련 공식 사이트

하이버네이트 Validator 관련 링크
공식 사이트: http://hibernate.org/validator/
공식 메뉴얼: https://docs.jboss.org/hibernate/validator/6.2/reference/en-US/html_single/
검증 애노테이션 모음: https://docs.jboss.org/hibernate/validator/6.2/reference/en-US/html_single/#validator-defineconstraints-spec


Bean-Validation의 에러코드를 수정해보자

  • Bean Validation을 적용하고 bindingResult 에 등록된 검증 오류 코드를 보자.
  • 오류 코드가 애노테이션 이름으로 등록된다. 마치 typeMismatch 와 유사하다.

오류 메시지에 대한 나의 인사이트???

  • 어라????
  • 오류 코드가, MessageCodesResolver에서 생성한 field Error랑 비슷한데???
  • 에노테이션이름.객체명.필드명 이렇게 생성해주네!
  • 그렇다면?? 우리가 지금까지 적용한거 처럼(자세한게 우선순위, 덜 자세한게 후 순위) 법칙을 이용해 볼까?

NotBlank.item.itemName = 상품 이름은 빈칸 없이 적어주세요!!!
NotBlank={0} 공백X

  • 이렇게 error.properties에 추가해준다!
  • 그러면 처음에는 공백 X로 나오던 오류 코드가, 메시지를 추가해주니, 상품 이름은 빈칸 없이 적어주세요!!! 이렇게 나오는 것을 볼 수 있다

Bean Validation - 오브젝트 오류

  • @ScrpitAssert()를 적용해, Bean Validator을 사용할 수 있다
@Data
@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >=
10000")
  • 하지만 최신 버전 스프링에서 지원하지 않고, 복잡한 Object Error에서는 사용하기 어렵다

    jdk8 ~ jdk14의 JVM 상에서 사용되는Nashorn 엔진은 javascript를 지원하는데, jdk14 이후 버전부터는 javascript가 지원되지 않는 GraalVM 을 사용한다고 합니다.스프링 부트 3이후에는 java 17 이상을 사용하는 것이 필수조건으로 되어있기 때문에, 더는 스프링 부트 3에서는 @ScriptAssert를 이용한 자바스크립트 표현식을 사용할 수 없다

  • 쓰지 말자~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!~!
  • 그럼 어떻게 Object Error 등록할까?
  • 자바 코드로 넣어주자!!
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
	//특정 필드 예외가 아닌 전체 예외
	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의 한계

  • 데이터의 등록할 때와, 수정할 때의 요구사항이 다를 수 있다...

  • 이런 요구사항이 추가 되었다고 생각해보자
  • 그러면 같은 Item 도메인으로는 둘 중 하나의 요구사항만 만족 시킬수 밖에 없다...
  • 그리고 실무에서는 html form 데이터에서 받은 파라미터(데이터)만으로 완전한 객체를 만드는 상황이 거의 없다
  • db에서 추가 정보를 가져오고.... 여러 고려 사항이 많음!!
  • 이 문제를 해결하기 위해서 2가지 방법이 있다
  1. BeanValidationgroups 기능
  2. Item객체를 직접 사용하지 않고, ItemSavedForm, ItemUpdateForm 같은 폼 전송을 위한 별도의 모델 객체를 만들어서 사용한다

BeanValidation의 Groups기능

  • 등록시에 검증할 기능과 수정시에 검증할 기능을 각각 그룹으로 나누어 적용할 수 있다
  • 이렇게 2가지 기능을 각각 인터페이스를 구현한다
  • 각 수정과, 등록 요청사항에 따라, BeanValidation 에노테이션 안에, groups라는 파라미터를 이용해 아까 만든 인터페이스의 클래스를 지정해준다
  • 둘다 사용하는 BeanValidation은 {}안에 모두 넣어주면 된다
  • 이렇게 Domain의 Item 클래스를 수정해주고
  • 컨트롤러의 검증을 하는 에노테이션인 @Validated안에 value파라미터 안에 인터페이스의 클래스를 지정해준다
  • 이렇게 해주면 등록과 수정시의 요청 사항을 다르게 적용할 수 있게 코드를 수정할 수 있다
  • @Valid에서는 groups기능을 지원하지 않는다
  • 그치만 groups는 실제 잘 사용하지 않는다
  • 뒤에 나오는 등록용 폼 객체, 수정용 폼 객체를 분리해서 사용한다!!!

Form 전송 객체 분리

  • 실무에서는 groups 를 잘 사용하지 않는데, 그 이유는 등록시 폼에서 전달하는 데이터가 Item 도메인 객체와 딱 맞지 않기 때문이다
  • 실무에서는 회원 등록시 회원과 관련된 데이터만 전달받는 것이 아니라, 약관 정보도 추가로 받는 등 Item 과 관계없는 수 많은 부가 데이터가 넘어온다
  • 복잡한 폼의 데이터를 컨트롤러까지 전달할 별도의 객체를 만들어서
    전달한다
  • 우리는 다음 2개의 Form에서 사용할 클래스를 만들 것이다
  • ItemSaveForm, ItemUpdateForm은 html form데이터에서 받을 때만 사용할 객체이기 때문에 컨트롤러 레벨까지만 사용할 것이다!!!
  • 화면과 웹에 특화면 클래스인 것이다
  • 따라서 다음의 경로에서 사용해준다!!!
  • 다시 도메인 객체인 ITEM을 원상 복귀 시켜준다
@Data
public class Item {
 private Long id;
 private String itemName;
 private Integer price;
 private Integer quantity;
}

ItemSaveForm - ITEM 저장용 폼

@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 - ITEM 수정용 폼

@Data
public class ItemUpdateForm {

    @NotNull
    private Long id;

    @NotBlank
    private String itemName;

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

    // 수정시에는 수량은 자유롭게 변경할 수 있다.
    private Integer quantity;
}
  • 이렇게 각각 폼을 만들고
  • 요청사항에 맞게 Bean Validation을 적용시켜준다

대망의 수정된 컨트롤러

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

        // 특정필드가 아닌 복합 룰 검증
        if (form.getPrice() != null && form.getQuantity() != null) {
            int resultPrice = form.getPrice() * form.getQuantity();
            if (resultPrice < 10000) {
                bindingResult.reject("totalPriceMin", new Object[]{100000, resultPrice}, null);
            }
        }

        if (bindingResult.hasErrors()) {
            log.info("errors={}", bindingResult);
            return "validation/v4/addForm";
        }
        //성공 로직
        Item item = new Item();
        item.setItemName(form.getItemName());
        item.setPrice(form.getPrice());
        item.setQuantity(form.getQuantity());

        Item savedItem = itemRepository.save(item);
        redirectAttributes.addAttribute("itemId", savedItem.getId());
        redirectAttributes.addAttribute("status", true);
        return "redirect:/validation/v4/items/{itemId}";
    }
    

    @PostMapping("/{itemId}/edit")
    public String editV2(@PathVariable Long itemId, @Validated @ModelAttribute("item") ItemUpdateForm form, BindingResult bindingResult) {

        // 특정필드가 아닌 복합 룰 검증
        if (form.getPrice() != null && form.getQuantity() != null) {
            int resultPrice = form.getPrice() * form.getQuantity();
            if (resultPrice < 10000) {
                bindingResult.reject("totalPriceMin", new Object[]{100000, resultPrice}, null);
            }
        }

        if (bindingResult.hasErrors()){
            log.info("errors={}",bindingResult);
            return "validation/v4/editForm";
        }

        Item itemParam = new Item(form.getItemName(), form.getPrice(), form.getPrice()); // 생성자를 오버라이딩 해놓은 이유

        itemRepository.update(itemId, itemParam);
        return "redirect:/validation/v4/items/{itemId}";
    }
}
  • @ModelAttribute("item") ItemSaveForm form이렇게 수정해서 객체를 방금 만든 요청 객체로 바꿔준다
  • 그리고 @ModdleAttributeitem을 명시해줘서 view코드를 수정하지 않는다
  • 그리고 DB나 레파지 토리에 저장할 때는, 로직이 Item객체를 저장하기 때문에
    1. 생성자로 넣어주거나
    2. setter로 넣어준다
  • 그럴려면 생성자의 규칙을 잘 오버로딩해서 -> 요청사항에 맞게 손쉽게 생성자를 사용하는 방안을 잘 생각해봐야 겠다

Bean Validation - HTTP 메시지 컨버터

  • 지금까지 @ModelAttribute를 이용해 HTTP 요청 파라미터를 처리했다
  • @RequestBody를 이용해 API요청에서도 Bean 검정을 사용해보자!!

실패인데 타입이 잘못 들어온다면??

  • 우리는 HttpMessageConverter가 json형식을 itemSavedForm형태로 form객체로 만들어 주었다
  • 그리고 나서 이 객체를 가지고 컨트롤러를 호출하였는데
  • 지금 이 요류는 타입 오류로 인해 itemSavedForm자체를 만들지 못하였고
  • 컨트롤러 또한 호출이 되지 않는다
  • 이럴땐, 스프링이 만들어 둔 검증을 사용한다
  • 컨트롤러가 호출되지 않으므로 우리가 지정한 Validator들 또한 실행되지 않는다

검증 오류 요청

  • 이 요류는 HttpMessageConverter가 json형식을 itemSavedForm형태로 form객체로 만들어 준게 성공하였다
  • 컨트롤러가 호출되고
  • 우리가 지정한 Validator들에서 검증 오류가 나는 경우이다
  • 실제 개발할 때는 이 객체들을 그대로 사용하지 말고, 필요한 데이터만 뽑아서 별도의 API 스펙을 정의하고 그에 맞는 객체를 만들어서 반환해야 한다.
  • 이 오류 강의는 8장에서 자세히 배운다
  • 지금은 API 방식에서도 Bean Validator을 사용할 수 있다를 알자!

@ModelAttribute vs @RequestBody

  1. HTTP 요청 파리미터를 처리하는 @ModelAttribute각각의 필드 단위로 세밀하게 적용된다.
  • 그래서 특정 필드에 타입이 맞지 않는 오류가 발생해도 나머지 필드는 정상 처리할 수 있었다.
  1. HttpMessageConverter@ModelAttribute다르게 각각의 필드 단위로 적용되는 것이 아니라, 전체 객체단위로 적용된다.
  • 따라서 메시지 컨버터의 작동이 성공해서 ItemSaveForm 객체를 만들어야 @Valid , @Validated 가 적용된다
  • HttpMessageConverter 단계에서 실패하면 예외가 발생한다. 예외 발생시 원하는 모양으로 예외를 처리하는 방법
    은 예외 처리 부분에서 다룬다
profile
개발 공부,정리

0개의 댓글