빈 검증(Bean Validation)

Lilac-_-P·2023년 4월 17일
0

스프링 MVC

목록 보기
9/15

이전 글에서는 스프링이 HTTP 요청을 통해 서버로 들어오는 데이터를 검증하는 방법에 대해 알아보았다.

이런 검증 기능들을 매번 코드로 작성하는 것은 상당히 번거롭다. 특히 특정 필드에 대한 검증 로직은 대부분 빈 값인지 아닌지, 특정 크기를 넘는지 아닌지와 같이 매우 일반적인 로직이다. 또, 일반적인 로직의 검증은 필요한 곳 어디에나 적용할 수 있기 때문에, 공통화 혹은 표준화를 할 수 있다.

이런 검증 로직을 모든 프로젝트에 적용할 수 있게 공통화하고, 표준화한 것이 바로 Bean Validation이다.
Bean Validation은 특청한 구현체가 아니라 Bean Validation 2.0이라는 "기술 표준" 이다.
쉽게 이야기하면 검증 애너테이션과 여러 인터페이스의 모음이라고 할 수 있다. 우리는 이 기술 표준을 구현한 구현체를 스프링에서 사용하는 것이다.

참고.
Bean Validation을 구현한 기술중에 일반적으로 사용하는 구현체는 하이버네이트 Validator이다.
JPA의 구현체인 하이버네이트와 이름이 똑같지만 관련은 없다.

Bean Validation annotation

Bean Validation이 제공하는 가장 중요한 기능은 annotation 하나로 검증 로직을 매우 편리하게 적용할 수 있다는 것이다.

아래와 코드를 보자. 클래스의 필드에 붙은 annotation을 보면 어떤 요소를 검증하는 것인지 직관적으로 알 수 있다.

public class ExClass {

	@NotBlank
	private String ex1;

	@NotNull
    @Range(min = 1000, max = 100000)
	private Integer ex2;
    
    @Max(9999)
    private Integer ex3;

}

@NotBlank나 @NotNull은 해당 필드에 빈값(공백)이나 null 값이 들어가지 못하게 검증할 것이고,
@Range는 해당 필드에 정해진 범위의 값만 들어갈 수 있도록 검증할 것이고,
@Max는 해당 필드의 값이 정해진 최대값을 넘지 않도록 검증할 것이다.

이외에도 java의 Bean Validation 표준에서 제공하는 annotation은 종류가 훨씬 다양하다. 모든 annotation을 외우고 있을 필요는 없으니 필요할때만 찾아서 사용하면 된다.

참고.
java의 Bean Validation 표준에서 제공하는 annotation 말고도 실제 하이버네이트 Validator 구현체에서는 더 많은 검증용 annotation과 추가 기능을 제공한다.

Bean Validation 자체는 스프링에서 제공하는 기능은 아니다.

따라서 Bean Validation을 직접 사용하려면, ValidatorFactory를 통해 Validator를 얻고, 이 Validator의 validate() 함수를 이용하여 검증을 수행하면된다. 여기서 제공되는 Validator 인터페이스는 스프링이 제공하는 인터페이스가 아닌, java의 표준으로 제공되는 Validator 인터페이스므로, 헷갈리지 말자.

그럼 스프링 MVC의 검증과정에 이 Bean Validation을 어떻게 적용하면 좋을까?

스프링은 이미 개발자를 위해 Bean Validation을 스프링에 완전히 통합해두었다.
개발자는 스프링이 통합해놓은 Bean Validation을 잘 사용하기만 하면 된다.

Spring MVC와 Bean Validation

스프링 부트는 spring-boot-starter-validation 디펜던시를 추가하면, 자동으로 Bean Validator를 인지하고 스프링에 통합한다. 좀 더 구체적으로 말하자면, LocalValidatorFactoryBean을 글로벌 Validatot로 등록하는데, 이 Validator는 Bean Validation용 annotation을 보고 검증을 수행한다. 개발자는 @NotNull과 같은 어떤 검증할지 명시하는 annotation과 @Validated와 @Valid 같은 어떤 대상을 검증할지 지정하는 annotation만 코드에 적어주면 된다.

Bean Validation이 통합된 Spring MVC의 검증은 다음과 같은 순서를 가진다.

  1. @ModelAttribute로 지정된 파라미터에 타입 변환을 통해 바인딩 시도(각각의 필드 단위로 검증을 적용함)
    • 성공하면 다음으로
    • 실패하면 typeMismatch로 FieldError 추가
  2. Validator 적용(Bean Validator)

순서에서도 볼 수 있듯이, Bean Validator는 바인딩에 실패한 필드는 Bean Validation을 적용하지 않는다.
생각해보면 타입 변환에 성공해서 바인딩이 성공한 필드여야 Bean Validation 적용이 의미가 있다. 값이 들어오지도 않았는데 검증을 할수는 없으니 말이다.

이 Bean Validation을 통해 오류가 발생하면, 동일하게 BindingResult에 발생한 오류를 넣어준다.

Bean Validation와 Message

BindingResult에 들어가는 FieldError 나 ObjectError에서는 스프링이 제공하는 메시지 기능을 사용할 수 있었다. 이때 MessageCodeResolver가 code 생성하는 규칙이 있었는데, 아주 편리하게도 Bean Validation을 이용하면 annotation의 이름으로 code가 생성된다.

즉, 아래와 같은 규칙으로 생성된다.

  1. annotation이름.클래스 타입.필드 이름
  2. annotation이름.필드 이름
  3. annotation이름.필드 이름의 클래스 타입
  4. annotation이름

Bean Validation에서는 생성된 메시지 코드를 사용하여 순서대로 메시지 소스에서 메시지를 찾는다. 여기서 찾는 메시지는 properties 또는 yml 파일에 정의되어있는 메시지를 말한다.

만약 원하는 메시지를 찾을 수 없다면, 그 다음은 annotation 내부의 message 속성을 사용한다. 따라서 annotation에 직접 에러 메시지를 적어줄 수 있다.

마지막으로, 그래도 메시지를 찾을 수가 없다면, 라이브러리가 제공하는 기본 값을 사용한다. 이는 라이브러리의 영역이기때문에 개발자가 건드릴수 없다.

주의.
특정 필드와 관련된 오류가 아닌 글로벌 오류, 즉 ObjectError는 annotation을 기반으로하는 Bean Validation으로 검증처리하는게 어렵다. ObjectError 같은 경우는 직접 자바코드로 작성해서 검증 처리를 해주는 것이 좋다.

Bean Validation과 DTO

보통 웹 애플리케이션에서 HTTP 요청으로 전달된 데이터는 데이터 그대로 프로그램 내에서 객체로 사용하지 않는다.
쉽게 말하면, HTTP 요청으로 전달되는 데이터를 엔티티에 해당하는 클래스로 직접받을 일이 없다.
만약 HTTP 요청으로 전달되는 데이터를 엔티티에 해당하는 클래스로 직접받는다면, 엔티티 클래스의 필드에 검증을 위한 annotation을 추가해줘야한다. 또, 상황에 따라서 다른 검증 기준을 적용해야하는 경우가 생길 수 있다. 엔티티 클래스의 필드에 검증을 위한 annotation을 추가한다면, 상황에 따라서 다른 검증 기준을 적용하기가 어렵고, 설령 추가한다하더라도 코드가 매우 지저분해진다. 엔티티 클래스는 1개밖에 없기 때문이다.

따라서, 엔티티는 최대한 순수하게 유지하고, 대신 HTTP 요청으로 전달될 데이터를 받을 수 있는 DTO 클래스를 따로 만들어서 이 클래스에 Bean Validation을 위한 annotation을 추가해주는 것이 좋다.

DTO를 통해 HTTP 요청 데이터를 받으면서 검증을 수행하고, 검증이 수행된 DTO를 엔티티로 변환하는 과정을 추가하는 것이 좋은 접근 방법이다.

참고.
@Validated에서 제공하는 groups라는 속성을 이용하면, DTO 객체를 만들지 않더라도 유연하게 검증 기준을 적용할 수 있다. 하지만, @Valid에는 이 기능을 제공하지 않을 뿐더러, 근본적으로 엔티티에 해당하는 클래스의 코드가 지저분해지는 것을 해결할 수 없다. 따라서, 검증은 데이터를 전송하는 용도로 사용하는 객체인 DTO를 적극적으로 활용하는 것이 좋다.

Bean Validation과 HTTP 메시지 컨버터

우리는 HTTP 요청 데이터를 받을 때, 쿼리 파라미터 형식의 데이터뿐만 아니라 @RequestBody와 HttpEntity를 이용해서 메시지 바디에 있는 데이터도 받을 수가 있었다.

이 경우에도 당연히 Bean Validation을 적용할 수 있다. 그런데 주의해야할 점이 있다.
API 통신의 경우 3가지 경우를 나누어서 생각해야 한다.

  1. 성공 요청 : 성공
  2. 실패 요청 : JSON을 객체로 생성하는 것 자체가 실패(바인딩 자체를 실패)
  3. 검증 오류 요청 : JSON을 객체로 생성하는 것은 성공했고, 검증에서 실패 (바인딩 된 값에 오류가 존재)

@ModelAttribute로 지정된 파라미터의 경우, 타입 변환을 통해 바인딩 시도할 때 각각의 필드 단위로 검증을 적용한다.

그런데 @RequestBody로 지정된 파라미터의 경우, JSON 형태의 데이터를 객체로 변환해주는 HTTP 메시지 컨버터가 스프링이 제공하는 기능이 아닌 외부 라이브러리에 해당한다. 따라서 외부 라이브러리가 타입 변환을 통해 객체를 생성하는 시점에 오류가 발생할 경우, Controller로 전달될 파라미터 자체가 생성되지 않은 것이기 때문에, 컨트롤러 자체가 호출되지 않고 그 전에 예외가 발생한다.

즉, @ModelAttribute는 각각의 필드 단위로 세밀하게 적용되기 때문에 특정 필드에 타입이 맞지 않는 오류가 발생해도 나머지 필드는 정상처리 할 수 있었다. 하지만, HTTP 메시지 컨버터는 필드 단위로 적용되는 것이 아니라 전체 객체 단위로 적용된다. 일단 JSON 데이터가 변환되어 객체가 생성이 되어야 컨트롤러도 호출할 수 있고, 검증도 적용할 수 있는 것이다.

@ModelAttribute와 @RequestBody의 차이점을 명확하게 알고 Bean Validation을 적용해야한다.

HTTP 요청 메시지 바디 JSON 형태의 데이터를 실어서 통신한다면, 바인딩 실패시 잭슨 라이브러리에서 예외가 발생할 것이고, 이 예외는 외부 라이브러리에 의한 예외기 때문에 반드시 예외 처리 방법을 강구해야한다.(ExceptionHandler, @ControllerAdvice)

profile
열심히 하자

0개의 댓글