[Spring MVC 2편] 5. Bean Validation

HJ·2023년 1월 17일
0

Spring MVC 2편

목록 보기
5/13

김영한 님의 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard


1. Bean Validation

1-1. Bean Validation 소개

  • if문을 이용해서 작성하던 검증 로직을 간편하게 작성할 수 있고 모든 프로젝트에 적용할 수 있도록 공통화하고 표준화한 것이 Bean Validation

  • Bean Validation은 구현체가 아닌 기술 표준

    • 여러 어노테이션과 인터페이스의 모음

    • jakarta.validation-api : Bean Validation 인터페이스

    • hibernate-validator : 일반적으로 사용하는 구현체

  • Bean Validation을 사용하려면 아래 의존관계를 추가해야한다

    • implementation 'org.springframework.boot:spring-boot-starter-validation'
  • 검증 애노테이션 모음


1-2. Bean Validation 어노테이션 적용하기

import org.hibernate.validator.constraints.Range;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@Data
public class Item {

    private Long id;

    @NotBlank
    private String itemName;

    @NotNull
    @Range(min=1000, max=1000000)
    private Integer price;

    @NotNull
    @Max(9999)
    private Integer quantity;
}
  • 검증 어노테이션

    • @NotBlank : 빈 값 + 공백만 있는 경우를 허용하지 않는다

    • @NotNull : null을 허용하지 않는다

    • @Range(min=, max=) : 최대, 최소 범위 지정

    • @Max : 최댓값 지정

  • import를 보면 @NotBlank@NotNulljavax.validation이고 @Rangehibernate.validator인 것을 확인할 수 있다

    • @NotBlank@NotNull은 Bean Validation이 표준적으로 제공하기 때문에 모든 구현체에서 사용 가능한 어노테이션이다

    • @Rangehibernate.validator에서만 동작한다


1-3. Bean Validation 테스트 코드

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
  • 검증기 생성하는 코드

  • 스프링과 통합하면 직접 이런 코드를 작성하지 않기 때문에 참고만


Set<ConstraintViolation<Item>> violations = validator.validate(item);
  • 검증기를 실행하는 코드

  • 검증 대상인 item을 검증기에 넣고 결과를 반환

  • Set에 ConstraintViolation 검증 오류가 담긴다

  • Set이 비어있다면 오류가 발생하지 않은 것이다

  • violation이 가진 메세지는 hibernate validator에서 기본적으로 제공하는 메세지

    • @NotNull(message="공백 불가") 처럼 작성하면 원하는 메세지로 바꿀 수 있다



2. 스프링에서 Bean Validation 동작 원리

  • validation 의존관계를 추가하면 스프링부트가 자동으로 Bean Validator를 인지하고 스프링에 통합

  • 그렇게되면 스프링부트는 LocalValidatorFactoryBean을 글로벌 Validator로 등록

  • 글로벌 Validator가 적용되어 있기 때문에 검증 대상에 @Valid@Validated만 적용하면 Validator는 @NotNull과 같은 어노테이션을 보고 검증을 수행한다

  • 검증 오류가 발생하면 FieldError, ObjectError를 생성해서 BindingResult에 담아준다

  • 즉, Controller에 지난 게시글에서 했던 ItemValidator를 주입받고 WebDataBinder에 등록하는 코드가 없어도 동작한다

  • 단> 지난 게시글의 마지막 부분처럼 직접 글로벌 Validator를 적용하면 스프링부트는 Bean Validator를 글로벌 Validator로 등록하지 않아 동작하지 않는다




3. FieldError

3-1. 검증 순서

  • @ModelAttribute가 각각의 필드에 타입 변환 시도

    • 실패하면 typeMisMatchFieldError 추가

    • 성공하면 Validator 적용

  • 즉, 바인딩에 성공한 필드만 Bean Validation을 적용해 어노테이션을 통한 검증을 수행한다 ( 값이 정상적으로 들어와야 검증이 의미가 있기 때문에 )

    • 바인딩에 성공해 검증 수행을 하던 중에 검증 오류가 발생하면 FieldError를 추가한다 ( 2번 참고 )

3-2. 검증 오류 처리

  • FieldError 객체를 만들 때 어노테이션 이름을 errorCode 로 사용한다

    • 즉, MessageCodesResolver가 어노테이션 이름을 errorCode 로 사용해 메세지 코드를 만들어낸다

    • ex> @NotBlank가 지켜지지 않은 경우 ➜ NotBlank.item.itemName, NotBlank.item 등과 같이 메세지 코드가 생성 ( 지난 게시글 참고 )

  • Bean Validation이 메세지를 찾는 순서는 아래와 같다

    1. 생성된 메시지 코드 순서대로 messageSource에서 메시지 찾기

    2. 어노테이션의 message 속성 사용

    3. 라이브러리가 제공하는 기본 값 사용

  • 즉, 생성된 메세지 코드를 메세지 파일( messageSource )에서 관리하면 가장 먼저 찾아지기 때문에 원하는 메세지를 출력할 수 있다

  • 참고> 이전 게시글에 있는 내용이지만 FieldError는 codes 파라미터에 위와 같은 메세지 코드 조합을 String[] 형태로 받는다




4. ObjectError 처리

@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000", message="총합이 10000원 넘게 입력해주세요")
public class Item {
    ...
}
  • 검증 대상에 @ScriptAssert 어노테이션을 붙이면 ObjectError를 처리할 수 있다

  • 생성되는 메세지 코드는 ScriptAssert.item, ScriptAssert 이다

  • but> 사용에 제약이 많고, 실제 실무에서는 객체의 범위를 넘어서는 경우가 많은데 이럴 때 대응하기 어렵다

  • ➡️ ObjectError를 처리할 때는 @ScriptAssert를 사용하는 것보다 자바 코드로 작성하는 것을 권장




5. Bean Validation 한계점

  • 등록할 때와 수정할 때의 검증 요구사항이 다르면 같은 객체에서 검증 조건이 충돌

  • 결과적으로 등록과 수정은 같은 Bean Validation을 적용할 수 없다

  • 해결 방법 1 : Bean Validation 의 groups 기능을 사용

  • 해결 방법 2 : Item 객체를 직접 사용하지 않고, ItemSaveForm, ItemUpdateForm 같은 폼 전송을 위한 별도의 모델 객체를 만들어서 사용




6. Bean Validation - groups

  1. 인터페이스 생성 ( ex> SaveCheck, UpdateCheck )

  2. 객체 필드에 어노테이션을 붙일 때 groups 속성에 인터페이스 이름을 지정

    • 등록, 수정 모두에 사용되는 필드 : @NotBlank(groups = {SaveCheck.class, UpdateCheck.class})

    • 수정만 사용하는 필드의 경우 : @NotNull(groups = UpdateCheck.class)

  3. Controller 에서 @Validated 를 붙일 때 등록인지, 수정인지 명시

    • 등록 메서드의 경우 : @Validated(SaveCheck.class)

    • 수정 메서드의 경우 : @Validated(UpdateCheck.class)

  • @Valid에는 groups 기능이 없기 때문에 @Validated를 사용해야함



7. Form 전송 객체 분리

  • 문제점

    • 등록, 수정에 필요한 데이터는 서로 다르다

    • 이런 데이터들이 도메인 객체와 정확하게 맞지 않는다

  • 해결

    • Form을 전달 받는 전용 객체를 만들어 @ModelAttribute로 사용

      • Form 별로 필요한 필드와 필드에 붙는 어노테이션이 다름
    • 이를 통해 Controller에서 데이터를 전달받고, 필요한 데이터를 사용해서 객체( 도메인 객체 )를 생성

      • form 객체의 getXXX()를 이용하여 도메인 객체를 생성

  • 폼 데이터 전달에 도메인 객체 사용

    • @ModelAttribute Item item

    • HTML Form ➜ 도메인 객체 ➜ Controller ➜ 도메인 객체 ➜ Repository

  • 폼 데이터 전달을 위한 별도의 객체 사용

    • @ModelAttribute ItemSavdForm form

    • HTML Form ➜ Form 객체 ➜ Controller ➜ 도메인 객체 생성 ➜ Repository




8. Bean Validation - HTTP Message Convertor

8-1. @RequestBody vs @ModelAttribute

  • @Valid , @Validated 는 HttpMessageConverter ( @RequestBody )에도 적용할 수 있다
  • @ModelAttribute

    • HTTP 요청 파라미터( URL 쿼리 스트링, POST Form )를 다룰 때 사용

    • 필드 단위로 바인딩이 적용되기 때문에 특정 필드가 바인딩 되지 않아도 나머지 필드는 정상 바인딩 되고, Validator를 사용한 검증도 적용할 수 있다

  • @RequestBody

    • HTTP Message Body의 데이터를 객체로 변환할 때 사용

    • 필드 단위로 적용하는 것이 아니라 전체 객체 단위로 적용

    • 즉, HttpMessageConverter 단계에서 JSON 데이터를 객체로 변경하지 못하면 이후 단계 자체가 진행되지 않고 예외가 발생

    • 예외가 발생하면 컨트롤러도 호출되지 않고, Validator도 적용할 수 없다

    • 예외가 발생했을 때 원하는 모양으로 처리할 수 있다 ( 이후 게시글 참고 )


8-2. @RequestBody 적용

@RestController
public class ValidationItemApiController {

    @PostMapping("/add")
    public Object addItem(@RequestBody @Validated ItemSaveForm form, BindingResult bindingResult) {

        log.info("API 컨트롤러 호출");

        if (bindingResult.hasErrors()) {
            log.info("검증 오류 발생 errors={}", bindingResult);
            return bindingResult.getAllErrors();
        }

        log.info("성공 로직 실행");
        return form;
    }
}
  • ex1> price에 문자를 입력한 경우 ( 타입 변환 오류 )

    • Controller가 호출되지 않는다

    • HttpMessageConverter에서 요청 JSON을 ItemSaveForm 객체로 생성하는데 실패

    • JSON 데이터로 ItemSaveForm 객체를 만들어야 Controller를 호출하는데 변환 자체가 실패해서 호출되지 않는 것임

  • ex> 수량 최대 범위 초과해서 입력한 경우 ( 검증 오류 )

    • JSON을 객체로 생성하는건 성공헀지만 검증에서 실패

    • 이 경우, Controller는 호출된다

  • 참고> @RestController = @Controller + @ResponseBody
profile
공부한 내용을 정리해서 기록하고 다시 보기 위한 공간

0개의 댓글