일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 자바의 정석 기초편 ch11
- 자바의 정석 기초편 ch6
- 자바 기본편 - 다형성
- 자바의 정석 기초편 ch8
- 자바의 정석 기초편 ch12
- 2024 정보처리기사 시나공 필기
- 스프링 고급 - 스프링 aop
- 스프링 mvc2 - 로그인 처리
- 자바의 정석 기초편 ch13
- 스프링 db1 - 스프링과 문제 해결
- 스프링 입문(무료)
- 스프링 mvc1 - 서블릿
- 스프링 mvc2 - 타임리프
- 자바의 정석 기초편 ch3
- 자바의 정석 기초편 ch14
- 자바의 정석 기초편 ch5
- jpa 활용2 - api 개발 고급
- 자바의 정석 기초편 ch4
- 코드로 시작하는 자바 첫걸음
- 자바의 정석 기초편 ch2
- 자바의 정석 기초편 ch9
- 스프링 mvc1 - 스프링 mvc
- 자바의 정석 기초편 ch7
- 2024 정보처리기사 수제비 실기
- 자바의 정석 기초편 ch1
- 게시글 목록 api
- jpa - 객체지향 쿼리 언어
- 스프링 mvc2 - 검증
- 스프링 db2 - 데이터 접근 기술
- @Aspect
- Today
- Total
나구리의 개발공부기록
검증1 - Validation, 오류 코드와 메시지 처리1 ~ 6, Validator 분리1 ~ 2 본문
검증1 - Validation, 오류 코드와 메시지 처리1 ~ 6, Validator 분리1 ~ 2
소소한나구리 2024. 9. 4. 14:18 출처 : 인프런 - 스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
1. 오류 코드와 메시지 처리1
1) 에러 메시지 파일 생성
- 오류 메시지가 항목마다, 기능마다 모두 다르다면 규모가 큰 애플리케이션에서 수정시 매우 번거올 수 있으므로 일관성 있게 오류 메시지를 다루는 것이 좋음
- FieldError와 ObjectError의 생성자는 codes, arguments를 제공하는데 이것은 오류 발생 시 오류 코드로 메시지를 찾기위해 사용되며 이것을 활용
- 기존 messages.properties에 error.item 이런식으로 등록해도 되지만 오류 메시지를 구분하기 쉽게 별도의 파일로 관리하는것이 좋을 수 있음
- 참고로 errors_en.properties 이런식으로 파일을 생성하면 오류 메시지도 국제화 처리가 가능함
(1) errors.properties 생성 (~/resources/)
- required.item.itemName 처럼 이름이 규칙이 있어 보이는 것은 떡밥임
- 에러코드.오브젝트명.필드명
required.item.itemName=상품 이름은 필수입니다.
range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
max.item.quantity=수량은 최대 {0} 까지 허용합니다.
totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
(2) application.properties 추가
spring.messages.basename=messages, errors
2) 코드 수정
(1) ValidationItemControllerV2 - addItemV3() 추가
- codes의 값은 String[]로 받음 -> 배열의 값을 순차적으로 적용 해서 계속 못찾으면 defaultMessage값이 출력됨
- codes에도 없고 defaultMessage에도 없으면 당연히 에러페이지가 뜸(500에러)
- arguments의 값은 Object[]로 받음 -> 작성된 코드의 {0}, {1}을 순서대로 치환 적용
// 오류 메시지 처리1
@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,
// codes의 값은 String 배열로 받음 -> 순차적으로 적용됨
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,
// arguments값은 Object[]로 받음 -> 순서대로 적용
new String[] {"range.item.price"}, new Object[] {1000, 1000000},null));
}
if (item.getQuantity() == null || item.getQuantity() > 9999 || item.getQuantity() < 1) {
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));
}
}
// ... 이하 코드들
}
2. 오류 코드와 메시지 처리2
1) 오류 코드 자동화
(1) 로그 확인
- 파라미터도 많고 뭔가 다루기가 번거로운 FieldError, ObjectError를 조금더 자동화 하기
- 컨트롤러에서 BindingResult는 검증해야할 객체인 target 바로 다음에 위치함 -> BindingResult는 이미 본인이 검증해야 할 객체인 target을 알고 있음!
log.info("objectName={}", bindingResult.getObjectName());
log.info("target={}", bindingResult.getTarget());
// 아무값이나 저장해서 보면 결과가 나온다
objectName=item
target=Item(id=null, itemName=상품, price=12345, quantity=1)
(2) rejectValue(), reject()
void rejectValue(@Nullable String field, String errorCode,
@Nullable Object[] errorArgs, @Nullable String defaultMessage);
void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String, defaultMessage);
- BindingResult가 제공하는 rejectValue() - Field, reject() - Object를 사용하면 FieldError, ObjectError를 직접 생성하지 않고 검증 오류를 다룰 수 있음
- field: 오류 필드명
- errorCode: 오류 코드 (messages에 등록된 코드가 아닌 messageResolver를 위한 오류 코드 -> 뒤에서 설명)
- errorArgs: 오류 메시지에서 {0}을 치환하기 위한 값
- defaultMessage: 오류 메시지를 찾을 수 없을 때 사용하는 기본 메시지
2) 코드 수정
(1) ValidationItemcontrollerV2 - addItemV4() 추가
- rejectValue(), reject()에서는 오류코드를 range, required처럼 매우 간단하게 입력 했음에도 오류 메시지가 잘 출력됨
- 이부분을 이해하려면 MessageCodesResolver를 이해해야 함
// 오류 메시지 처리2
@PostMapping("/add")
public String addItemV4(@ModelAttribute Item item, BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
log.info("objectName={}", bindingResult.getObjectName());
log.info("target={}", bindingResult.getTarget());
// 필드에러는 rejectValue
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 || item.getQuantity() < 1) {
bindingResult.rejectValue("quantity", "max", new Object[]{9999}, null);
}
// 오브젝트 에러는 reject
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
}
// ... 이하 코드
}
3. 오류 코드와 메시지 처리3 - 오류코드의 설계
1) 오류 코드의 설계
(1) 설계
- 오류 메시지는 자세히 만들수도 단순하게 만들수도 있음
- 단순하게 만들면 범용성이 좋아서 여러 곳에서 사용할 수 있지만 메시지를 세밀하게 작성하기는 어려움
- 자세히 만든 오류 메시지는 범용성이 떨어짐
- 가장 좋은 방법은 범용성으로 사용하다가 세밀하게 작성해야 하는 경우 세밀한 내용이 적용하도록 단계를 두는 방법임
# 디테일한 메시지를 높은 우선순위로 사용
required.item.itemName: 상품 이름은 필수 입니다
...
# 범용적인 메시지를 낮은 우선순위로 사용
required: 필수 값 입니다.
(2) MessageCodesResolver
- 앞서 에러를 출력하는 메서드의 기능이 new String[]로 {"required.item.itemName", "required"} 오류 코드를 배열로 받게 된 것처럼 우선순위가 적용 된다면 추가적인 수정 요청이 오더라도 코드는 고치지 않고 properties 메시지만 관리하면 문제가 쉽게 해결 됨 -> 이렇게 범용성 있게 개발하는 것이 개발을 잘하는 것임
- 이러한 기능을 스프링이 지원하는데 그것이 바로 MessageCodesResolver임
4. 오류 코드와 메시지 처리4
1) 테스트 코드로 MessageCodesResolver를 알아보기
(1) MessageCodesResolverTest
- test하위에 itemservice.validation 경로에 작성
- MessageCodesResolver: 검증 오류 코드로 메시지코드를 생성
- MessageCodesResolver는 인터페이스이며 DefaultMessageCodesResolver는 기본 구현체임
- ObjectError, FieldError와 함께 사용
public class MessageCodesResolverTest {
// MessageCodesResolver - 인터페이스
// DefaultMessageCodesResolver - 구현체
MessageCodesResolver codesResolver = new DefaultMessageCodesResolver();
// ObjectError 테스트
@Test
void messageCodesResolverObject() {
// 에러코드명, 오브젝트명 입력
String[] messageCodes = codesResolver.resolveMessageCodes("required", "item");
for (String messageCode : messageCodes) {
System.out.println("messageCode = " + messageCode);
// 출력 결과
// messageCode = required.item - 1순위
// messageCode = required - 2순위
// 이렇게 우선순위가 디테일한 것부터 우선순위가 적용되는 것을 rejectValue(), reject()가 해주고 있는 것
}
// 검증
assertThat(messageCodes).containsExactly("required.item", "required");
}
// Field 테스트
@Test
void messageCodesResolverField() {
// 필드명, 필드의 타입 추가
String[] messageCodes = codesResolver.resolveMessageCodes("required", "item", "itemName", String.class);
for (String messageCode : messageCodes) {
System.out.println("messageCode = " + messageCode);
// 출력 결과
// messageCode = required.item.itemName - 1순위
// messageCode = required.itemName - 2순위
// messageCode = required.java.lang.String - 3순위(에러코드명.타입)
// messageCode = required - 4순위
}
assertThat(messageCodes).containsExactly(
"required.item.itemName",
"required.itemName",
"required.java.lang.String",
"required");
}
}
(1) DefaultMessageCodesResolver의 기본 메시지 생성 규칙
오류 종류 | 순서 |
객체 오류 | 1 : code + "." + object name 2 : code ex) 오류 코드 : required object name : item 1 : required.item 2 : required |
필드 오류 | 1 : code + "." + object name + "." + field 2 : code + "." + field 3 : code + "." + filed type 4 : code ex) 오류 코드 : typeMismatch object name : user field : age field type : int 1 : typeMismatch.user.age 2 : typeMismatch.age 3 : typeMismatch.int 4 : typeMismatch |
(2) 동작 방식
- rejectValue(), reject()는 내부에서 MessageCodesResolver를 사용하여 메시지 코드들을 생성함
- BindingResult의 로그를 통해서 확인해보면 MessageCodesResolver를 통해 생성된 순서대로 오류 코드를 보관하는 것을 확인할 수 있음
- 타임리프 화면을 렌더링 할 때 th:errors가 실행되고 오류가 있다면 오류 메시지 코드를 순서대로 돌아가면서 메시지를 찾고 없으면 디폴트 메시지를 출력함
5. 오류 코드와 메시지 처리5
1) 오류 코드 관리 전략
- 핵심은 구체적인 것에서 덜 구체적인 것으로 만드는 것 -> 정말 중요한 메시지 (구체적), 중요하지 않은 메시지(범용성있게)
- 이렇게 복잡하게 사용하는 이유는 모든 오류 코드에 대해 메시지를 다 정의하면 개발자 입장에서 관리하기가 너무 어려움
2) 오류 코드 전략 도입해보기
(1) errors.properties의 기존 코드는 주석처리하고 새롭게 작성
- Level1 ~ 4 단계를 순차적으로 주석을 적용하면서 테스트를 해보면 각 레벨단계의 오류 메시지가 출력됨
- Level4 까지 전부 주석처리하면 디폴트 메시지를 사용함
- 구체적인 것에서 덜 구체적인 순서대로 찾기 때문에 크게 중요하지 않은 오류 메시지는 기존에 정의된 것을 그냥 재활용 하면 됨
#required.item.itemName=상품 이름은 필수입니다.
#range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
#max.item.quantity=수량은 최대 {0} 까지 허용합니다.
#totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
# == Object Error ==
# 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} 까지 허용합니다.
2) ValidationUtils
(1) 검증 유틸리티
- if문으로 되어있던 검증로직을 1줄로 가능 - Empty, 공백 처리하는 if문을 간단하게 해주는 유틸리티
// ValidationUtils 사용 전
if (!StringUtilshasText(item.getTimeName())) {
bindingResult.rejectValue("itemName", "required");
}
// ValidationUtils 사용 후
ValidationUtils.rejectIfEmptyOrWhitespace(bindingResult, "itemName", "required");
6. 오류 코드와 메시지 처리6
1) 스프링이 직접 만든 오류 메시지 처리
(1) 기본 메시지
- price 필드에 문자를 입력해보면 스프링이 만든 에러 메시지가 출력되는데, 내가 직접 만들지 않아도 스프링은 타입오류가 발생하면 typeMismatch라는 오류코드를 사용해서 4가지 메시지 코드를 생성함
- 이렇게 오류 메시지가 길게 나온것은 메시지 코드를 입력해준 것이 없기 때문에 스프링이 기본으로 제공하는 메시지가 출력된 것임
- 해당 메시지는 스프링이 만든 것이기 때문에 컨트롤러 코드를 수정할 수 없는데 생성된 메시지 코드를 보고 직접 properties에 등록해주면 원하는 메시지로 등록할 수 있음
- log에 찍힌 메시지 코드를 보면 기존에 배웠던 형식과 똑같이 생성된 것을 확인 할 수 있음
typeMismatch.item.price,
typeMismatch.price,
typeMismatch.java.lang.Integer,
typeMismatch
(2) 스프링이 제공하는 기본 메시지를 직접 지정
- errors.properties에 스프링이 만들어낸 메시지 코드를 입력해서 적용하면 내가 지정한 오류 메시지가 출력됨
- 적용 순서는 배웠던 내용과 동일함
- 보통 bindingResult 에러처리는 컨트롤러 코드 앞에서 처리해 오류가 발생한 즉시 반환되도록 하긴하지만, 개발자가 판단에 따라서 오류를 2개 다 보여줄지, 1개만 보여줄지, if문으로 처리할지 정하면 됨
# 타입 에러 추가
typeMismatch.java.lang.Integer = 숫자를 입력해주세요.
typeMismatch = 타입 오류 입니다.
7. Validator 분리1
1) Validator 분리
(1) 검증 로직 분리
- 컨트롤러에서 검증 로직이 차지하는 부분은 매우 크기때문에 별도의 클래스로 역할을 분리하는 것이 좋음
- 분리한 검증로직은 재사용 할 수도 있음
- 잘 재사용하진 않지만 비슷한 검증 로직이 있다면 가능함
(2) ItemValidator 클래스 생성
- 스프링은 검증을 체계적으로 제공하기위해 Validator 인터페이스를 제공함
- supports(): 해당 검증기를 지원하는 여부
- validate(Object target, Errors errors): 검증 대상 객체와 BindingResult
// 스프링이 지원하는 Validator를 구현해서 사용
@Component
public class ItemValidator implements Validator {
@Override
public boolean supports(Class<?> aclass) {
return Item.class.isAssignableFrom(aclass);
// isAssignableFrom(): 자식 클래스까지 커버가 됨
// item == aclass
// item == subItem(자식 클래스)
}
@Override
public void validate(Object target, Errors errors) {
Item item = (Item) target;
if (!StringUtils.hasText(item.getItemName())) {
errors.rejectValue("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 || item.getQuantity() < 1) {
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);
}
}
}
}
(3) ValidationItemControllerV2 - addItemV5() 추가
- ItemValidator를 Component로 스프링 빈에 등록해서 Autowired로 불러와서 직접 호출
- Validator 인터페이스를 사용하지 않고 new ItemValidator()로 객체를 생성하여 사용해도 되지만 이렇게 스프링 기능을 사용하는 이유가 있음 -> Bean Validation의 강의에서 나옴
@Slf4j
@Controller
@RequestMapping("/validation/v2/items")
@RequiredArgsConstructor
public class ValidationItemControllerV2 {
// 생성자는 하나고 파라미터가 2개인 상태 -> Autowired로 주입
private final ItemRepository itemRepository;
private final ItemValidator itemValidator;
// ... 기존 코드 생략
// 검증로직을 ItemValidator라는 클래스로 분리
@PostMapping("/add")
public String addItemV5(@ModelAttribute Item item, BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
// 기존 검증로직은 모두 지우고 ItemValidator 클래스를 불러오기
// validate(target, errors)
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}";
}
// ... 기존 코드 생략
}
8. Validator 분리2
1) WebDataBinder를 통해서 사용하기
(1) 스프링의 추가적인 도움 받기
- Validator 인터페이스를 사용하면 스프링의 추가적인 도움을 받을 수 있음
- WebDataBinder는 스프링의 파라미터 바인딩 역할을 해주고 검증 기능도 내부에 포함됨
(2) ValidationItemControllerV2 - init() 추가
- WebDataBinder에 검증기를 추가하면 해당 컨트롤러(클래스)에서는 검증기를 자동으로 적용할 수 있음
- @InitBinder는 해당 컨트롤러에만 영향을 주며 전체 컨트롤러에 적용하는 설정은 별도로 해야함
public class ValidationItemControllerV2 {
// 생성자는 하나고 파라미터가 2개인 상태 -> Autowired로 주입
private final ItemRepository itemRepository;
private final ItemValidator itemValidator;
@InitBinder
public void init(WebDataBinder dataBinder) {
log.info("init binder {}", dataBinder);
dataBinder.addValidators(itemValidator);
}
//...
}
(3) ValidationItemControllerV2 - addItemV6()
- 검증 대상 앞에 @Validated만 넣어주고 기존에 ItemValidator를 불러오는 코드는 삭제하면 끝
- addItemV5와 동일하게 동작됨
@PostMapping("/add")
public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
// itemValidator.validate(item, bindingResult); 삭제
// ... 기존 코드 유지
}
(4) @Validated 동작 방식
- 검증기를 실행하라는 애노테이션
- 해당 애노테이션이 붙으면 WebDataBinder에 등록한 검증기를 찾아 실행
- 여러 검증기가 등록되었을 때 사용되는 것이 ItemValidator에서 오버라이딩하여 구현한 supports()임
- 각 검증기에 등록된 클래스 타입이 호출되고 결과가 true이면 각 검증기의 validate()가 호출되는 것
(5) 글로벌 설정 - 모든 컨트롤러에 다 적용
- 해당 프로젝트의 애플리케이션에 WebMvcConfigurer를 구현
- getValidator()를 오버라이딩하면 프로젝트의 전체 컨트롤러에 @Validated 애노테이션을 넣어서 검증을 사용할 수 있음
- 글로벌 설정을 하면 이후에 설명하는 BeanValidator가 자동으로 등록되지 않으니 BeanValidator강의를 진행할 때는 글로벌 설정 부분은 모두 주석처리 후 진행해야함
- 또한 글로벌 설정을 직접 사용하는 경우는 드묾
@SpringBootApplication
public class ItemServiceApplication implements WebMvcConfigurer {
public static void main(String[] args) {
SpringApplication.run(ItemServiceApplication.class, args);
}
@Override
public Validator getValidator() {
return new ItemValidator();
}
}
** 참고
- 검증시 @Valid도 사용가능한데 build.gradle 의존관계 추가가 필요함
-
implementation 'org.springframework.boot:spring-boot-starter-validation'
-
@Validated는 스프링 전용 검증 애노테이션이고 @Valid는 자바 표준 검증 애노테이션임