본문 바로가기

웹 프로그래밍/스프링

스프링 MVC(2) 검증 정리

메시지, 국제화

하드코딩을 일일이 바꿀려면 번거러움 -> 메시지 프로퍼티로 관리(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