메시지, 국제화
하드코딩을 일일이 바꿀려면 번거러움 -> 메시지 프로퍼티로 관리(messages.properties)
나라마다 다른언어로 뿌려줘야함 -> 국제화
타임리프의 메시지 표현식 #{...}을 사용하면 편리하게 스프링 메시지 조회 가능
-> #{label.item}
국제화 -> 스프링이 언어 선택시 Accept-Language 헤더값을 사용 ->
messages_en.properties 파일을 만들어서 관리하면 홈페이지 언어 설정에 따라 자동으로 갈아낌
검증(Validation)
컨트롤러의 중요한 역할중 하나는 HTTP 요청이 정상인지 검증하는 것이다.
서버측에서 검증은 필수
th:if="${errors?.containsKey('globalError')}"
?문법: errors가 null일때 NullPointerException 대신 그냥 null을 반환하는 문법 -> th:if에서 null은 조건 불만족으로 처리
BindingResult
public String addItemV1(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model)
순서 중요 @ModelAttribute 뒤에 와야함
th:if="${#fields.hasGlobalErrors()}
-> #fields로 BindingResult 접근 가능
th:class="${errors?.containsKey('price')} ? 'form-control field-error' : 'form-control'"
-> th:errorclass="field-error"
이걸로 삼항연산자 해결가능
th:errors="*{itemName}"
-> 에러 메시지도 출력 가능
BindingResult는 @ModelAttribute에 받지 못해서 오류페이지가 난것을 받을수 있고 컨트롤러를 호출하고
오류메시지(Field Error) 출력가능
즉, BindingResult는 바인딩 실패와 실제 오류 두가지 모두 사용가능
오류 값 보존
bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false, null, null, "상품 이름은 필수 입니다"); -> Field 에러 값 보존하기위한 생성자
컨트롤러 오기전에 bindingResult에 넘어온값이 담긴다.
타임 리프의 th:field="*{price}"는 정상상황일때는 모델 객체값을 사용하지만, 오류가 발생하면 FieldError에서 보관한 값을 사용해서 값을 출력 (th:field의 위력)
오류메시지1
오류메시지도 메시지처럼 일관성있게 관리해야 유지 보수에 좋다 -> errors.properties에서 관리
bindingResult.addError(new FieldError("item", "price", item.getPrice(), false, new String[]{"range.item.price"}, new Object[]{1000, 1000000}, null));
-> 동적으로 메시지를 만들어서 사용(일반적)
오류메시지2
FieldError, ObjectError 너무 번거로움
사실 bindingResult는 타겟을 알고있다(뒤에 있으니까)
bindingResult.rejectValue("price", "range", new Object[]{1000, 1000000}, null);
-> 이런식으로 관례를 따르면 간편히 수정가능
오류메시지3
오류코드를 어떻게 작명해야하는가? (규칙에 따라, 오류명 + 객체명 + 필드명의 조합)
단순하게 만들면 범용성이 좋으나 구체적이지 않다.
반면에 너무 자세히 만들면 범용성이 떨어진다.
-> 범용성으로 쓰다가 특별한 경우를 단계별로 만들어주는게 좋다.
-> 이름이 같더라도 이름이 더 구체적이면 구체적인게 우선순위를 높게하면 범용성, 특수성 둘다 커버 가능
-> 개발 코드는 건드리지 않고 properties만으로 오류메시지 관리 가능
new String[]{"required.item.itemName", "required"}
스프링의 MessageCodesResolver가 이 기능을 지원
오류메시지4
MessageCodesResolver는 검증 오류코드(required)로 메시지 코드들을 생성함
bindingResult.rejectValue("price", "range", new Object[]{1000, 1000000}, null);
여기서 rejectValue 내부에서 자동으로 일어남
객체 오류
1. code + "." + object name
2. code
필드 오류
1. code + "." + object name + "." + field
2. code + "." + field
3. code + "." + field type
4. code
오류메시지5
오류 코드 관리 전략
구체적 -> 덜 구체적
errors.property에서 Level 별로 쓰고싶은 메시지를 입력
오류메시지6
1. 개발자가 직접 설정한 오류 메시지
2. 스프링이 직접 추가한 메시지(타입 정보가 맞지 않음, typeMismatch)
e.g.
typeMismatch.item.price
typeMismatch.price
typeMismatch.java.lang.Integer
typeMismatch
-> 따라서 마찬가지로 errors.properties를 수정해주면 스프링 오류메시지를 변경해서 사용가능하다.
Validator 분리1
컨트롤러에 너무 검증 코드가 많음
-> 별도의 클래스를 만들어 역할을 분리
@Component
public class ItemValidator implements Validator
Validator 분리2
스프링의 Validtor 인터페이스를 사용하면 체계적 검증이 가능
컨트롤러에 추가
@InitBinder
public void init(WebDataBinder dataBinder){
dataBinder.addValidators(itemValidator);
}
항상 검증기를 넣어줄수 있음-> @Validated 애노테이션으로 직접 적용이 가능(검증기를 실행하라는 의미)
-> 어떤 검증기 있지 어떻게 구분? -> Validator의 supports 메서드로 확인
public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model)
여기서 부터 중요
Bean Validation - 스프링 적용
꼭 @Valid나 @Validated를 붙여줘야함(이 애노테이션이면 앞의 수많은 과정 다 자동)
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model)
-> 스프링 부트는 LocalValidatorFactoryBean을 글로벌 Validator로 등록(객체 애노테이션 기반 검증기)
-> 검증 오류 발생시 FieldError, ObjectError를 생성해서 BindingResult에 담음
Bean Validation - 에러 코드
오류코드가 애노테이션 이름으로 등록됨(마치 typeMismatch)
예를 들어 NotBlank라는 오류 코드를 기반으로 MessageCodesResolver를 통해 메시지 코드가 생성
-NotBlank.item.itemName
-NotBlank.itemName
-NotBlank.java.lang.String
-NotBlank
errors.properties를 정의해주기만 하면됨
Bean Valiation - 오브젝트 오류
필드가 아닌 조합과 관련된 오브젝트 오류는?
@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000", message = "총합이 10000원 넘게 입력해 주세요")
-> 실무에서는 안쓰인다. 그냥 자바코드로 짤것
Bean Validation - 한계
수정할때는 조건을 바꾸면 싶다면? -> Item 객체의 애노테이션 -> 등록할때도 바껴버림
-> groups로 해결
Bean Validation - groups
껍데기 인터페이스 SaveForm, UpdateForm을 만든후
@NotBlank(groups = {SaveCheck.class, UpdateCheck.class})
private String itemName;
@Max(value = 9999 ,groups = {SaveCheck.class})
private Integer quantity;
적용되는 프로퍼티에 적용
public String addItemV2(@Validated(SaveCheck.class) @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model)
인터페이스에 따라 골라서 쓸수있음 @Validated 만 된다
-> 실무에서는 아예 폼 객체를 분리해서 등록과 수정을 다르게 사용함
Form 전송 객체 분리(★실무★)
이전에는 Item 객체를 그대로 사용했지만 실무에선 ItemSaveForm 같은 폼 전용 객체를 사용함(마치 DTO)
1. 폼 데이터가 복잡해서 전용 폼객체를 사용해서 데이터를 전달받을수 있음
2. 등록용, 수정용을 다로만들기 때문에 검증 충돌이 발생하지 않음(groups 사용할 필요없음)
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute("item") ItemSaveForm form, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model)
@ModelAttribute에 value 값을 안주면 itemSaveForm(객체명)으로 모델이 넘어가게됨
대신 엔티티를 그대로 쓰지 않기 때문에 컨트롤러에서 리포지토리 호출시 데이터 변환 필요
// 성공 로직
Item item = new Item();
item.setItemName(form.getItemName());
item.setPrice(form.getPrice());
item.setQuantity(form.getQuantity());
검증 애노테이션은 필요하면 검색해서 찾으면 왠만한 검증은 다 있다.
Bean Validation - HTTP 메시지 컨버터
@PostMapping("/add")
public @ResponseBody Object addItem(@RequestBody @Validated ItemSaveForm form, BindingResult bindingResult)
if(bindingResult.hasErrors()){
log.info("검증 오류 발생 errors={}", bindingResult);
return bindingResult.getAllErrors(); // ObjectError와 FieldError를 스프링이 Json으로 변환해서 반환
}
-> Json(API)으로 받을때도 사용가능
만약 price = "qqq"를 넣으면?
-> 원래는 HTTP 메시지 컨버터에 의해 객체로 바뀌는데 오류가 생겨버려서 컨트롤러를 호출하지 못함
@ModelAttribute는 되지 않았나?
@ModelAttribute vs @RequestBody
@ModelAttribute는 필드 단위로 각각 바인딩, 즉 특정 필드가 바인딩 안돼도 나머지 필드가 바인딩 -> 컨트롤러 호출가능
@RequestBody는 전체 객체 단위로 적용, 즉 HttpMessageConverter가 객체로 변환하는데 실패하면 -> 예외 발생 종료
(컨트롤러 호출X, Validator 적용X)
'웹 프로그래밍 > 스프링' 카테고리의 다른 글
자바 예외 이해 (0) | 2023.06.03 |
---|---|
스프링 데이터 접근 핵심 원리 (0) | 2023.05.08 |
스프링 MVC(1) 원리 정리 (0) | 2023.04.08 |
웹 애플리케이션 이해 (0) | 2023.04.05 |
빈 스코프 (0) | 2023.02.20 |