Spring Bean을 검증하는 방법
Spring Bean을 검증하는 방법에는 어떤 것들이 있을까?
- 코드로 검증하기
- Bean Validation
코드로 검증한다고 한다면?
다음과 같은 객체가 있다고 가정하자.
public class Item {
private final String itemName;
private final int price;
}
그리고 다음과 같은 검증이 필요하다고 가정해보자.
- itemName은 null값이 올 수 없다.
- itemName은 2자 이상, 30자 이하의 값이 와야한다.
- price는 null값이 올 수 없다.
- price 는 1000이상이어야한다….
이러한 과정을 모두 코드로 작성한다면?
코드가 복잡해질 수 있다.
Bean Validation을 활용한다면 어노테이션 하나로 검증 로직을 매우 편리하게 적용할 수 있다.
Bean Validation이란?
Bean Validation이란 특정 구현체가 아니라 Bean Validation 2.0(JSR-380)이라는 기술 표준이다.
즉, 검증 어노테이션과 여러 인터페이스의 모음이다.
마치 JPA가 표준 기술이고 그 구현체로 하이버네이트가 있는 것과 같다.
Bean Validation을 구현한 기술중에 일반적으로 사용하는 구현체는 하이버네이트 Validator이다.
이름이 하이버네이트가 붙어서 그렇지 ORM과는 관련이 없다.
public class Item {
@NotNull
private final String itemName;
@NotNull
private final int price;
}
다음과 같이 어노테이션으로 검증코드를 대체하는 것이다.
그렇다면 이 BeanValidation 어노테이션에는 두가지가 있다.
@Validated vs @Valid
- @Valid는 자바 표준 검증 어노테이션이다. @Valid를 사용하려면 build.gradle파일에implementation ‘org.springframework.boot:spring-boot-starter-validation’의존성 추가가필요하다.
- @Validated는 스프링 전용 검증 어노테이션이다. 내부에 groups라는 기능을 포함하고 있다.
spring-boot-starter-validation 의존관계를 추가하면 라이브러리가 추가된다.
Jakarta Bean Validation
- jakarta.validation-api: Bean Validation 인터페이스
- hibernate-validator 구현체
@Valid동작 원리
스프링은 어떻게 Bean Validator를 사용할까?
스프링 부트가 spring-boot-starter-validation 라이브러리를 넣으면 자동으로 Bean Validator를 인지하고 스프링에 통합한다.
스프링부트는 자동으로 LocalValidatorFactoryBean 을 글로벌 Validator로 등록한다.
이 Validator는 @NoNull 같은 어노테이션을 보고 검증을 수행한다. 이렇게 글로벌 Validator가 적용되어 있기 때문에 @Valid, @Validated만 적용하면 된다.
모든 요청은 디스패처 서블릿을 통해 컨트롤러로 전달된다. 전달 과정에서는 컨트롤러 메소드의 객체를 만들어주는 ArgumentResolver가 동작하는데, @Valid는 이 ArgumentResolver에 의해 처리된다.
- @RequestBody는 json 형식의 요청을 객체로 변환해주는 작업이 ArgumentResolver 구현체인 RequestResponseBodyMethodProcessor가 처리하며, 이 내부에 @Valid로 시작하는 어노테이션이 있을 경우에 유효성을 검사한다.
- @ModelAttribute는 ModelAttributeMethodProcessor에 의해 @Valid가 처리된다.
검증에 오류가 있다면 MethodArgumentNotValidException 예외가 발생한다. 디스패처 서블릿에 기본으로 등록된 Exception Resolver인 DefaultHandlerExceptionResolver에 의해 400 BadRequest 에러가 발생한다.
@Valid는 컨트롤러 게층에서만 동작하 다른 계층에서는 검증되지 않는다고 한다.
다른 계층 (Service 등)에서도 파라미터를 검증하기 위해서는 @Validated와 결합이 되어야 한다고 한다.
@Validated 동작 원리
입력 파라미터의 유효성 검증은 컨트롤러에서 최대한 처리하고 넘겨주는 것이 좋다. 그러나 개발을 하다보면 팀 컨벤션에 따라서 다른 계층에서 파라미터를 검증해야 할 수도 있다. 예) Service단에 requestDto가 오는 경우 등
Spring에서는 AOP기반으로 메소드의 요청을 가로채서 유효성 검증을 진행해주는 @Validated를 제공하고 있다.
@Validated는 JSR 표준 기술이 아니며 Spring 프레임워크에서 제공하는 어노테이션 및 기능이다.
유효성 검증에 실패하면@Valid의 MethodArgumentNotValidException 예외가 아닌 ConstraintViolationException 예외가 발생한다. @Valid는 특정 ArgumentResolver에 의해 유효성 검사가 진행되었다. 그러나 @Validated는 AOP기반으로 메소드 요청을 인터셉터하여 처리된다. @Validated를 클래스 레벨에 선언하면 해당 클래스에 유효성 검증을 위한 인터셉터(MethodValidationInterceptor)가 등록된다.
그리고 해당 클래스의 메소드들이 호출될 때 AOP의 포인트 컷으로써 요청을 가로채서 유효성 검증을 진행한다.
이러한 이유로 @Validated를 사용하면 컨트롤러, 서비스, 레포지토리 등 계층에 무관하게 스프링 빈이라면 유효성 검증을 진행할 수 있다.
대신 클래스에는 유효성 검증 인터셉터를 등록하도록 @Validated를, 검증을 진행할 메소드에는 @Valid를 선언해주어야 한다.
이러한 이유로 @Valid에 의한 예외는 MethodArgumentNotValidException이며, @Validated에 의한 예외는 ConstraintViolationException이다. 이를 알고 있으면 나중에 예외 처리를 할 때 도움이 된다.
또한 @Validated는 groups라는 기능도 지원한다.
groups 기능은 동일한 객체에 대해 제약조건이 요청에 따라 달라질 수 있을 때 사용하면 좋다.
다음과 같은 필드를 가지는 객체가 있을 때
public class Item {
private final Stirng itemName;
private final int price;
}
예를 들어 생성하는 요청은 price가 5000이상이어야하지만
수정하는 요청은 price가 10000이상이어야한다.
이럴 때는 제약 조건이 적용될 검증 그룹을 지정할 수 있는 기능을 @Validated를 통해 제공하고 있다.
public interface SaveCheck {}
public interface UpdateCheck {}
public class Item {
private final Stirng itemName;
@Min(value = 5000, groups = SaveCheck.class)
@Min(value = 10000, groups = UpdateCheck.class)
private final int price;
}
@PostMapping
public String editItem(@Validated(UpdateCheck.class)
@ModelAttribute Item item) {
...
}
다음과 같이 파라미터로 UpdateCheck를 넣어주었다면 UpdateCheck에 해당하는 제약 조건만 검증된다. 만일 @Validated에 특정 마커를 지정해주지 않았거나, groups가 지정되어 있는데 @Valid를 이용한다면 다음과 같이 처리된다.
- @Validated에 특정 클래스를 지정하지 않는 경우: groups가 없는 속성들만 처리
- @Valid or @Validated에 특정 클래스를 지정한 경우: 지정된 클래스를 groups로 가진 제약사항만 처리
그러나 groups 기능을 복잡해서 실제 잘 사용되지 않는다고 한다. 다음과 같은 경우는 그냥 ItemSaveRequest, ItemUpdateRequest처럼 객체를 분리해 사용한다.
제약 조건 어노테이션 예
- @NotNull: 해당 값이 null이 아닌지 검증함
- @NotEmpty: 해당 값이 null이 아니고, 빈 스트링("") 아닌지 검증함(" "은 허용됨)
- @NotBlank: 해당 값이 null이 아니고, 공백(""과 " " 모두 포함)이 아닌지 검증함
- @AssertTrue: 해당 값이 true인지 검증함
- @Size: 해당 값이 주어진 값 사이에 해당하는지 검증함(String, Collection, Map, Array에도 적용 가능)
- @Min: 해당 값이 주어진 값보다 작지 않은지 검증함
- @Max: 해당 값이 주어진 값보다 크지 않은지 검증함
- @Pattern: 해당 값이 주어진 패턴과 일치하는지 검증함
hibernate validator 공식문서에 더 있다.
검증 순서
- @ModelAttribute 각각의 필드에 타입 변환 시도
- 성공하면 다음으로
- 실패하면 typeMismatch로 FieldError추가
- Validator 적용
바인딩에 성공한 필드만 Bean Validation을 적용한다.
BeanValidator는 바인딩에 실패한 필드는 BeanValidation을 적용하지 않는다.
@ModelAttribute → 각각의 필드 타입 변환시도 → 변환에 성공한 필드만 BeanValidation 적용
private final Stirng itemName;
private final int price;
위와 같은 필드가 있다고 가정할 때
예)
- itemName에 문자 “A” 입력 → 타입 변환 성공 → itemName 필드에 BeanValidation 적용
- price에 문자 “A”입력 → “A”를 숫자 타입 변환 시도 실패 → typeMismatch FieldError 추가 → price 필드는 BeanValidation 적용 X
@ModelAttribute vs @RequestBody
- HTTP 요청 파라미터를 처리하는 @ModelAttribute는 각각의 필드 단위로 세밀하게 적용된다. 그래서 특정 필드에 타입이 맞지 않는 오류가 발생해도 나머지 필드는 정상 처리할 수 있다.
- HttpMessageConverter를 사용하는 @RequestBody는 @ModelAttribute와 다르게 각각의 필드 안뒤로 적용되는 것이 아니라, 전체 객체 단위로 적용된다. 즉 JSON객체를 데이터 객체로 변환하지 못하면 이후 단계 자체가 진행되지 않고 예외가 발생한다. 컨트롤러도 호출되지 않고, Validator도 적용할 수 없다.
reference
https://mangkyu.tistory.com/174
인프런 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술
'Spring' 카테고리의 다른 글
[Spring] DI 의존성 주입 방식과 생성자 주입을 사용해야 하는 이유 (0) | 2022.11.22 |
---|---|
[Spring] ArgumentResolver란? (0) | 2022.11.18 |
[Spring] @Transactional의 트랜잭션 전파레벨 (0) | 2022.11.17 |
[Spring] 필터와 인터셉터 (Filter vs Interceptor) (0) | 2022.11.16 |
[Spring] SpringBootTest 테스트 격리 방법 (0) | 2022.11.15 |