Spring / / 2024. 7. 21. 23:41

@Valid 검증 어노테이션

‘@Valid’는 Bean Validation(빈 검증기)를 이용하여 객체에 대한 조건을 확신하는 어노테이션 입니다.

Spring은 LocalValidatorFactoryBean 어댑터를 활용하여 객체 조건을 확인하게 됩니다.

LocalValidatorFactoryBean 어댑터를 사용하기 위해서는 spring-boot-starter-validation 을 추가해 주면 됩니다.

주요 제약 어노테이션은 다음과 같습니다

  • ‘@NotNull’
    • null이 아니어야 함
  • ‘@NonEmpty’
    • 빈 값이 아니어야 함(문자열, 컬렉션 등)
  • ‘@Min’ , ‘@Max’
    • 숫자의 최소/최대값
  • ‘@Email’
    • 이메일 형식
  • ‘@Pattern’
    • 정규 표현식 패턴
public class User {
    @NotNull(message = "이름은 빈 값이 아니어야 합니다.")
    @Size(min = 2, max = 30, message = "2자에서 30자 사이의 이름을 입력해주세요")
    private String name;

    @Email(message = "이메일 형식으로 지정해야 합니다.")
    private String email;

    @Min(value = 18, message = "나이는 18세보다 많아야 합ㅎ니다.")
    private int age;
}

@RestController
public class UserController {
    @PostMapping("/users")
    public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
        // 유효성 검사를 통과한 경우에만 실행
        return ResponseEntity.ok(userService.createUser(user));
    }
}

유효성 검증에서 실패 시, MethodArgumentNotValidException이 발생합니다.

이 예외는 ‘@ControllerAdvice’를 활용하여 전역적으로 처리할 수 있습니다.

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Object> 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 ResponseEntity.badRequest().body(errors);
    }
}

또한 ‘@Valid’는 중첩된 객체에서도 적용할 수 있습니다.

public class Order {
    @Valid
    private User user;
}

여기서 말하는 중첩된 객체란, 객체 내에 원시값이 아닌 래퍼 클래스를 의미합니다.

다음 코드로 예시를 보여드리겠습니다.

public class Address {
    @NotBlank(message = "Street is required")
    private String street;

    @NotBlank(message = "City is required")
    private String city;

    @NotBlank(message = "Zip code is required")
    @Pattern(regexp = "\\\\d{5}", message = "Zip code must be 5 digits")
    private String zipCode;
}

public class User {
    @NotBlank(message = "Name is required")
    private String name;

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

    @Valid  // 중요: 중첩된 객체에 @Valid 적용
    private Address address;
}

@RestController
public class UserController {
    @PostMapping("/users")
    public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
        // user와 user.address 모두 유효성 검사 수행
        return ResponseEntity.ok(userService.createUser(user));
    }
}

이는 User 객체 내에 Address 객체가 멤버변수로 있는 것을 확인할 수 있습니다.

이때, Address 안에 객체 검증을 위해 어노테이션(NotBlank, Pattern)등이 존재하는데, User 내에서 Address에 ‘@Valid’ 어노테이션을 사용함으로써 해당 객체에 대한 검증을 진행할 수 있습니다.

또한, 정해진 검증 어노테이션 뿐만 아니라 커스텀 검증 어노테이션을 만들 수 있습니다.

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = AdultValidator.class)
public @interface Adult {
    String message() default "성인이어야 합니다.";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

이렇게 Adult 라는 어노테이션을 정의해주고 Validator 를 구현합니다.

public class AdultValidator implements ConstraintValidator<Adult, LocalDate> {
    @Override
    public void initialize(Adult constraintAnnotation) {
    }

    @Override
    public boolean isValid(LocalDate value, ConstraintValidatorContext context) {
        if (value == null) {
            return true;  // null 값은 @NotNull로 처리
        }
        return ChronoUnit.YEARS.between(value, LocalDate.now()) >= 18;
    }
}

해당 Validator를 구현함으로써, Adult 어노테이션을 통해 검증을 할 수 있습니다.

public class User {
    @NotBlank(message = "Name is required")
    private String name;

    @Adult(message = "18세 이상이어야 합니다.")
    private LocalDate birthDate;
}

이렇듯 Valid를 통해 객체 검증에 대해 조금 더 간단하고 전역적인 처리가 가능합니다.


Reference

https://mangkyu.tistory.com/174

'Spring' 카테고리의 다른 글

Pagination  (0) 2024.07.14
Bean Lifecycle  (0) 2024.07.07
Transaction Propagation  (2) 2024.06.30
ThreadLocal  (2) 2024.06.09
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유