이 곳에 오면서 우리는 이전에 만든 프로젝트에서 새로운 요구사항이 발생했다.
.
.
여기서 가장 중요한 것은 두 가지다.
컨트롤러의 중요한 역할 중 하나는 HTTP 요청이 정상인지 검증하는 것이다.
그리고... 정상 로직보다 이런 검증 로직을 잘 개발하는 것이 어쩌면 더 어려울 수 있다.
- 참고: 클라이언트 검증, 서버 검증
- 클라이언트 검증은 조작할 수 있으므로 보안에 취약하다.
- 서버만으로 검증하면, 즉각적인 고객 사용성이 부족해진다.
- 둘을 적절히 섞어서 사용하되, 최종적으로 서버 검증은 필수
- API 방식을 사용하면 API 스펙을 잘 정의해서 검증 오류를 API 응답 결과에 잘 남겨주어야 함 -> API 명세서 등
사용자가 상품 등록 폼에서 정상 범위의 데이터를 입력하면, 서버에서는 검증 로직이 통과하고, 상품을 저장하고, 상품 상세 화면으로 redirect한다
=> 서버 검증 로직이 실패해야 한다.
이렇게 검증에 실패한 경우 고객에게 다시 상품 등록 폼을 보여주고, 어떤 값을 잘못 입력했는지 친절하게 알려주어야 한다.
[ValidationItemControllerV1 - addItem() 수정]
@PostMapping("/add")
public String addItem(@ModelAttribute Item item, RedirectAttributes
redirectAttributes, Model model) {
//검증 오류 결과를 보관
Map<String, String> errors = new HashMap<>();
//검증 로직
if (!StringUtils.hasText(item.getItemName())) {
errors.put("itemName", "상품 이름은 필수입니다.");
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() >
1000000) {
errors.put("price", "가격은 1,000 ~ 1,000,000 까지 허용합니다.");
}
if (item.getQuantity() == null || item.getQuantity() >= 9999) {
errors.put("quantity", "수량은 최대 9,999 까지 허용합니다.");
}
//특정 필드가 아닌 복합 룰 검증
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
errors.put("globalError", "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice);
}
}
//검증에 실패하면 다시 입력 폼으로
if (!errors.isEmpty()) {
model.addAttribute("errors", errors);
return "validation/v1/addForm";
}
//성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v1/items/{itemId}";
}
이 중에서 주로 살펴 볼 것 위주로 작성해보겠다.
//검증 오류 보관
Map<String, String> errors = new HashMap<>();
//검증 로직
if (!StringUtils.hasText(item.getItemName())) {
errors.put("itemName", "상품 이름은 필수입니다."); }
검증 오류 :
만약 검증시 오류가 발생하면 어떤 검증에서 오류가 발생했는지 '검증 오류 결과'(오류난 곳, "메시지") 보관 하는 곳.
검증 로직:
errors에 key, value 타입으로
=>(오류가 발생한 필드명, 고객에게 전달할 메시지)
//특정 필드의 범위를 넘어서는 검증 로직
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
errors.put("globalError", "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값
= " + resultPrice);
}
}
=> 즉, globalError
라는 Key
를 사용
if (!errors.isEmpty()) {
model.addAttribute("errors", errors);
return "validation/v1/addForm";
}
검증에서 '오류 메시지'가 1개라도 있다면, 사용자에게 오류 메시지 출력을 해주기 위해
-> model에 errors 객체를 담아 '뷰 템플릿'에 보내준다.
.field-error {
border-color: #dc3545;
color: #dc3545;
}
errors
에 내용이 있을 때만 출력하면 된다.th:if
를 사용하면 조건에 만족할 때만 해당 HTML 태그를 출력할 수 있다. -> 여기서는 errors 객체에 '글로벌 오류'가 포함되어 있는지에 따라서 HTML 태그가 적용 ( 위에 css를 적용한다.)
errors?.
errors
가 null
일때 NullPointerException
이 발생하는 대신, null 을 반환하는 문법이다.th:if
에서 null 은 실패로 처리되므로 오류 메시지가 출력되지 않는다.
classappend
를 사용해서 해당 필드에 오류가 있으면field-error
라는 클래스 정보를 더해서
폼의 색깔을 빨간색으로 강조한다.
_
(No-Operation): 아무것도 하지 않는다.
<input type="text" class="form-control field-error"
위에서 말했 듯이 class ="" 안에 css 설정 등을 적용한다.
<div class="field-error" th:if="${errors?.containsKey('itemName')}" th:text="$
{errors['itemName']}">
상품명 오류
</div>
조건문에 해당될 때, errors객체에 key 값을 넣어서 오류 메시지를 가져올 수 있다.
BindingResult bindingResult
파라미터의 위치는 @ModelAttribute Item item
꼭 다음에 와야 한다. ⭐
@PostMapping("/add")
public String addItemV1(@ModelAttribute Item item, BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.addError(new FieldError("item", "itemName", "상품 이름은 필수입니다."));
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() >
1000000) {
bindingResult.addError(new FieldError("item", "price", "가격은 1,000 ~ 1, 000, 000까지 허용합니다."));
}
if (item.getQuantity() == null || item.getQuantity() > 10000) {
bindingResult.addError(new FieldError("item", "quantity", "수량은 최대 9, 999까지 허용합니다."));
}
//특정 필드 예외가 아닌 전체 예외
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.addError(new ObjectError("item", "가격 * 수량의 합은 10,000원 이상이어야 합니다.현재 값 = " + resultPrice));
}
}
if (bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
return "validation/v2/addForm";
}
//성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v2/items/{itemId}";
}
필드에 오류가 있으면 FieldError
객체를 생성해서 bindingResult
에 담아두면 된다. => 위에서 errors와 같다.
(구체적 코드)
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.addError(new FieldError("item", "itemName", "상품 이름은
필수입니다."));
}
objectName
: @ModelAttribute
이름field
: 오류가 발생한 필드 이름defaultMessage
: 오류 기본 메시지(구체적 코드)
bindingResult.addError(new ObjectError("item", "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice));
ObjectError
객체를 생성해서 bindingResult
에 담아두면 된다.objectName
: @ModelAttribute 의 이름defaultMessage
: 오류 기본 메시지-> 특정 필드 (x) 당연히, 그 자리는 없음
타임리프는 스프링의 BindingResult 를 활용해서 편리하게 검증 오류를 표현하는 기능을 제공
#fields
: #fields
로 BindingResult
가 제공하는 검증 오류에 접근할 수 있다.th:errors
: 해당 필드에 오류가 있는 경우에 태그를 출력한다.th:if
의 편의 버전이다.th:errorclass
: th:field
에서 지정한 필드에 오류가 있으면 class 정보를 추가한다.스프링이 제공하는 검증 오류를 보관하는 객체이다. 검증 오류가 발생하면 여기에 보관하면 된다.
BindingResult
가 있으면 @ModelAttribute
에 데이터 바인딩 시 오류가 발생해도 컨트롤러가 호출된다!숫자가 입력되어야 할 곳에 문자를 입력해서 타입을 다르게 해서 BindingResult
를 호출하고
bindingResult
의 값을 확인해보자.
주의
BindingResult 는 검증할 대상 바로 다음에 와야한다. 순서가 중요하다. 예를 들어서 @ModelAttribute Item item , 바로 다음에 BindingResult 가 와야 한다.
BindingResult 는 Model에 자동으로 포함된다.
BeanPropertyBindingResult
라는 것인데, 둘다 구현하고 있으므로 BindingResult
대신에 Errors 를 사용해도 된다.Errors 인터페이스
는 단순한 오류 저장과 조회 기능을 제공한다. BindingResult
는 여기에 더해서 추가적인 기능들을 제공한다. addError()
도 BindingResult
가 제공하므로 여기서는 BindingResult
를 사용하자.BindingResult
를 많이 사용한다.BindingResult
, FieldError
, ObjectError
를 사용해서 오류 메시지를 처리하는 방법을 알아보았다.
=> 그런데 오류가 발생하는 경우 고객이 입력한 내용이 모두 사라진다. 이 문제를 해결해보자
FieldError
, ObjectError
에 대해서 더 자세히 알아보자.ValidationItemControllerV2 - addItemV2
@PostMapping("/add")
public String addItemV2(@ModelAttribute Item item, BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.addError(new FieldError("item", "itemName",
item.getItemName(), false, null, null, "상품 이름은 필수입니다."));
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() >
1000000) {
bindingResult.addError(new FieldError("item", "price", item.getPrice(),
false, null, null, "가격은 1,000 ~ 1,000,000 까지 허용합니다."));
}
if (item.getQuantity() == null || item.getQuantity() > 10000) {
bindingResult.addError(new FieldError("item", "quantity",
item.getQuantity(), false, null, null, "수량은 최대 9,999 까지 허용합니다."));
}
//특정 필드 예외가 아닌 전체 예외
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.addError(new ObjectError("item", null, null, "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice));
}
}
if (bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
return "validation/v2/addForm";
}
//성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v2/items/{itemId}";
}
이렇게 2가지 생성자를 제공한다.
public FieldError(String objectName, String field, String defaultMessage);
public FieldError(String objectName, String field, @Nullable Object
rejectedValue, boolean bindingFailure, @Nullable String[] codes, @Nullable
Object[] arguments, @Nullable String defaultMessage)
[파라미터 목록]
objectName
: 오류가 발생한 객체 이름field
: 오류 필드rejectedValue
: 사용자가 입력한 값(거절된 값)bindingFailure
: codes
: 메시지 코드arguments
: 메시지에서 사용하는 인자defaultMessage
: 기본 오류 메시지ObjectError 또한 여기서 field만 뺀 상태로 유사하게 2가지 생성자를 제공한다.
new FieldError("item", "price", item.getPrice(), false, null, null, "가격은 1,000 ~
1,000,000 까지 허용합니다.")
이번 파트의 목표는 '오류 발생'시,
-> 객체에 입력한 값을 보내줬던 것처럼 저장해서 다시 보내주면 좋겠다.
사용자의 입력 데이터가 컨트롤러의 @ModelAttribute
에 바인딩되는 시점에 오류가 발생하면, 모델 객체에 사용자 입력 값을 유지하기 어렵다.
=> 아까 위에서 본 1번 방식과 똑같다! (BindingResult에 검증 오류를 적용하는 3가지 방법) 😮👍
ex) 가격에 숫자가 아닌 문자가 입력된다면 가격은 Integer 타입
이므로 문자를 보관할 수 있는 방법이 없다.😥
=> 그래서 오류가 발생한 경우 사용자 입력 값을 보관하는 별도의 방법이 필요
ㄴ> 그리고 이렇게 보관한 사용자 입력 값을 검증 오류 발생시 화면(다시 뜬 입력 폼 화면)에 다시 출력하면 된다.
FieldError
는 오류 발생시 사용자 입력 값을 저장하는 기능을 제공한다(요약) 저 매개변수 중에 rejectedValue
가 바로 오류 발생시 '사용자 입력 값'을 저장하는 필드다.
bindingFailure
는 타입 오류 같은 바인딩이 실패했는지 여부를 적어주면 된다. 여기서는 바인딩이 실패한 것은 아니기 때문에 false 를 사용.
th:field="*{price}"
타임리프의 th:field
는 매우 똑똑하게 동작하는데,
FieldError
에서 보관한 값을 사용해서 값을 출력한다.FieldError
를 생성하면서 사용자가 입력한 값을 넣어둔다. BindingResult
에 담아서 컨트롤러를 호출한다.public FieldError(String objectName, String field, String defaultMessage);
public FieldError(String objectName, String field, @Nullable Object
rejectedValue, boolean bindingFailure, @Nullable String[] codes, @Nullable
Object[] arguments, @Nullable String defaultMessage)
<파라미터 목록>
근데 codes 랑 arguments에 아무것도 안 넣네..?
=> 아니다 이것들은 오류 발생시 오류코드로 메시지를 찾기 위해 사용되는 아이들이다. 써보자💬
application.properties
에spring.messages.basename=messages,errors
스프링 부트가 해당 메시지 파일을 인식할 수 있게 다음 설정을 추가
messages.properties
,rrors.properties
두 파일을 모두 인식
+) 생략 시,messages.properties
를 기본으로 인식
src/main/resources/errors.properties
]required.item.itemName=상품 이름은 필수입니다.
range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
max.item.quantity=수량은 최대 {0} 까지 허용합니다.
totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
물론 국제화 처리도 가능하다.
얘네 덕분에 이제 Controller는
[ValidationItemControllerV2] 에서 -addItemV3() 추가
if (item.getQuantity() == null || item.getQuantity() > 10000) {
bindingResult.addError(new FieldError("item", "quantity",
item.getQuantity(), false, new String[]{"max.item.quantity"}, new Object[]
{9999}, null));
}
//특정 필드 예외가 아닌 전체 예외
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.addError(new ObjectError("item", new String[]
{"totalPriceMin"}, new Object[]{10000, resultPrice}, null));
}
}
required.item.itemName
를 사용해서 위에 우리가 만들어 놓은 errors에서 메시지 코드를 지정한다.Object[]{1000, 1000000}
를 사용해서 코드의 {0} , {1} 로 치환할 값을 전달한다.목표
1. FieldError
, ObjectError
는 다루기 너무 번거롭다.😥
2. 오류 코드도 좀 더 자동화 할 수 있지 않을까? 예) item.itemName 처럼? 🤔
컨트롤러에서 BindingResult
는 검증해야 할 객체인 target 바로 다음에 온다.
따라서 BindingResult
는 이미 본인이 검증해야 할 객체인 target 을 알고 있다
log.info("objectName={}", bindingResult.getObjectName());
log.info("target={}", bindingResult.getTarget());
놀랄 노짜다 이미 알고 있었다니,,, 그랬따면 우리는 Item을 굳이 안 넣어도 되지 않았을까?
BindingResult 가 제공하는 rejectValue()
, reject()
를 사용하면 FieldError , ObjectError 를 직접 생성하지 않고, 깔끔하게 검증 오류를 다룰 수 있다. 😮👍👍
if (item.getQuantity() == null || item.getQuantity() > 10000) {
bindingResult.rejectValue("quantity", "max",
new Object[]{9999}, null);
}
//특정 필드 예외가 아닌 전체 예외
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.reject("totalPriceMin",
new Object[]{10000, resultPrice}, null);
}
}
errors.properties
에 있는 코드를 직접 입력하지 않았는데 어떻게 된 것일까? 💦💦💦A. 이것은 메시지에 등록된 코드는 아니다. 그러나 이전 FieldError()을 다룰 때와는 달리 오류 코드를 전체 다 입력하지 않고 (ex range.item.price
)
range로 간단하게 입력했다.
=> 뒤에서 이것에 대해 자세히 배운다. (마치... 무언가 규칙이 있는 것 같아)
- 앞에서 BindingResult 는 어떤 객체를 대상으로 검증하는지
target을 이미 알고 있다고 했다. 따라서 target(item)에 대한 정보는 없어도 된다. 오류 필드명은 동일하게 quantity 를 사용했다.
ex)
오류 코드를 만들 때 다음과 같이 [자세히 만들 수도 있고]
required.item.itemName
: 상품 이름은 필수 입니다.
range.item.price
: 상품의 가격 범위 오류 입니다.
또는 다음과 같이 [단순하게 만들 수도 있다.]
required
: 필수 값 입니다.
range
: 범위 오류 입니다.
단순하게 만들면 범용성이 좋아서 여러곳에서 사용할 수 있지만, 메시지를 세밀하게 작성하기 어렵다.
반대로 너무 자세하게 만들면 범용성이 떨어진다.
=> 가장 좋은 방법은 범용성으로 사용하다가, 세밀하게
작성해야 하는 경우에는 세밀한 내용이 적용되도록 메시지에 단계를 두는 방법이다.
객체명과 필드명을 조합한 메시지가 있는지 우선 확인하고, 없으면 좀 더 범용적인 메시지를 선택하도록 추가 개발을 해야겠지만,
✔ 범용성 있게 잘 개발해두면, 메시지의 추가 만으로 매우 편리하게 오류 메시지를 관리할 수 있을 것이다.
✔ 스프링은MessageCodesResolver
라는 것으로 이러한 기능을 지원한다.
public class MessageCodesResolverTest {
MessageCodesResolver codesResolver = new DefaultMessageCodesResolver();
@Test
void messageCodesResolverObject() {
String[] messageCodes = codesResolver.resolveMessageCodes("required",
"item");
assertThat(messageCodes).containsExactly("required.item", "required");
}
@Test
void messageCodesResolverField() {
String[] messageCodes = codesResolver.resolveMessageCodes("required",
"item", "itemName", String.class);
assertThat(messageCodes).containsExactly(
"required.item.itemName",
"required.itemName",
"required.java.lang.String",
"required"
);
}
}
이런 순대로 만들어진다.
ex)
다음 4가지 오류 코드를 자동으로 생성
required.item.itemName
required.itemName
required.java.lang.String
required
다음 2가지 오류 코드를 자동으로 생성
totalPriceMin.item
totalPriceMin
rejectValue()
, reject()
는 내부에서 MessageCodesResolver
를 사용한다. 여기에서 메시지 코드들을 생성한다.FieldError
, ObjectError
의 생성자를 보면, 오류 코드를 하나가 아니라 여러 오류 코드를 가질 수 있다.MessageCodesResolver
를 통해서 생성된 순서대로 오류 코드를 보관한다.codes [range.item.price, range.price, range.java.lang.Integer, range]
-> required
로 끝내고 구체적이면 required.item.itemName
이렇게
th:erros
에서 메시지 코드들로 메시지를 '순서대로' 메시지에서 찾고, 노출=> 이 말은 위에서 봤던 것처럼 순위 높은대로 생성이 될테니 순서대로란 뜻은 즉, 그 순위 높은 순서대로 메시지에서 찾는다는 거다.
물론 자세히 써놓았다면 그 메시지 코드를 적용할 것임.
검증 오류 코드는 다음과 같이 2가지로 나눌 수 있다.
rejectValue()
를 직접 호출그래서 타입의 오류 경우에는 신기하게도!
로그를 확인해보면 BindingResult
에 FieldError
가 담겨있고, 다음과 같은 메시지 코드들이 생성된 것을 확인할 수 있다.
codes[typeMismatch.item.price,typeMismatch.price,typeMismatch.java.lang.Integer,typ
eMismatch]
스프링은 타입 오류가 발생하면
typeMismatch
라는 오류 코드를 사용한다.
이 오류 코드가MessageCodesResolver
를 통하면서 4가지 메시지 코드가 생성된 것이다.
어쨌든 errors.properties에 코드가 없기에 기본 메시지가 출력 될 것이다. 추가해주자
목표
복잡한 검증 로직을 별도로 분리하자
<왜냐?>
컨트롤러에서 검증 로직이 차지하는 부분은 매우 크다.
이런 경우 별도의 클래스로 역할을 분리하는 것이
좋다. 그리고 이렇게 분리한 검증 로직을 재사용 할 수도 있다.
supports() {}
validate(Object target, Errors errors)
그래서 해당 컨트롤러에 검증로직은 따로 분리하여
itemValidator.validate(item, bindingResult);
이렇게 대체해버리고 본 코드는
@Component
public class ItemValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Item.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
Item item = (Item) target;
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "itemName", "required");
if (item.getPrice() == null || item.getPrice() < 1000
|| item.getPrice() > 1000000) {
errors.rejectValue("price", "range", new Object[]{1000, 1000000},
null);
}
if (item.getQuantity() == null || item.getQuantity() > 10000) {
errors.rejectValue("quantity", "max", new Object[]{9999}, null);
}
//특정 필드 예외가 아닌 전체 예외
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
errors.reject("totalPriceMin", new Object[]{10000,
resultPrice}, null);
}
}
}
}
물론 이렇게 사용해도 좋다. 그러나 스프링은 이런 도움을 안 줄까?
이 아이는 스프링의 '파라미터 바인딩' 역할을 한다. 그리고 내부에 '검증 기능'도 가지고 있다.
[ValidationItemControllerV2]에 코드 추가
@InitBinder
public void init(WebDataBinder dataBinder) {
log.info("init binder {}", dataBinder);
dataBinder.addValidators(itemValidator);
//원하면 여기에 (추가하고 싶은 검증기)로 추가하면 돼
}
이렇게
WebDataBinder
에 검증기를 추가하면 해당 컨트롤러에서는 검증기를 자동으로 적용할 수 있다.
@InitBinder
해당 컨트롤러에만 영향을 준다. 글로벌 설정은 별도로 해야한다.- 해당 컨트롤러가 호출되면 제일 먼저 얘부터 시작함. -> 그리고 매번 초기화 된다!!!!
그 다음
@PostMapping("/add")
public String addItemV6(@Validated @ModelAttribute Item item, BindingResult
bindingResult, RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
return "validation/v2/addForm";
}
//성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v2/items/{itemId}";
}
@Validated
가 붙었다.@Validated
는 검증기를 실행하라는 애노테이션이다.
1. 이 애노테이션이 붙으면 앞서 WebDataBinder
에 등록한 검증기를 찾아서 실행한다. (그리고 파라미터 바인딩을 해준다)
-> 그런데 여러 검증기를 등록한다면 그 중에 어떤 검증기가 실행되어야 할지 구분이 필요하다.
2. 이때 supports() 가 사용된다.
3. 여기서는 supports(Item.class)
호출되고,
4. 결과가 true 이므로 ItemValidator
의 validate()
가 호출된다.
@SpringBootApplication
public class ItemServiceApplication implements WebMvcConfigurer {
public static void main(String[] args) {
SpringApplication.run(ItemServiceApplication.class, args);
}
//모든 컨트롤러에 다 적용한다.
@Override
public Validator getValidator() {
return new ItemValidator();
}
}
근데 BeanValidator가 자동 등록되지 않으니 조심하자.
아 그리고
참고
- 검증시
@Validated
@Valid
둘다 사용가능하다.javax.validation.@Valid
를 사용하려면build.gradle
의존관계 추가가 필요하다.
implementation'org.springframework.boot:spring-boot-starter-validation'
@Validated
는 스프링 전용 검증 애노테이션이고,@Valid
는 자바 표준 검증 애노테이션이다