Spring and Spring Boot/Class, Annotation, Library

Annotation : 검증 관련 어노테이션 (@Valid, @Validated)

마이트너 2024. 8. 22. 19:59
CF. Validation과 Verification
: 두 용어 모두 확인 및 검증을 의미한다. 다만 그 검증에 있어 영역에 차이가 있는데, Verification은 개발자 관점에서의 시스템 검증을 의미한다. 개발하고 있는 시스템이 미리 정의한 사양(specification)에 부합하는지 검증하는 것이다. 아직 실제 시스템이 구현된 상태가 아닐 수 있기 때문에 시뮬레이션을 통해 검증이 이루어진다.

반면, Validation은 사용자 관점에서의 시스템 검증을 의미한다. 개발 완료된 시스템이 사용자의 요구사항을 충족하는지 확인하는 지 검증하는 것이다. 실제 시스템이 구현된 상태이기 때문에 직접 타깃 시스템에 검증을 하게 된다.

➡️ @Valid

 Validation을 목적으로 하는 어노테이션이다. Java의 Bean Validation API에서 제공하는 어노테이션으로, JSR-380 (Bean Validation 2.0) 규격을 기반으로 한다. 주로 컨트롤러 메소드와 서비스 메소드에서 사용되어 입력값의 유효성을 검증한다.

 

 

 

 @Valid의 용도와 사용방법

 : Spring에서는 일종의 어댑터인 LocalValidatorFactoryBean가 제약 조건 검증을 처리하는데, 이를 이용하려면 LocalValidatorFactoryBean을 빈으로 등록해야 한다. 반면, Spring Boot에서는 의존성만 추가하면 같은 기능을 사용할 수 있다. 또한, 엔티티 클래스 정의는 모든 @Valid를 이용한 유효성 검사에 필수적이므로 미리 짚고 지나가자. 예시를 보며 이해해보자. 아래와 같이 정의된 User 클래스는 @Validated 어노테이션이 적용된 컨트롤러나 서비스에서 검증된다. 검증 규칙은 javax.validation.constraints 패키지의 @NotNull, @Size 등의 어노테이션을 통해 정의할 수 있다.

 

  • @Valid 사용을 위한 build.gradle 의존성 추가
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-validation', version: '2.5.2'

 

  • 유효성 검증 대상이 되는 엔티티 클래스를 정의하는 방법 : 제약 조건 적용을 적용하는 방법에 대한 내용으로  엔티티 클래스의 필드에 @NotNull, @Size, @Email 등 다양한 제약 조건을 적용할 수 있다.
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

public class User {
    @NotBlank(message = "Username is mandatory")
    @Size(min = 2, max = 30, message = "Username must be between 2 and 30 characters")
    private String username;

    @NotBlank(message = "Password is mandatory")
    @Size(min = 6, message = "Password must be at least 6 characters")
    private String password;

    @Email(message = "Email should be valid")
    private String email;

    // Getters and setters
}

 

  • 객체 검증 : 객체의 필드 값이 검증 어노테이션에 정의된 규칙을 충족하는지 확인한다. 예시의 경우, @Valid 어노테이션은 User 객체의 필드 즉, 입력 파라미터가 User 클래스에 정의된 검증 규칙을 충족하는지 검사하고 있다. RestController를 통해 @RequestBody 객체를 사용자로부터 가져올 때, 들어오는 값을 검증하는 것이다. 즉, @Valid를 서비스 단까지 가지 않아도 Controller 단에서 좀 더 빨리 검증할 수 있는 수단으로 사용한 경우이다. 
// 컨트롤러 메소드에서의 사용 : HTTP 요청의 본문에 포함된 객체의 유효성을 검사

import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/users")
@Validated
public class UserController {

    @PostMapping("/register")
    public ResponseEntity<String> registerUser(@Valid @RequestBody User user) {
        // 유효성 검사가 통과한 후 비즈니스 로직 수행
        return ResponseEntity.ok("User registered successfully");
    }
}
//서비스 계층에서의 사용 : 비즈니스 로직을 수행하기 전에 객체의 유효성을 확인

import org.springframework.stereotype.Service;
import javax.validation.Valid;

@Service
public class UserService {

    public void registerUser(@Valid User user) {
        // 유효성 검사가 통과한 후 비즈니스 로직 수행
        System.out.println("User registered: " + user.getUsername());
    }
}

 

  • 검증 오류 처리 : 유효성 검사에 실패할 경우 MethodArgumentNotValidException 에러로 인해 BadRequest Response가 발생하기도 하므로, 예외 처리(Exception Handling)에 더욱 신경써야 한다. 대표적인 방법으로는 예외 처리를 정의하여 커스터마이즈하거나 에러가 발생하는 메서드 내에서 직접 처리하는 경우로 나눌 수 있다.
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;

import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
    }
}
@RestControllerAdvice
public class ApiControllerAdvice {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex){
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors()
                .forEach(c -> errors.put(((FieldError) c).getField(), c.getDefaultMessage()));
        return ResponseEntity.badRequest().body(errors);
    }
}

 

 

 

 

 @Valid의 동작원리

 : 클라이언트의 HTTP 요청은 Dispatcher Servlet을 통해 컨트롤러로 전달되는데, 전달 과정에서 컨트롤러 메소드의 객체를 만들어주는 ArgumentResolver가 동작한다. @RequestBody를 사용하는 경우 ArgumentResolver의 구현체인 RequestResponseBodyMethodProcessor가 @RequestBody의 JSON 메세지를 객체로 변환해주는데, 이때 @Valid로 시작하는 어노테이션이 있을 경우 유효성 검사를 진행하는 것이다. 참고로 @RequestBody가 아닌 @ModelAttribute를 사용 중인 경우에는 ModelAttributeMethodProcessor에 의해 @Valid가 처리된다.

 

검증 과정에서 오류가 발견되면 MethodArgumentNotValidException 예외가 발생한다. 그러면 Dispatcher Servlet에 기본으로 등록된 Exception Resolver인 DefaultHandlerExceptionResolver에 의해 400 BadRequest 에러를 내보낸다. 이렇듯 @Valid의 동작은 Dispatcher Servlet을 활용하여 Validation이 이루어지기 때문에 주로 Controller에서 사용한다.

 

 


➡️ @Validated

Java의 Spring Framework에서 사용되는 어노테이션으로, 주로 스프링의 데이터 유효성 검사를 지원하기 위해 사용된다. 이 어노테이션은 Bean Validation과 함께 작동하여 객체의 유효성 검사를 수행할 수 있게 해준다. 

 

 

✅ @Validated의 개념

 : 클라이언트에서 들어오는 input 즉, 입력 파라미터의 유효성 검증은 빠르면 빠를 수록 좋다. 때문에 일반적으로 컨트롤러에서 수행하는 경우가 많지만 불가피하게 다른 곳에서 검증해야하는 경우도 발생한다. 이를 위한 대책으로 Spring은 AOP 기반의 @Validated를 제공해준다. 이는 메소드의 요청을 가로채 유효성 검증을 해주는 어노테이션이다. 주로 아래의 예시와 같이 @Valid와 함께 사용된다. 

@Service
@Validated
public class UserService {

	public void addUser(@Valid AddUserRequest addUserRequest) {
		...
	}
}

 

 

 

✅ @Validated의 용도와 사용방법

 : 엔티티 클래스 정의는 @Valid와 마찬가지로 모든 @Validated를 이용한 유효성 검사에 필수적이다.

 

  • 유효성 검증 대상이 되는 엔티티 클래스를 정의하는 방법 : 이미 위에서 설명했으니 코드만 보고 지나가자.
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

public class User {
    @NotNull
    @Size(min = 2, max = 30)
    private String username;

    @NotNull
    @Size(min = 6)
    private String password;

    // getters and setters
}

 

 

  • 컨트롤러 메소드의 유효성 검사 : HTTP 요청을 처리하는 컨트롤러에서 입력값의 유효성을 검사할 때 사용한다. 주로 @Valid와 함께 사용한다.
@Validated
@Controller
public class Controller {

    @PostMapping("/submit")
    public ResponseEntity<String> submitForm(@Valid @RequestBody MyForm form) {
        // ...
    }
}

 

  • 서비스 계층의 검증 : 비즈니스 로직을 수행하기 전에 객체의 상태를 검증할 때 사용한다. 다음 예시는 서비스 클래스에서 @Validated를 사용하여 User 객체의 유효성을 검사하는 예시이다. 
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;

@Service
@Validated
public class UserService {

    // 비즈니스 로직 수행 전에 유효성 검사 수행
    public void registerUser(@Valid User user) {
        // User 객체의 유효성이 검사된 상태에서 비즈니스 로직 수행
        // 예를 들어, 사용자 정보를 데이터베이스에 저장
        System.out.println("User registered: " + user.getUsername());
    }
}

 

  • 검증 그룹 지정 : 유효성 검사 그룹을 사용하여 조건에 따라 특정 검증 그룹을 지정하여 검증 규칙을 세분화할 수 있다.
public interface BasicValidationGroup {}
public interface AdvancedValidationGroup {}

@Validated(BasicValidationGroup.class)
public class Service {
    // ...
}

 

 

  • 유효성 검사 작동 테스트
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.junit.jupiter.api.Test;

@SpringBootTest
public class UserServiceTest {

    @Autowired
    private UserService userService;

    @Test
    public void testRegisterUser_valid() {
        User user = new User();
        user.setUsername("john_doe");
        user.setPassword("password123");
        user.setEmail("john.doe@example.com");

        userService.registerUser(user);
    }

    @Test
    public void testRegisterUser_invalid() {
        User user = new User();
        user.setUsername("j");  // too short
        user.setPassword("pass");  // too short
        user.setEmail("invalid-email");

        // This should throw a validation exception
        try {
            userService.registerUser(user);
        } catch (Exception e) {
            System.out.println("Validation failed: " + e.getMessage());
        }
    }
}

 

 

 

 @Validated의 동작 원리

 : 유효성 검증에 실패가 하면 ConstraintViolationException 에러가 발생하게 된다. 얼핏 생각하면 앞서 설명한 @Valid처럼 MethodArgumentNotValidException 에러가 발생해야 할 것으로 보이는데, ConstraintViolationException가 발생하는 원인은 @Validated의 동작원리에 있다. @Validated를 클래스 레벨에 선언하면 클래스의 유효성 검증을 위해 AOP의 어드바이스 또는 MethodValidationInterceptor가 등록되고, 해당 클래스의 메소드들이 호출될 때 AOP의 포인트 컷으로써 요청을 가로채서 유효성 검증을 진행한다. 때문에 @Validated를 사용하면 컨트롤러, 서비스, 레포지토리 등 계층에 무관하게 스프링 빈이라면 유효성 검증을 진행할 수 있다. 

 

 

 @Valid와의 차이점

 : @Valid는 JSR-380(Bean Validation 2.0) 규격에 따라 객체의 유효성을 검증하지만, @Validated는 Spring의 유효성 검증을 지원하며 유효성 검증 그룹을 지정할 수 있는 추가적인 기능을 제공한다는 차이가 있다. 이러한 이유에서 @Valid와 달리 @Validated는 따로 의존성을 추가할 필요가 없다.

 


➡️ 참고자료

[블로그]

https://jyami.tistory.com/55

https://mangkyu.tistory.com/174

728x90