스프링 스터디 ; 검증2 Bean Validation

24309 단어 스프링스프링

Bean Validation

: 검증 로직을 공통화하고, 표준화한 것입니다. 이렇게 지정한다면 애노테이션 하나로 검증 로직을 매우 편리하게 적용할 수 있습니다.

@NotBlank @NotNull @Range(min=1000, max=100000) 과 같은 검증 애노테이션이 존재합니다.


: javax.validation으로 시작하면 '특정 구현'에 관계 없이 제공되는 표준 인터페이스입니다.

: org.hibernate.validator로 시작하면 하이버네이트 validator구현체를 사용할 때만 제공되는 검증입니다.
: 하지만 실무에서 대부분 하이버네이트 validator를 사용하므로 자유롭게 사용해도 됩니다.

Bean Validator는 보편적으로 사용되는 빈 값, 범위 등에 대한 검증 로직을 모든 프로젝트에 범용적으로 사용할 수 있도록 표준화한 인터페이스 입니다. 즉, Bean Validation은 특정한 구현체가 아닙니다. 기술 표준으로서 다양한 애노테이션과 여러 인터페이스의 모음입니다. Bean Validation의 구현체 로는 hibernate Validator가 가장 많이 사용됩니다.
참조블로그

Bean Validation 스프링 적용

검증 순서

  1. @ModelAttribute 각각의 필드에 타입 변환을 시도합니다. 성공하면 다음으로 넘어가고, 실패한다면 typeMismatchFieldError를 추가합니다.
  2. 그 이후 Validator를 적용합니다.

이를 통해 알 수 있는 것은, 타입 변환에 성공해서 바인딩에 성공한! 필드여야 Bean Validation이 의미가 있다는 것입니다.

Bean Validation 에러 코드

: Bean Validation을 적용하고 bindingResult에는 오류 코드가 애노테이션 이름으로 등록됩니다.

: NotBlank라는 오류 코드를 기반으로 MessageCodeResolver를 통해 Field Error에 대한 다양한 메시지 코드가 순서대로 생성되는 예시입니다.
: NotBlank.item.itmeName
: NotBlank.itemName
: NotBlank.java.lang.String
: NotBlank

MessageCodesResolver는 검증 오류 코드로 메시지 코드들을 생성합니다. 해당 인터페이스를 통해 개발자는 범용적인 메시지와 세밀한 메시지 모두 손쉽게 추가할 수 있게 됩니다. BindingResult에 FieldError나 ObjectError를 추가해주면 메시지코드리졸버가 메시지 코드들을 생성해줍니다.

: FieldError를 위한 메시지 코드로는 resolver는 다음의 순서로 네 가지 메시지 코드를 생성합니다.
1. code + "." + object name + "." + field
2. code + "." + field
3. code + "." + field type
4. code

: 스프링은 기본값보다 구체적인 값을 발견했을 때 구체적인 설정 값을 사용합니다.

: ObjectError를 위한 메시지 코드는 다음과 같습니다. (객체 오류)
1. code + "." + object name
2. code

: 필드에러의 경우는 유용하지만, 오브젝트와 관련된 오류는 Field에 애노테이션을 붙일 수 없으므로 @ScriptAssert()를 사용하면 됩니다.

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

}

: 하지만 Object Error를 이렇게 사용하면 제약이 많고 복잡해집니다. 따라서 Object Error(글로벌 오류)의 경우 따로 자바 코드로 작성하는 것이 보다 유용합니다.

BeanValidation 메시지 찾는 순서

  1. 생성된 메시지 코드의 순서대로 메시지 찾기(위에서 설명한 메시지 코드)
  2. 애노테이션의 message 속성 사용
@NotBlank(messgae="공백을 입력할 수 없습니다.")
  1. 라이브러리가 제공하는 기본 값 사용

Bean Validation의 한계

: 데이터 등록시와 수정시의 요구사항이 다를 수 있습니다.
: 예를들어 회원 등록의 경우에는 다음과 같이 약관 동의와 같은 초기 정보를 받을 수 있습니다.

: 하지만 회원 정보 수정시에는 등록과 완전히 똑같은 정보를 요구하지만은 아님을 확인할 수 있습니다.

: 이를 위해 스프링은 두 가지의 방법을 제공합니다.
1. BeanValidation의 groups 기능 사용
2. ItemSaveForm, ItemUpdateForm과 같은 폼 전송을 위한 별도의 모델 객체를 만들어서 사용

groups 기능 사용

저장용 groups과

package hello.itemservice.domain.item;
public interface SaveCheck {
}

수정용 groups

package hello.itemservice.domain.item;
public interface UpdateCheck {
}

을 생성하고 Item 클래스에는 다음과 같이 적용합니다.

@Data
public class Item {
	@NotNull(groups = UpdateCheck.class) //수정시에만 적용
	private Long id;
	@NotBlank(groups = {SaveCheck.class, UpdateCheck.class})
	private String itemName;
...
}
}

그리고 저장 로직에는

public String addItemV2(@Validated(SaveCheck.class) ...)

수정 로직에는

public String editV2(@Validated(UpdateCheck.class) ..)

처럼 적용하면 됩니다.

form 전송 객체 분리

: 하지만 보통 실무에서는 폼 전송 객체를 분리하는 방식으로 사용합니다.
: 이 방식은 Item을 직접 전달받는 것이 아니라, 복잡한 폼의 데이터를 컨트롤러 까지 전달할 별도의 객체를 만들어서 전달하는 것입니다.
: 예를 들어 ItemSaveForm이라는 폼을 전달받는 전용 객체를 만들어서 @ModelAttribute로 사용하는 것입니다.
: 이를 통해서 컨트롤러에서 폼 데이터를 전달받고, 필요한 정보만을 사용해서 Item을 생성하면 됩니다.

: 이때, 이러한 폼 전달 객체 생성의 과정에서 ItemSaveForm과 ItemEditForm을 분리하면 등록폼과 수정폼의 내용이 달라지도록 구현할 수 있습니다.

예시 코드는 다음과 같습니다.

: Item에서 모든 검정 코드는 적용하지 않아도 됩니다.

@Data
public class Item{
	private long id;
    private String itemName;
    ...
}

: ITEM 저장용! 폼입니다.

@Data
public class ItemSaveForm{
	@NotBlank
    private String itemName;
    
    @NotNull
    @Range(min=1000, max = 1000000)
    pirvate Integer price;
    
    ...
}

: ITEM 수정용! 폼입니다.

@Data
public class ItemUpdateForm{
	@NotNull
    private Long id;
    
    @NotBlank
    private String itemName;
    
    ...
}

: 해당 폼을 사용하도록 컨트롤러를 수정합니다.

@Slf4j
@Controller
@RequestMapping("/validation/v4/items")
@RequiredArgsConstructor
public class ValidationItemController{
	private final ItemRepository itemrRepository;
    
    @GetMapping("/add")
    public String addForm(Model model){
    	model.addAttribute("item", new Item());
        return "validation/v4/addForm";
    }
    
    @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[]{1000, 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}";
}	

HTTP 메시지 컨버터 Bean Validation

: @Validated를 HTTPMessageConverter(@RequestBody)에도 사용할 수 있습니다.
: @RequestBody는 주로 API JSON 요청을 다룰 때 사용합니다.

@Slf4j
@RestController
@RequestMapping("/validation/api/items")
public class ValidationItemApiController {
@PostMapping("/add")
public Object addItem(@RequestBody @Validated ItemSaveForm form,BindingResult bindingResult) {
	log.info("API 컨트롤러 호출");
	if (bindingResult.hasErrors()) {
		log.info("검증 오류 발생 errors={}", bindingResult);
		return bindingResult.getAllErrors();
	}
	log.info("성공 로직 실행");
	return form;
}
}

해당 코드에서 얻어낼 수 있는 결과는 세가지가 있습니다.
1. 성공하는 경우
2. JSON을 객체로 생성하는 것 자체가 실패하는 경우
(ex. price 값에 숫자가 아닌 문자를 전달한 경우)
(Item 객체를 만들지 못하므로 컨트롤러 자체가 호출되지 않고 그 전에 예외가 발생합니다.)
3. JOSN을 객체로 생성했지만, 검증에서 실패하는 경우 입니다.

: 2번째 case가 생기는 이유는 다음과 같습니다. 바로 필드단위로 적용되는 @ModelAttribute대비 HttpMessageConverter는 전체 객체 단위로 적용되기 때문입니다. @ModelAttribute는 하나의 필드가 잘못되어도 다른 필드는 정상처리할 수 있지만 HttpMessageConverter는 불가능합니다.

좋은 웹페이지 즐겨찾기