ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [스프링MVC - 2편] 검증 (3)
    Spring&SpringBoot 2025. 3. 2. 15:37

    Bean Validation

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

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

     

     

    Item - Bean Validation 애노테이션 적용

    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;
        
        //...
    }

    검증 애노테이션

    • @NotBlank : 빈값 + 공백만 있는 경우를 허용하지 않는다
    • @NotNull : null을 허용하지 않는다
    • @Range(min = 1000, max = 1000000) : 범위 안의 값이어야 한다
    • @Max(9999) : 최대 9999까지만 허용한다

     

     

    Bean Validation 이란?

    먼저 Bean Validation 특정한 구현체가 아니라 Bean Validation 2.0(JSR-380)이라는 기술 표준이다

    쉽게 이야기해서 검증 애노테이션과 여러 인터페이스의 모음이다

    Bean Validation 구현한 기술중에 일반적으로 사용하는 구현체는 하이버네이트 Validator이다

     

     

    하이버네이트 Validator 관련 링크

     

     

    Bean Validation 의존관계 추가

    // build.gradle
    implementation 'org.springframework.boot:spring-boot-starter-validation'

     

     

    BeanValidationTest

    public class BeanValidationTest {
        @Test
        void beanValidation() {
            ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
            Validator validator = factory.getValidator();
    
            Item item = new Item();
            item.setItemName(" "); //공백
            item.setPrice(0);
            item.setQuantity(10000);
            
            Set<ConstraintViolation<Item>> violations = validator.validate(item);
            for (ConstraintViolation<Item> violation : violations) {
                System.out.println("violation=" + violation);
                System.out.println("violation.message=" + violation.getMessage());
            }
        }
    }

     

     

     

    Bean Validation - 스프링 적용

    ValidationItemControllerV3 코드 수정

    @Slf4j
    @Controller
    @RequestMapping("/validation/v3/items")
    @RequiredArgsConstructor
    public class ValidationItemControllerV3 {
    	private final ItemRepository itemRepository;
    
    	@GetMapping
    	public String items(Model model) {
    		List<Item> items = itemRepository.findAll();
    		model.addAttribute("items", items);
    		return "validation/v3/items";
    	}
    
    	@GetMapping("/{itemId}")
    	public String item(@PathVariable long itemId, Model model) {
    		Item item = itemRepository.findById(itemId);
    		model.addAttribute("item", item);
    		return "validation/v3/item";
    	}
        
    	@GetMapping("/add")
    	public String addForm(Model model) {
    		model.addAttribute("item", new Item());
    		return "validation/v3/addForm";
    	}
        
    	@PostMapping("/add")
    	public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
    		if (bindingResult.hasErrors()) {
    			log.info("errors={}", bindingResult);
    			return "validation/v3/addForm";
    		}
            
    		// 성공 로직
    		Item savedItem = itemRepository.save(item);
    		redirectAttributes.addAttribute("itemId", savedItem.getId());
    		redirectAttributes.addAttribute("status", true);
    		return "redirect:/validation/v3/items/{itemId}";
    	}
        
    	@GetMapping("/{itemId}/edit")
    	public String editForm(@PathVariable Long itemId, Model model) {
    		Item item = itemRepository.findById(itemId);
    		model.addAttribute("item", item);
    		return "validation/v3/editForm";
    	}
        
    	@PostMapping("/{itemId}/edit")
    	public String edit(@PathVariable Long itemId, @ModelAttribute Item item) {
    		itemRepository.update(itemId, item);
    		return "redirect:/validation/v3/items/{itemId}";
    	}
    }

    스프링 MVC 어떻게 Bean Validator 사용?

    스프링 부트가 spring-boot-starter-validation 라이브러리를 넣으면 자동으로 Bean Validator 인지하 스프링에 통합한다

     

    스프링 부트는 자동으로 글로벌 Validator 등록한다

    LocalValidatorFactoryBean 글로벌 Validator 등록한다 

    Validator @NotNull 같은 애노테이션을 보고 검증을 수행한다

    이렇게 글로벌 Validator 적용되어 있기 때문에, @Valid, @Validated 적용하면 된다

    검증 오류가 발생하면 FieldError, ObjectError 생성해서 BindingResult 담아준다

     

     

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

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

    BeanValidator는 바인딩에 실패한 필드는 BeanValidation 적용하지 않는다

    @ModelAttribute 각각의 필드 타입 변환시도 변환에 성공한 필드만 BeanValidation 적용

    • 예) itemName에 문자 "A" 입력 > 타입 변환 성공 > itemName필드에 BeanValidation 적용
    • 예) price에 문자 "A" 입력 > "A"를 숫자 타입 변환 시도 실패 > ypeMismatch FieldError 추가 > price 필드는 BeanValidation 적용 X

     

     

    Bean Validation - 에러 코드

    errors.properties

    #Bean Validation 추가
    NotBlank={0} 공백X
    Range={0}, {2} ~ {1} 허용
    Max={0}, 최대 {1}

     

     

    BeanValidation 메시지 찾는 순서

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

     

     

    Bean Validation - 오브젝트 오류

    Bean Validation에서 특정 필드(FieldError) 아닌 해당 오브젝트 관련 오류(ObjectError) 어떻게 처리는 @ScriptAssert() 사용하면 된다

    @Data
    @ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >=10000")
    public class Item {
        //...
    }

    - 실무에서는 검증 기능이 해당 객체의 범위를 넘어서는 경우들도 종종 등장하는데, 그런 경우 대응이 어렵다

     

     

    ValidationItemControllerV3 - 글로벌 오류 추가

    @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);
    		}
    	}
        
    	if (bindingResult.hasErrors()) {
    		log.info("errors={}", bindingResult);
    		return "validation/v3/addForm";
    	}
        
    	// 성공 로직
    	Item savedItem = itemRepository.save(item);
    	redirectAttributes.addAttribute("itemId", savedItem.getId());
    	redirectAttributes.addAttribute("status", true);
    	return "redirect:/validation/v3/items/{itemId}";
    }

    - 따라서 오브젝트 오류(글로벌 오류) 경우 @ScriptAssert 억지로 사용하는 보다는 오브젝트 오류 관련 부분만 직접 자바 코드로 작성하는 것을 권장한다

     

     

    Bean Validation - 수정에 적용

    상품 수정에도 검증(Bean Validation) 적용

     

     

    Bean Validation - 한계

    수정시 요구사항 발생

    • 등록시에는 quantity 수량을 최대 9999까지 등록할  있지만 수정시에는 수량을 무제한으로 변경 할수있다
    • 등록시에는 id에 값이 없어도 되지만 수정시에는 id 값이 필수이다
    • 수정시에는 Item에서 id 값이 필수이고 quantity도 무제한으로 적용할  있다

     

     

    item 수정

    @Data
    public class Item {
    	@NotNull // 수정 요구사항 추가
    	private Long id;
    
    	@NotBlank
    	private String itemName;
    
    	@NotNull
    	@Range(min = 1000, max = 1000000)
    	private Integer price;
    
    	@NotNull
    	// @Max(9999) // 수정 요구사항 추가
    	private Integer quantity;
    
    	//...
    }

    수정은 동작하지만 등록에서 문제가 발생한다

    - 등록시에는 id 값도 없고, quantity 수량 제한 최대 값인 9999 적용되지 않는다

    - 'id': rejected value [null];

     

     

    Bean Validation - groups

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

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

     

     

    BeanValidation groups 기능 사용

    등록시에 검증할 기능과 수정시에 검증할 기능을 각각 그룹으로 나누어 적용할 있다

    저장용 groups, 수정용 groups  생성

    public interface SaveCheck {
    }
    public interface UpdateCheck {
    }

     

     

    Item - groups 적용

    @Data
    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;
    
    	public Item() {
    	}
    
    	public Item(String itemName, Integer price, Integer quantity) {
    		this.itemName = itemName;
    		this.price = price;
    		this.quantity = quantity;
    	}
    }

     

     

    ValidationItemControllerV3 - 수정 로직에 UpdateCheck Groups 적용

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

     

    728x90
Designed by Tistory.