김영한 님의 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard
if문을 이용해서 작성하던 검증 로직을 간편하게 작성할 수 있고 모든 프로젝트에 적용할 수 있도록 공통화하고 표준화한 것이 Bean Validation
Bean Validation은 구현체가 아닌 기술 표준
여러 어노테이션과 인터페이스의 모음
jakarta.validation-api
: Bean Validation 인터페이스
hibernate-validator
: 일반적으로 사용하는 구현체
Bean Validation을 사용하려면 아래 의존관계를 추가해야한다
implementation 'org.springframework.boot:spring-boot-starter-validation'
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@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;
}
검증 어노테이션
@NotBlank
: 빈 값 + 공백만 있는 경우를 허용하지 않는다
@NotNull
: null을 허용하지 않는다
@Range(min=, max=)
: 최대, 최소 범위 지정
@Max
: 최댓값 지정
import를 보면 @NotBlank
와 @NotNull
은 javax.validation이고 @Range
는 hibernate.validator인 것을 확인할 수 있다
@NotBlank
와 @NotNull
은 Bean Validation이 표준적으로 제공하기 때문에 모든 구현체에서 사용 가능한 어노테이션이다
@Range
는 hibernate.validator에서만 동작한다
ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); Validator validator = factory.getValidator();
검증기 생성하는 코드
스프링과 통합하면 직접 이런 코드를 작성하지 않기 때문에 참고만
Set<ConstraintViolation<Item>> violations = validator.validate(item);
검증기를 실행하는 코드
검증 대상인 item을 검증기에 넣고 결과를 반환
Set에 ConstraintViolation 검증 오류가 담긴다
Set이 비어있다면 오류가 발생하지 않은 것이다
violation이 가진 메세지는 hibernate validator에서 기본적으로 제공하는 메세지
@NotNull(message="공백 불가")
처럼 작성하면 원하는 메세지로 바꿀 수 있다validation 의존관계를 추가하면 스프링부트가 자동으로 Bean Validator를 인지하고 스프링에 통합
그렇게되면 스프링부트는 LocalValidatorFactoryBean을 글로벌 Validator로 등록
글로벌 Validator가 적용되어 있기 때문에 검증 대상에 @Valid
나 @Validated
만 적용하면 Validator는 @NotNull
과 같은 어노테이션을 보고 검증을 수행한다
검증 오류가 발생하면 FieldError, ObjectError를 생성해서 BindingResult에 담아준다
즉, Controller에 지난 게시글에서 했던 ItemValidator를 주입받고 WebDataBinder에 등록하는 코드가 없어도 동작한다
단> 지난 게시글의 마지막 부분처럼 직접 글로벌 Validator를 적용하면 스프링부트는 Bean Validator를 글로벌 Validator로 등록하지 않아 동작하지 않는다
@ModelAttribute
가 각각의 필드에 타입 변환 시도
실패하면 typeMisMatch
로 FieldError 추가
성공하면 Validator
적용
즉, 바인딩에 성공한 필드만 Bean Validation을 적용해 어노테이션을 통한 검증을 수행한다 ( 값이 정상적으로 들어와야 검증이 의미가 있기 때문에 )
FieldError 객체를 만들 때 어노테이션 이름을 errorCode 로 사용한다
즉, MessageCodesResolver
가 어노테이션 이름을 errorCode 로 사용해 메세지 코드를 만들어낸다
ex> @NotBlank
가 지켜지지 않은 경우 ➜ NotBlank.item.itemName
, NotBlank.item
등과 같이 메세지 코드가 생성 ( 지난 게시글 참고 )
Bean Validation이 메세지를 찾는 순서는 아래와 같다
생성된 메시지 코드 순서대로 messageSource
에서 메시지 찾기
어노테이션의 message 속성 사용
라이브러리가 제공하는 기본 값 사용
즉, 생성된 메세지 코드를 메세지 파일( messageSource )에서 관리하면 가장 먼저 찾아지기 때문에 원하는 메세지를 출력할 수 있다
참고> 이전 게시글에 있는 내용이지만 FieldError는 codes 파라미터에 위와 같은 메세지 코드 조합을 String[] 형태로 받는다
@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000", message="총합이 10000원 넘게 입력해주세요")
public class Item {
...
}
검증 대상에 @ScriptAssert
어노테이션을 붙이면 ObjectError를 처리할 수 있다
생성되는 메세지 코드는 ScriptAssert.item
, ScriptAssert
이다
but> 사용에 제약이 많고, 실제 실무에서는 객체의 범위를 넘어서는 경우가 많은데 이럴 때 대응하기 어렵다
➡️ ObjectError를 처리할 때는 @ScriptAssert를 사용하는 것보다 자바 코드로 작성하는 것을 권장
등록할 때와 수정할 때의 검증 요구사항이 다르면 같은 객체에서 검증 조건이 충돌
결과적으로 등록과 수정은 같은 Bean Validation을 적용할 수 없다
해결 방법 1 : Bean Validation 의 groups 기능을 사용
해결 방법 2 : Item 객체를 직접 사용하지 않고, ItemSaveForm, ItemUpdateForm 같은 폼 전송을 위한 별도의 모델 객체를 만들어서 사용
인터페이스 생성 ( ex> SaveCheck, UpdateCheck )
객체 필드에 어노테이션을 붙일 때 groups 속성에 인터페이스 이름을 지정
등록, 수정 모두에 사용되는 필드 : @NotBlank(groups = {SaveCheck.class, UpdateCheck.class})
수정만 사용하는 필드의 경우 : @NotNull(groups = UpdateCheck.class)
Controller 에서 @Validated
를 붙일 때 등록인지, 수정인지 명시
등록 메서드의 경우 : @Validated(SaveCheck.class)
수정 메서드의 경우 : @Validated(UpdateCheck.class)
@Valid
에는 groups 기능이 없기 때문에 @Validated
를 사용해야함문제점
등록, 수정에 필요한 데이터는 서로 다르다
이런 데이터들이 도메인 객체와 정확하게 맞지 않는다
해결
Form을 전달 받는 전용 객체를 만들어 @ModelAttribute
로 사용
이를 통해 Controller에서 데이터를 전달받고, 필요한 데이터를 사용해서 객체( 도메인 객체 )를 생성
getXXX()
를 이용하여 도메인 객체를 생성폼 데이터 전달에 도메인 객체 사용
@ModelAttribute Item item
HTML Form ➜ 도메인 객체 ➜ Controller ➜ 도메인 객체 ➜ Repository
폼 데이터 전달을 위한 별도의 객체 사용
@ModelAttribute ItemSavdForm form
HTML Form ➜ Form 객체 ➜ Controller ➜ 도메인 객체 생성 ➜ Repository
@RequestBody
vs @ModelAttribute
@Valid
, @Validated
는 HttpMessageConverter ( @RequestBody
)에도 적용할 수 있다@ModelAttribute
HTTP 요청 파라미터( URL 쿼리 스트링, POST Form )를 다룰 때 사용
필드 단위로 바인딩이 적용되기 때문에 특정 필드가 바인딩 되지 않아도 나머지 필드는 정상 바인딩 되고, Validator를 사용한 검증도 적용할 수 있다
@RequestBody
HTTP Message Body의 데이터를 객체로 변환할 때 사용
필드 단위로 적용하는 것이 아니라 전체 객체 단위로 적용
즉, HttpMessageConverter 단계에서 JSON 데이터를 객체로 변경하지 못하면 이후 단계 자체가 진행되지 않고 예외가 발생
예외가 발생하면 컨트롤러도 호출되지 않고, Validator도 적용할 수 없다
예외가 발생했을 때 원하는 모양으로 처리할 수 있다 ( 이후 게시글 참고 )
@RequestBody
적용@RestController
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;
}
}
ex1> price에 문자를 입력한 경우 ( 타입 변환 오류 )
Controller가 호출되지 않는다
HttpMessageConverter
에서 요청 JSON을 ItemSaveForm
객체로 생성하는데 실패
JSON 데이터로 ItemSaveForm 객체를 만들어야 Controller를 호출하는데 변환 자체가 실패해서 호출되지 않는 것임
ex> 수량 최대 범위 초과해서 입력한 경우 ( 검증 오류 )
JSON을 객체로 생성하는건 성공헀지만 검증에서 실패
이 경우, Controller는 호출된다
@RestController
= @Controller
+ @ResponseBody