'스프링 MVC 2편 - 백엔드 웹 개발 활용 기술' 수업을 듣고 정리한 내용입니다.
🔔 목표
- 오류 메시지를 체계적으로 다루어보자.
FieldError 생성자
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 : 기본 오류 메시지
FieldError
, ObjectError
의 생성자는 errorCode
, arguments
를 제공한다.
이것은 오류 발생시 오류 코드로 메시지를 찾기 위해 사용된다.
✓ errors 메시지 파일 생성
messages.properties
를 사용해도 되지만, 오류 메시지를 구분하기 쉽게 errors.properties
라는 별도의 파일로 관리해보자.
✓ 스프링 부트 메시지 설정 추가
application.properties
spring.messages.basename=messages,errors
messages.properties
, errors.properties
두 파일을 모두 인식한다. (사용한다.)messages.properties
를 기본으로 인식한다.
✓ errors.properties 추가
src/main/resources/errors.properties
required.item.itemName=상품 이름은 필수입니다.
range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
max.item.quantity=수량은 최대 {0} 까지 허용합니다.
totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
💡 참고
properties에서 한국어 입력처리가 안될 때
ex) 한국 → ?? 로 출력됨
이때는
Global
,Project
,Default
를 UTF-8로 맞추고Transparent native ~
를 체크 한다.
errors_en.properties
파일을 생성하면 오류 메시지도 국제화 처리를 할 수 있다.
ValidationItemControllerV2 - addItemV3() 추가
errors 에 등록한 메시지를 사용하도록 코드를 변경
@PostMapping("/add")
public String addItemV3(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
// 검증 로직
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.addError(new FieldError("item", "itemName", item.getItemName(),
false, new String[]{"required.item.itemName"}, null, null));
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
bindingResult.addError(new FieldError("item", "price", item.getPrice(), false, new String[]{"range.item.price"}, new Object[]{1000, 100000}, null));
}
if (item.getQuantity() == null || item.getQuantity() >= 9999) {
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));
}
}
// 검증에 실패하면 다시 입력 폼으로
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}";
}
//range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
new FieldError("item", "price", item.getPrice(), false, new String[] {"range.item.price"}, new Object[]{1000, 1000000}
codes :
required.item.itemName
를 사용해서 메시지 코드를 지정한다.- 메시지 코드는 하나가 아니라 배열로 여러 값을 전달할 수 있는데, 순서대로 매칭해서 처음 매칭되는 메시지가 사용된다.
arguments
Object[]{1000, 1000000}
를 사용해서 코드의{0}
,{1}
로 치환할 값을 전달한다.
실행
MessageSource
를 찾아서 메시지를 조회하는 것을 확인할 수 있다.
🔔 목표
FieldError
,ObjectError
는 다루기 너무 번거롭다.- 더 편리하게 사용할 수 있는 방법을 알아보자!
log.info("objectName={}", bindingResult.getObjectName());
log.info("target={}", bindingResult.getTarget());
컨트롤러에서 BindingResult
는 검증해야 할 객체인 target
바로 다음에 온다.
BindingResult
는 이미 본인이 검증해야 할 객체인 target
을 알고 있다.
출력 결과
objectName=item //@ModelAttribute name
target=Item(id=null, itemName=상품, price=100, quantity=1234)
rejectValue()
,reject()
BindingResult
가 제공하는rejectValue()
,reject()
를 사용하면FieldError
,ObjectError
를 직접 생성하지 않고, 깔끔하게 검증 오류를 다룰 수 있다.
ValidationItemControllerV2 - addItemV4() 추가
rejectValue()
,reject()
를 사용해서 기존 코드를 단순화해보자.
@PostMapping("/add")
public String addItemV4(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
log.info("objectName={}", bindingResult.getObjectName());
log.info("target={}", bindingResult.getTarget());
// 검증 로직
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.rejectValue("itemName","required");
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
bindingResult.rejectValue("price","range", new Object[]{1000, 1000000}, null);
}
if (item.getQuantity() == null || item.getQuantity() >= 9999) {
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);
}
}
// 검증에 실패하면 다시 입력 폼으로
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}";
}
실행
errors.properties
에 있는 코드를 직접 입력하지 않았는데 오류 메시지가 정상 출력된다.
void rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);
field
: 오류 필드명errorCode
: 오류 코드(이 오류 코드는 메시지에 등록된 코드가 아니다. messageResolver
를 위한 오류 코드이다.)errorArgs
: 오류 메시지에서 {0}
을 치환하기 위한 값 defaultMessage
: 오류 메시지를 찾을 수 없을 때 사용하는 기본 메시지
bindingResult.rejectValue("price", "range", new Object[]{1000, 1000000}, null)
BindingResult
는 어떤 객체를 대상으로 검증하는지 target
을 이미 알고 있다고 했다.target(item)
에 대한 정보는 없어도 된다. price
를 사용했다.
FieldError()
를 직접 사용했을 때는range.item.price
와 같이 모두 입력했다.rejectValue()
를 직접 사용할때는range
로 간단하게 입력했다. 그래도 오류 메시지를 잘 찾아서 출력한다.
errors.properties
range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
✓ reject()
void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);
errorCode
: 오류 코드(이 오류 코드는 메시지에 등록된 코드가 아니다. messageResolver
를 위한 오류 코드이다.)errorArgs
: 오류 메시지에서 {0}
을 치환하기 위한 값 defaultMessage
: 오류 메시지를 찾을 수 없을 때 사용하는 기본 메시지
오류 코드, 자세히 vs 단순 만들 수 있다.
(1) 자세히
required.item.itemName
: 상품 이름은 필 수 입니다.range.item.price
: 상품의 가격 범위 오류 입니다.
➡️ 자세하게 만들면 범용성이 떨어진다.
➡️ 자세하기 때문에 모든 경우마다 만들어줘야하기에 비효율적이다.(2) 단순
required
: 필수 값 입니다.range
: 범위 오류 입니다.
➡️ 단순하게 만들면 범용성이 좋아서 여러 곳에서 사용할 수 있지만, 메시지를 세밀하게 작성하기 어렵다.
➡️ 비슷한 에러가 여러 개 발생하면 어디에서 발생한 에러인지 바로 파악하기 어렵다.❗️가장 좋은 방법 : 범용성으로 사용하다가, 세밀하게 작성해야 하는 경우에는 세밀한 내용이 적용되도록 메시지에 단계를 두는 방법이다❗️
예를 들면)
required
라고 오류 코드를 사용한다고 가정해보자.required
라는 메시지만 있으면 이 메시지를 선택해서 사용하는 것이다.required: 필수 값 입니다.
그런데 오류 메시지에 required.item.itemName
와 같이 객체명과 필드명을 조합한 세밀한 메시지 코드가 있으면 이 메시지를 높은 우선순위로 사용하는 것이다.
#Level1
required.item.itemName: 상품 이름은 필수 입니다.
#Level2
required: 필수 값 입니다.
➡️ 조금 더 설명하면, required
가 출력되도록 하다가, 특별하게 itemName
값이 채워지지 않는 경우에는 더 구체적인 required.item.itemName
이 출력되도록 하는 것이다❗️
💡 참고
- 물론 이렇게 객체명과 필드명을 조합한 메시지가 있는지 우선 확인하고, 없으면 좀 더 범용적인 메시지를 선택하도록 추가 개발을 해야겠지만, 범용성 있게 잘 개발해두면, 메시지의 추가 만으로 매우 편리하게 오류 메시지를 관리할 수 있을 것이다.
- 스프링은
MessageCodesResolver
라는 것으로 이러한 기능을 지원한다.
테스트 코드로
MessageCodesResolver
를 알아보자!
MessageCodesResolverTest
package hello.itemservice.validation;
import org.junit.jupiter.api.Test;
import org.springframework.validation.DefaultMessageCodesResolver;
import org.springframework.validation.MessageCodesResolver;
import static org.assertj.core.api.Assertions.*;
public class MessageCodesResolverTest {
MessageCodesResolver codesResolver = new DefaultMessageCodesResolver();
@Test
void messageCodesResolverObject(){
// 객체 오류
String[] messageCodes = codesResolver.resolveMessageCodes("required", "item");
// for (String messageCode : messageCodes) {
// System.out.println("messageCode = " + messageCode);
// }
assertThat(messageCodes).containsExactly("required.item", "required");
// new ObjectError("item", new String[]{"required.item", "required"});
}
@Test
void messageCodesResolverField(){
// 필드 오류
String[] messageCodes = codesResolver.resolveMessageCodes("required", "item", "itemName", String.class);
for (String messageCode : messageCodes) {
System.out.println("messageCode = " + messageCode);
}
// BindingResult.rejectValue("itemName","required");
// new FieldError("item","itemName",null,false,messageCodes,null,null);
assertThat(messageCodes).containsExactly(
"required.item.itemName",
"required.itemName",
"required.java.lang.String",
"required"
);
}
}
MessageCodesResolve
MessageCodesResolver codesResolver = new DefaultMessageCodesResolver();
MessageCodesResolver
인터페이스이고 DefaultMessageCodesResolver
는 기본 구현체이다.ObjectError
, FieldError
와 함께 사용한다.
실행 결과
객체 오류의 경우 다음 순서로 2가지 생성
1.: code + "." + object name
2.: code
예) 오류 코드: required, object name: item
1.: required.item
2.: required
필드 오류의 경우 다음 순서로 4가지 메시지 코드 생성
1.: code + "." + object name + "." + field
2.: code + "." + field
3.: code + "." + field type
4.: code
예) 오류 코드: typeMismatch, object name "user", field "age", field type: int
1. "typeMismatch.user.age"
2. "typeMismatch.age"
3. "typeMismatch.int"
4. "typeMismatch"
rejectValue()
, reject()
는 내부에서 MessageCodesResolver
를 사용한다. 여기에서 메시지 코드들을 생성한다.FieldError
, ObjectError
의 생성자를 보면, 오류 코드를 하나가 아니라 여러 오류 코드를 가질 수 있다. MessageCodesResolver
를 통해서 생성된 순서대로 오류 코드를 보관한다.BindingResult
의 로그를 통해서 확인해보자.codes [range.item.price, range.price, range.java.lang.Integer, range]
✓ FieldError rejectValue("itemName", "required")
required.item.itemName
required.itemName
required.java.lang.String
required
➡️ 위 4가지 오류 코드를 자동으로 생성한다.
ObjectError reject("totalPriceMin")
totalPriceMin.item
totalPriceMin
➡️ 위 2가지 오류 코드를 자동으로 생성한다.
✓ 오류 메시지 출력
th:errors
가 실행된다.
📣 구체적인 것에서! 덜 구체적인 것으로!
MessageCodesResolver
는required.item.itemName
처럼 구체적인 것을 먼저 만들어주고,required
처럼 덜 구체적인 것을 가장 나중에 만든다.- 이렇게 하면 앞서 말한 것 처럼 메시지와 관련된 공통 전략을 편리하게 도입할 수 있다.
📣 왜 이렇게 복잡하게 사용하는가?
- 모든 오류 코드에 대해서 메시지를 각각 다 정의하면 개발자 입장에서 관리하기 너무 힘들다.
- 크게 중요하지 않은 메시지는 범용성 있는
requried
같은 메시지로 끝낸다.- 정말 중요한 메시지는 꼭 필요할 때 구체적으로 적어서 사용하는 방식이 더 효과적이다.
errors.properties
#required.item.itemName=상품 이름은 필수입니다.
#range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
#max.item.quantity=수량은 최대 {0} 까지 허용합니다.
#totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
#==ObjectError==
#Level1
totalPriceMin.item=상품의 가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
#Level2 - 생략
totalPriceMin=전체 가격은 {0}원 이상이어야 합니다. 현재 값 = {1}
#==FieldError==
#Level1
required.item.itemName=상품 이름은 필수입니다.
range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
max.item.quantity=수량은 최대 {0} 까지 허용합니다.
#Level2 - 생략
#Level3
required.java.lang.String = 필수 문자입니다.
required.java.lang.Integer = 필수 숫자입니다.
min.java.lang.String = {0} 이상의 문자를 입력해주세요.
min.java.lang.Integer = {0} 이상의 숫자를 입력해주세요.
range.java.lang.String = {0} ~ {1} 까지의 문자를 입력해주세요.
range.java.lang.Integer = {0} ~ {1} 까지의 숫자를 입력해주세요.
max.java.lang.String = {0} 까지의 숫자를 허용합니다.
max.java.lang.Integer = {0} 까지의 숫자를 허용합니다.
#Level4
required = 필수 값 입니다.
min= {0} 이상이어야 합니다.
range= {0} ~ {1} 범위를 허용합니다.
max= {0} 까지 허용합니다.
itemName
의 경우required
검증 오류 메시지가 발생하면 다음 코드 순서대로 메시지가 생성된다.
(1)required.item.itemName
(2)required.itemName
(3)required.java.lang.String
(4)required
그리고 이렇게 생성된 메시지 코드를 기반으로 순서대로 MessageSource
에서 메시지에서 찾는다.
구체적인 것에서 덜 구체적인 순서대로 찾는다. 메시지에 1번
이 없으면 2번
을 찾고, 2번
이 없으면 3번
을 찾는다.
➡️ 이렇게 되면 만약에 크게 중요하지 않은 오류 메시지는 기존에 정의된 것을 그냥 재활용 하면 된다!
실행
Level1
전부 주석
Level2, 3
까지 전부 주석
Level4
까지 전부 주석
➡️ 못찾으면 코드에 작성한 디폴트 메시지를 사용한다.
Object 오류
도 Level1
, Level2
로 재활용 가능하다.
레벨이 낮은 것을 주석해보면 점점 범용성이 큰 메시지가 출력되는 것을 발견할 수 있다.
ValidationUtils 사용 전
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.rejectValue("itemName", "required", "기본: 상품 이름은 필수입니다.");
}
ValidationUtils 사용 후
제공하는 기능은
Empty
,공백
같은 단순한 기능만 제공
ValidationUtils.rejectIfEmptyOrWhitespace(bindingResult, "itemName", "required");
📌 정리
(1)rejectValue()
호출
(2)MessageCodesResolver
를 사용해서 검증 오류 코드로 메시지 코드들을 생성
(3)new FieldError()
를 생성하면서 메시지 코드들을 보관
(4)th:erros
에서 메시지 코드들로 메시지를 순서대로 메시지에서 찾고, 노출
📣 검증 오류 코드 종류
- 개발자가 직접 설정한 오류 코드 →
rejectValue()
를 직접 호출- 스프링이 직접 검증 오류에 추가한 경우(주로 타입 정보가 맞지 않음)
이제 메시지 코드 전략의 강점을 확인해보자!
price 필드에 문자 "A"를 입력해보자.
errors.properties
에 타입 오류와 관련된 메시지 코드를 작성하지 않았기 때문에, 스프링이 생성한 기본 메시지가 출력된다.BindingResult
에 FieldError
가 담겨있고, 다음과 같은 메시지 코드들이 생성된 것을 확인할 수 있다.codes[typeMismatch.item.price,typeMismatch.price,typeMismatch.java.lang.Integer,typeMismatch]
4가지 메시지 코드가 입력되어 있다.
typeMismatch.item.price
typeMismatch.price
typeMismatch.java.lang.Integer
typeMismatch
typeMismatch
라는 오류 코드를 사용한다.MessageCodesResolver
를 통하면서 4가지 메시지 코드가 생성된 것이다.
실행
errors.properties
에 메시지 코드가 없기 때문에 스프링이 생성한 기본 메시지가 출력된다.
error.properties
에 내용 추가
# 추가
typeMismatch.java.lang.Integer=숫자를 입력해주세요.
typeMismatch=타입 오류입니다.
다시 실행해보자
결과적으로 소스코드를 하나도 건들지 않고, 원하는 메시지를 단계별로 설정할 수 있다.
원하는 메시지가 출력된다.
📌 정리
메시지 코드 생성 전략은 그냥 만들어진 것이 아니다. 조금 뒤에서Bean Validation
을 학습하면 그 진가를 더 확인할 수 있다.
🔔 목표
복잡한 검증 로직을 별도로 분리하자.
ItemValidator를 추가하자!
package hello.itemservice.web.validation;
import hello.itemservice.domain.item.Item;
import org.springframework.stereotype.Component;
import org.springframework.validation.*;
@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() >= 9999) {
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);
}
}
}
}
스프링은 검증을 체계적으로 제공하기 위해 다음 인터페이스를 제공한다.
public interface Validator {
boolean supports(Class<?> clazz);
void validate(Object target, Errors errors);
}
supports() {}
: 해당 검증기를 지원하는 여부 확인(뒤에서 설명) validate(Object target, Errors errors)
: 검증 대상 객체와 BindingResult
ItemValidator 직접 호출하기
ValidationItemControllerV2 - addItemV5()
private final ItemValidator itemValidator;
@PostMapping("/add")
public String addItemV5(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
itemValidator.validate(item, bindingResult);
// 검증에 실패하면 다시 입력 폼으로
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}";
}
ItemValidator
를 스프링 빈으로 주입 받아서 직접 호출했다.실행
- 스프링이
Validator
인터페이스를 별도로 제공하는 이유는 체계적으로 검증 기능을 도입하기 위해서다.- 앞에서는 검증기를 직접 불러서 사용했다.
Validator
인터페이스를 사용해서 검증기를 만들면 스프링의 추가적인 도움을 받을 수 있다.
WebDataBinder
는 스프링의 파라미터 바인딩의 역할을 해주고 검증 기능도 내부에 포함한다.
ValidationItemControllerV2
에 다음 코드를 추가하자 (검증기 추가)
@InitBinder
public void init(WebDataBinder dataBinder) {
log.info("init binder {}", dataBinder);
dataBinder.addValidators(itemValidator);
}
WebDataBinder
에 검증기를 추가하면 해당 컨트롤러에서는 검증기를 자동으로 적용할 수 있다.@InitBinder
: 해당 컨트롤러에만 영향을 준다. 글로벌 설정은 별도로 해야한다.
ValidationItemControllerV2 - addItemV6()
@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}";
}
validator
를 직접 호출하는 부분이 사라지고, 대신에 검증 대상 앞에 @Validated
가 붙었다.
실행
동작 방식(@Validated 적용)
@Validated
는 검증기를 실행하라는 애노테이션이다.WebDataBinder
에 등록한 검증기를 찾아서 실행한다.supports()
가 사용된다.
supports(Item.class)
호출되고, 결과가 true
이므로 ItemValidator
의 validate()
가 호출된다.@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) {...}
}
글로벌 설정 - 모든 컨트롤러에 다 적용
@SpringBootApplication
public class ItemServiceApplication implements WebMvcConfigurer {
public static void main(String[] args) {
SpringApplication.run(ItemServiceApplication.class, args);
}
@Override
public Validator getValidator() {
return new ItemValidator();
}
}
@InitBinder
를 제거해도 글로벌 설정으로 정상 동작하는 것을 확인할 수 있다. (이어지는 다음 강의를 위해서 글로벌 설정은 꼭 제거해두자.)
⚠️ 주의
- 글로벌 설정을 하면 다음에 설명할
BeanValidator
가 자동 등록되지 않는다.- 글로벌 설정 부분은 주석처리 해두자.
- 참고로 글로벌 설정을 직접 사용하는 경우는 드물다.
💡 참고
검증시
@Validated
,@Valid
둘다 사용가능하다.
javax.validation.@Valid
를 사용하려면build.gradle
의존관계 추가가 필요하다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
@Validated
: 스프링 전용 검증 애노테이션@Valid
는 자바 표준 검증 애노테이션자세한 내용은
Bean Validation
에서 공부한다!
참고