본문 바로가기
Spring-Java/Spring

@Valid, @Validated

by 현대타운301 2024. 4. 25.

 

세 줄 요약

- 도메인 지식은 도메인 클래스 내부에서 머물러야 한다.

- 컨트롤러에서 매개변수에 대한 유효성 검사는 @Valid를 통해 진행한다.

- 클래스에 @Validated를 선언하면 컨트롤러 외에도 모든 계층에서 유효성 검사를 진행할 수 있다.

 


 

Bean Validation

 

Validation(유효성 검사)

데이터가 스펙(요구사항)에 맞는지 확인하는 작업을 의미합니다.

 

 

Bean Validation

JSR(Java Specification Requests)-303(or 308) 이라는 스펙을 사용하여 유효성을 검사하는 행위를 의미합니다.

해당 스펙을 만족하는 대표적인 구현체로 'Hibernate Validator'가 있습니다.

스프링에선 'Spring Boot Starter Validation'을 제공하며, 이를 사용해 Hibernate Validator를 통한 Bean Validation을 진행합니다.

 

출처 : Maven Repository

 

 

의존성 추가

Maven (pom.xml)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

 

Gradle (build.gradle)

implementation 'org.springframework.boot:spring-boot-starter-validation'

 

의존성 추가 시 version을 따로 명시하지 않으면 자동으로 org.springframework.boot의 버전을 따라갑니다.

 


 

어디서 유효성 검사를 진행해야 할까?

 

상품 관리 어플리케이션에서의 유효성 검사

해당 어플리케이션에는 다음과 같은 요구 사항이 있습니다.

상품 번호는 1부터 시작하여 상품이 추가될 때마다 1씩 증가한다. 동일한 번호를 가지는 상품은 존재할 수 없다.
상품 이름은 1글자 이상 100글자 이하의 문자열로, 동일한 이름을 가지는 상품이 존재할 수 있다.
가격은 0원 이상 1,000,000원 이하의 값을 가질 수 있다.
재고 수량은 0개 이상 9,999개 이하의 값을 가질 수 있다.

 

프로젝트 내부에는 도메인 Entity인 'Product' 클래스 와 그 DTO인 'ProductDTO' 클래스가 있습니다.

둘 중 어느 곳에서 유효성 검사를 진행해야 할까요?

 

정답은 두 군데 모두입니다.

 

'상품 이름이 1글자 이상 100글자 이하'

'가격은 0원 이상 1,000,000원 이하'

'재고 수량은 0개 이상 9,999개 이하'

 

위 요구사항들과 같이 도메인 객체의 필드와 관련된 부분도메인 지식이라 하며, 이는 해당 클래스의 외부로 노출시키지 않는 것이 좋습니다. 따라서 Product 클래스에서는 도메인 지식과 관련된 유효성을 검사합니다.

public class Product {
    private Long id;

    @Size(min = 1, max = 100)
    private String name;

    @Max(1_000_000)
    @Min(0)
    private Integer price;

    @Max(9_999)
    @Min(0)
    private Integer amount;
}

 

ProductDTO 클래스에서는 클라이언트의 요청(Request)에 대한 유효성을 검사합니다.

  → 특정 필드를 JSON에 포함시키지 않았거나 null 값을 보내는 경우

public class ProductDTO {
    private Long id;

    @NotNull
    private String name;

    @NotNull
    private Integer price;

    @NotNull
    private Integer amount;
}

 


 

컨트롤러에서의 유효성 검사

 

@Valid

컨트롤러로 들어온 요청을 처리하는 과정에서 메소드의 객체가 생성되는데, 이 때 @Valid 어노테이션 또한 처리됩니다.

이러한 이유로 @Valid는 기본적으로 컨트롤러에서만 동작합니다.

 

@Valid를 사용할 수 있는 Target에는 1) 메소드, 2) 필드, 3) 생성자, 4) 매개변수, 5) 모든 타입 선언부 등이 있습니다.

 

아래와 같이 컨트롤러를 구성하고 ProductDTO 클래스의 제약조건에 위배되는 데이터를 보내봅니다.

 

ProductController

@RestController
public class ProductController {
	
    @PostMapping("/products")
    // 매개변수인 DTO에 대해 유효성 검사 실시
    public ProductDTO createProduct(@Valid @RequestBody ProductDTO productDTO) {
        return productService.add(productDTO);
    }
}

 

Postman 전송 결과

amount 필드를 보내지 않아 @NotNull 유효성 검사에 실패한 모습

 

HTTP 응답과 발생한 예외

400 Bad Request
org.springframework.web.bind.MethodArgumentNotValidException

 


 

다른 계층에서의 유효성 검사

 

@Validated

스프링 프레임워크에서 제공하는 유효성 검사를 위한 어노테이션 입니다.

클래스 레벨에 @Validated를 선언하면 @Valid가 붙은 매개변수에 대해 유효성 검사를 실시합니다.

이를 통해 다른 계층에서도 유효성 검사를 실시할 수 있습니다.

 

아래와 같이 ValidationService 클래스를 추가로 구성하고 Product 클래스의 제약조건에 위배되는 요청을 보내봅니다.

 

ValidationService

@Service
@Validated
public class ValidationService {
    public <T> void checkValid(@Valid T validationTarger) {
        // 매개변수에 선언된 @Valid에 의해 인자로 값을 넣고 호출하는 행위만으로 유효성 검사가 진행된다.
    }
}

 

ProductService

@Service
public class ProductService {

    private ProductRepository productRepository;
    private ValidationService validationService;

    @Autowired
    public ProductService(ProductRepository productRepository, ValidationService validationService) {
        this.productRepository = productRepository;
        this.validationService = validationService;
    }

    public ProductDTO add(ProductDTO productDTO) {
        Product product = ProductDTO.toEntity(productDTO);
        validationService.checkValid(product);	// 인자로 넣어준 객체에 대해 유효성 검사 실시
        Product savedProduct = productRepository.add(product);
        ProductDTO savedProductDTO = ProductDTO.toDTO(savedProduct);
        return savedProductDTO;
    }
}

 

Postman 전송 결과

price, amount 각각 범위를 벗어난 값으로 인해 유효성 검사에 실패한 모습

 

HTTP 응답과 발생한 예외

500 Internal Server Error
jakarta.validation.ConstraintViolationException

 


 

500 Internal Server Error?

 

새로운 상품을 추가할 때 들어오는 데이터는 클라이언트의 요청 본문의 데이터입니다.

이 때 유효성 검사에 실패하면 400 Bad Request 가 적절한 응답으로 보입니다.

하지만 어플리케이션 계층인 서비스 클래스에서는 500 Internal Server Error가 발생했습니다.

 

유효성 검사에 실패한 경우, 컨트롤러와 서비스가 각각 다른 응답을 보인 이유는 발생한 예외가 다르기 때문입니다.

이를 적절히 처리하기 위해선 전역 예외 핸들러를 구성해야 합니다.

 

 

전역 예외 핸들러 관련 글 보러가기

 

 

 

* refs

https://mangkyu.tistory.com/174