Bean Validation 정의
검증 애노테이션과 여러 인터페이스의 모음 - 인터페이스만 제공하고 구현체를 갈아끼울 수 있음 주로 Hibernate 이용
Hibernate : 객체 관계 매핑 프레임워크
애노테이션만 붙이면 될까? NO! 👉 Validator를 필요로 함
Bean Validation 사전준비
build.gradle 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-validation'
jarcarta.validation.api 설치 확인가능 -> hibernate.validator가 실제 구현체로 동작
- javax.validation (ex:@NotNull, @NotBlank) : 특정 구현에 상관없이 제공하는 표준 인터페이스
- org.hibernate.validator (ex:@Range) : hibernate 구현체를 사용할 때만 제공되는 검증 기능✨(대부분 이걸 사용)
위와 같이 의존성을 추가하면 Spring boot는 자동으로 Validator를 글로벌로 등록해준다.
어노테이션은 @Validated, @Valid 둘 다 사용가능하나, valid 는 위에서 했던 것(build.gradle에서의 starter-validation)과 같이 의존성을 추가해주어야 한다.
검증 순서
1️⃣ ModelAttribute의 각 필드에 대해 매핑 시도
2️⃣ 성공시 Validator 적용
3️⃣ 실패 시 typeMismatch로 FieldError 추가
✍ BeanValidator는 바인딩이 실패한 경우 적용하지 않는다. 👉 타입 변환이 성공될 시에만 검증
타입 변환 성공 플로우
가격 필드에 숫자 입력 -> 타입 변환 성공 -> Bean Validation 적용 가능
타입 변환 오류 플로우
가격 필드에 숫자가 아닌 문자 입력 -> 타입 변환 실패 -> typeMismatch FieldError 추가 -> BeanValidation 적용 불가
Bean Validation 기본 에러 메시지 수정 방법
BindingResult에 등록된 검증 오류 코드를 뜯어보게 되면
오류 코드가 애노테이션 이름으로 등록되는 것을 확인 할 수 있다. (마치 typemismatch처럼!)
NotBlank 오류의 경우 MessageResolver 우선순위 순서
NotBlank.item.itemName
NotBlank.itemName
NotBlank.java.lang.String
NotBlank
Bean Validation의 메시지 찾는 순서
1️⃣ Message 코드 순으로 messageSource 에서 메시지 찾기
2️⃣ Annotation에 명시된 message 사용
3️⃣ 라이브러리 제공 기본 값 사용
오브젝트 오류
필드 에러가 아닌 오브젝트 에러는 여러 필드의 조합을 필요로한다.
@ScriptAssert를 사용
@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000")
두 개의 필드값의 수식이 참이기를 원하는 어노테이션을 활용한다
해당 오류 메시지는
ScriptAssert.item(오브젝트 이름)
ScriptAssert
와 같은 우선순위로 실행된다.
그러나 위와 같은 방법은 1️⃣객체의 범위를 넘어서는 경우 대응이 어려우며 2️⃣제약이 많고 복잡하기에 따로 자바 코드로 구현하는 것을 권장한다.
필드 에러는 Bean Validation을 사용하고, 오브젝트 에러는 자바 코드를 사용하는 것이 보편적이다.
Bean Validation의 한계점
타입 검증의 차별성이 필요할 때 (ex: 데이터를 등록할 때와 수정할 때의 제약사항이 달라지는 경우)
수정 시에는 id값이 필수고, quantity는 무제한이다
id에 @NotNull을 붙여주게 되면, 당연히 등록할 시에는 튕겨버리게 된다😢
하나의 필드에 대한 다른 요구사항이 충돌하게 된다.
이를 해결하기 위해서
1️⃣ Bean Validation Group
2️⃣ ModelAttribute 객체를 분리 - 폼 객체 따로 생성
Bean Validation의 한계 극복 방법
Bean Validation Group
등록일 때 이 조건, 수정할 때 이 조건을 사용할 수 있도록 Group을 이용하자
인터페이스 생성
public interface UpdateCheck {
}
NotNull에 group 적용
@NotNull(groups = {UpdateCheck.class})
private Long id;
* Groups 기능을 사용하려면 Validated를 사용해야 한다.
ModelAttribute 객체 분리✨ 주로 이용
직접 도메인에 적용하지 않고, 폼 전송을 위한 객체를 분리하자! 별도의 ModelAttribute를 생성하는 것이다.
원하는 특정 정보 뿐만 아니라 부가적인 정보를 받게 되는 경우도 허다하기 때문에 폼을 하나 더 생성해서 ModelAttribute 에 사용한다.
별도의 객체를 생성하게 되면 [👎중간에 실제 객체를 만드는 과정이 추가적으로 필요해지게 되나], [👍검증 로직이 중복되지는 않는다].
Form의 네이밍은 답이 정해져 있지는 않지만 일관성 있게 작성하자! Form, Dto etc.. (나는 프로젝트를 진행할 때 Form을 사용했다 🙃)
앞서 Bean Validation을 주석 처리하고, Save와 Update의 Form 을 분리해서 생성해 주자.
@Data
public class ItemSaveForm {
@NotBlank
private String itemName;
@Data
public class ItemUpdateForm {
@NotNull
private Long id;
위와 같이 새로 객체를 저장하는 Save의 경우에는 id가 필요 없지만, Update의 경우에는 id가 필수인 경우처럼 검증을 분리할 수 있게 된다.
Controller 수정
ModelAttribute 수정, 객체 변환
@PostMapping("/add")
public String addItemV3(@Validated @ModelAttribute("item") ItemSaveForm form, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
...
Item item = new Item();
item.setItemName(form.getItemName());
item.setQuantity(form.getQuantity());
item.setPrice(form.getPrice());
// 검증 성공
Item savedItem = itemRepository.save(item);
...
}
Bean Validation HTTP 메시지 컨버터에 적용(@ReqeustBody)
RequestBody로 숫자 필드에 문자를 대입한 뒤 실행하게 되면, JSON을 객체로 변환할 때 오류가 발생하게 된다. ParsingError 생성(컨트롤러 자체가 호출되지 않음)
API의 세 가지 플로우
1️⃣ 성공
2️⃣ JSON -> 객체 변환 실패
3️⃣ JSON -> 객체 변환 성공 & 검증 실패 👉 예외 처리 필요
ModelAttribute, RequestBody 의 검증 차이
@ModelAttribute | @RequestBody | |
적용 단위 | 각각의 필드 단위로 정교하게 적용 | 전체 객체로 정교하게 적용 |
특정 필드 바인딩이 실패시 | 나머지 필드에 대해 Validator 적용 가능 | 다음 단계 진행 불가, 컨트롤러 진입X |
'Spring' 카테고리의 다른 글
MVC2 - Filter, Interceptor (1) | 2022.10.05 |
---|---|
MVC2 - 로그인(쿠키, 세션) (1) | 2022.10.03 |
MVC2 - Validation (0) | 2022.09.27 |
File->MultipartFile (0) | 2022.07.26 |
QueryDSL 사용 전 환경설정 (0) | 2022.07.12 |