유효성 검사(validation)
- 애플리케이션의 비즈니스 로직이 올바르게 동작하는지 검증하는 작업을 유효성 검사라고 한다.
- 유효성 검사의 예로는 여러 계층에서 들어오는 데이터에 대해 의도한 형식대로 값이 들어오는지 체크하는 과정이 있다.
- 특히 자바에서 가장 신경 써야 하는 것 중 하나로 NullPointException이 있다.
Bean Validation
- 계층별로 진행하는 유효성 검사는 검증 로직이 각 클래스별로 분산돼 있어 관리하기가 어렵다.
- 그리고 검증 로직에 의외로 중복이 많아 여러 곳에 유사한 기능의 코드가 존재할 수 있다.
- 검증해야 할 값이 많다면 검증하는 코드가 길어진다.
- 이러한 문제로 코드가 복잡해지고 가독성이 떨어지는 문제가 있다.
- 이 같은 문제를 해결하기 위해 자바 진영에서는 2009년부터 Bean Validation 이라는 데이터 유효성 검사 프레임워크를 제공한다.
- Bean Validation은 어노테이션을 통해 다양한 데이터를 검증하는 기능을 제공한다.
- Bean Validation을 사용한다는 것은 유효성 검사를 위한 로직을 DTO같은 도메인 모델과 묶어서 각 계층에서 사용하면서 검증 자체를 도메인 모델에 얹는 방식으로 수행한다는 의미이다.
- 또한 Bean Validation은 어노테이션을 사용한 검증 방식이기 때문에 코드의 간결함도 유지할 수 있다.
Hibernate Validator
- Hibernate Validator는 Bean Validation 명세의 구현체이다.
- 스프링 부트에서는 Hibernate Validator를 유효성 검사 표준으로 채택해서 사용하고 있다.
- Hibernate Validator는 JSR-303명세의 구현체로서 도메인 모델에서 어노테이션을 통한 필드값 검증을 가능하게 도와준다.
- JSR이란 Java Specification Request의 약자로, Java 애플리케이션에서 데이터를 검증하는 기준을 명시하고 있다.
스프링 부트의 유효성 검사
- 유효성 검사는 각 계층으로 데이터가 넘어오는 시점에 해당 데이터에 대한 검사를 실시한다.
- 보통 스프링 부트 프로젝트에서는 계층 간 데이터 전송에 DTO(Data Transfer Object)를 활용하고 있기 때문에 다음과 같은 구조로 유효성 검사를 수행하는 것이 일반적이다.
스프링 부트에서 유효성 검사 기능 사용하기
- 원래 스프링 부트의 유효성 검사 기능은 spring-boot-starter-web에 포함돼 있었으나, 스프링 부트 2.3 버전 이후로 별도의 라이브러리로 제공하고 있다.
- 따라서 다음과 같은 의존성을 추가해야 한다.
Maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Gradle
dependencies {
implementation 'org.hibernate.validator:hibernate-validator:'
}
유효성 검사를 위한 대표적인 어노테이션은 다음과 같다.
문자열 검증
@Null
: null 값만 허용합니다.@NotNull
: null을 허용하지 않습니다. "", " "는 허용합니다.@NotEmpty
: null, ""을 허용하지 않습니다. " "는 허용합니다.@NotBlank
: null, "", " "을 허용하지 않습니다.
최댓값/최솟값 검증
BigDecimal
,BigInteger
,int
,long
등 타입을 지원합니다.@DecimalMax(value = "$numberString")
:$numberString
보다 작은 값을 허용합니다.@DecimalMin(value = "$numberString")
:$numberString
보다 큰 값을 허용합니다.@Min(value = $number)
:$number
이상의 값을 허용합니다.@Max(value = $number)
:$number
이하의 값을 허용합니다.
값의 범위 검증
BigDecimal
,BigInteger
,int
,long
등 타입을 지원합니다.@Positive
: 양수를 허용합니다.@PositiveOrZero
: 0 포함 양수를 허용합니다.@Negative
: 음수를 허용합니다.@NegativeOrZero
: 0 포함 음수를 허용합니다.
시간에 대한 검증
- Date, LocalDate, LocalDateTime 등의 타입을 지원합니다.
@Future
: 현재보다 미래 날짜를 허용합니다.@FutureOrPresent
: 현재 포함 미래 날짜를 허용합니다.@Past
: 현재보다 과거 날짜를 허용합니다.@PastOrPresent
: 현재 포함 과거 날짜를 허용합니다.
이메일 검증
@Email
: 이메일 형식을 검사합니다. ""을 허용합니다.
자릿수 범위 검증
BigDecimal
,BigInteger
,int
,long
등 타입을 지원합니다.@Digits(integer = $number1, fraction = $number2)
: $number1의 정수 자릿수와 $number2의 소수 자릿수를 허용합니다.
Boolean 검증
@AssertTrue
: true인지 체크합니다. null 값은 체크하지 않습니다.@AssertFalse
: false인지 체크합니다. null 값은 체크하지 않습니다.
문자열 길이 검증
@Size(min = $number1, max = $number2)
: $number1 이상 $number2 이하 범위 허용
정규식 검증
- @Pattern(regexp = "$expression") : 정규식을 검사합니다. 정규식은 자바의 java.util.regex.Pattern 패키지 컨벤션을 따릅니다.
사용 예시
@RestController
@RequestMapping("/validation")
public class ValidationController {
private final Logger LOGGER = LoggerFactory.getLogger(ValidationController.class);
@PostMapping("/valid")
public ResponseEntity<String> checkValidationByValid(
@Valid @RequestBody ValidRequestDto validRequestDto) {
LOGGER.info(validRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK).body(validRequestDto.toString());
}
위 코드에서 checkValidationByValid
메서드는 인자로 ValidRequestDto
를 받고 RequestBody로 받고 있다.
앞에 @Valid
어노테이션을 지정해야 해당 클래스에 대해 유효성 검사를 수행한다.
아래는 ValidRequestDto
클래스이다.
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class ValidRequestDto {
@NotBlank
private String name;
@Email
private String email;
@Pattern(regexp = "01(?:0|1|[6-9])[.-]?(\\d{3}|\\d{4})[.-]?(\\d{4})$")
private String phoneNumber;
@Min(value = 20)
@Max(value = 40)
private int age;
@Size(min = 0, max = 40)
private String description;
@Positive
private int count;
@AssertTrue
private boolean booleanCheck;
}
위 DTO에서 만약 Validation이 어긋나는 필드가 포함된 요청이 들어온다면 400 Bad Request
를 클라이언트에게 응답한다.
@Validated 활용하기
- 컨트롤러 클래스에서 메서드에 인자 앞에 명시한 @Valid 어노테이션은 자바에서 지원하는 어노테이션이며, 스프링에서 지원하는 어노테이션은 @Validated라는 어노테이션이다.
- 이 어노테이션은 @Valid의 기능을 포함하고 있기 때문에 @Validated로 변경할 수 있다.
- @Valid와 다른 점은 @Validated는 유효성 검사를 그룹으로 묶어 대상을 특정할 수 있는 기능이 있다.
예를 들어, 두 인터페이스를 생성한다.
ValidationGroup1
public interface ValidationGroup1 {
}
ValidationGroup2
public interface ValidationGroup2 {
}
그리고 위에서 사용한 Dto 객체에서 age
와 count
에 다음과 같이 수정한다.
@Min(value = 20, groups = ValidationGroup1.class)
@Max(value = 40, groups = ValidationGroup1.class)
private int age;
@Positive(groups = ValidationGroup2.class)
private int count;
각 어노테이션에 groups 속성을 사용하여 그룹을 설정한다.
실제로 어느 구룹에 유효성 검사를 실시할지 결정하는 것은 @Validated
어노테이션에서 한다.
@RestController
@RequestMapping("/validation")
public class ValidationController {
private final Logger LOGGER = LoggerFactory.getLogger(ValidationController.class);
@PostMapping("/valid1")
public ResponseEntity<String> checkValidation1(
@Validated(ValidationGroup1.class) @RequestBody ValidRequestDto validRequestDto) {
LOGGER.info(validRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK).body(validRequestDto.toString());
}
@PostMapping("/valid2")
public ResponseEntity<String> checkValidation2(
@Validated(ValidationGroup2.class) @RequestBody ValidRequestDto validRequestDto) {
LOGGER.info(validRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK).body(validRequestDto.toString());
}
위 컨트롤러 클래스의 checkValidation1
메서드는 ValidRequestDto에서 ValidationGroup1.class
의 그룹으로 설정한 필드에 대해서만 유효성 검사를 실시한다.
마찬가지로 checkValidation2
메서드는 ValidRequestDto에서 ValidationGroup2.class
의 그룹으로 설정한 필드에 대해서만 유효성 검사를 실시한다.
만약 그룹으로 지정하지 않고 @Validated
으로만 사용한다면 그룹으로 지정하지 않은 필드에 대해서만 유효성 검사를 실시한다.
Custom Validation
- 자바 또는 스프링의 유효성 검사 어노테이션에서 제공하지 않는 기능을 사용해야 할 경우, ConstrainValidator와 커스텀 어노테이션을 조합해서 별도의 유효성 검사 어노테이션을 생성할 수 있다.
전화번호 형식이 일치하는지 확인하는 간단한 유효성 검사 어노테이션을 생성
- 먼저 ConstraintValidator 인터페이스를 구현하는 클래스를 생성한다.
- 이 인터페이스는 inValid 메서드를 정의하고 있으며, 이 메서드를 구현해야한다.
이 메서드에서 false가 리턴되면 MethodArgumentNotValidException 예외가 발생한다.
public class TelephoneValidator implements ConstraintValidator<Telephone, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if(value==null){
return false;
}
return value.matches("01(?:0|1|[6-9])[.-]?(\\d{3}|\\d{4})[.-]?(\\d{4})$");
}
}
- 커스텀한 로직을 어노테이션으로 사용하기 위해 다음과 같이 어노테이션 인터페이스를 구현한다.
- 참고로 어노테이션 인터페이스는 클래스의 메타데이터를 제공하기 위해 사용된다. 이는 컴파일러나 런타임 환경에서 추가적인 정보를 제공할 수 있다.
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = TelephoneValidator.class)
public @interface Telephone {
String message() default "전화번호 형식이 일치하지 않습니다.";
Class[] groups() default {};
Class[] payload() default {};
}
@Target
어노테이션은 이 어노테이션이 어디서 선언할 수 있는지 정의하는 데 사용된다.
사용할 수 있는 ElementType
은 대표적으로 다음과 같다.
- ElementType.FIELD: 각 필드에 적용 가능합니다.
- ElementType.METHOD: 메서드에서 사용하는 모든 파라미터를 검사하기 위해 사용됩니다.
- ElementType.TYPE: 클래스 레벨에서 유효성을 검사하기 위해 사용되며, 같은 객체에서 다른 필드들의 관계를 확인하는 데 유용합니다.
- ElementType.PARAMETER: 메서드의 특정 파라미터를 검사하기 위해 사용됩니다.
@Retention
어노테이션은 이 어노테이션이 실제로 적용되고 유지되는 범위를 의미한다.@Retention
의 적용 범위는 RetentionPolicy
를 통해 지정하며, 지정 가능한 항목은 다음과 같다.
- RetentionPolicy.RUNTIME: 컴파일 이후에도 JVM에 의해 계속 참조합니다. 리플렉션이나 로깅에 많이 사용되는 정책입니다.
- RetentionPolicy.CLASS: 컴파일러가 클래스를 참조할 때까지 유지합니다.
- RetentionPolicy.SOURCE: 컴파일 전까지만 유지됩니다. 컴파일 이후에는 사라집니다.
@Constraint
어노테이션은 앞에서ConstrainValidator
를 구현한TelephoneValidator
를 매핑한다.
또한message()
메서드를 통해 유효성 검사를 위배했을 때 보여질 메세지를 구현한다.
groups()
는 유효성 검사를 사용하는 그룹으로 설정하며,payload()
는 사용자가 추가 정보를 위해 전달하는 값이다.
이와 관련된 내용은ConstraintHelper
를 참조
이제 커스텀한 @Telephone
어노테이션을 사용할 수 있다.
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class ValidRequestDto {
@NotBlank
private String name;
@Email
private String email;
@Telephone // custom validation annotation
private String phoneNumber;
@Min(value = 20)
@Max(value = 40)
private int age;
@Size(min = 0, max = 40)
private String description;
@Positive
private int count;
@AssertTrue
private boolean booleanCheck;
}
참고
- 스프링 부트 핵심 가이드 "스프링 부트를 활용한 애플리케이션 개발 실무" , 장정우, 2022
- https://github.com/wikibook/springboot/tree/main/chapter10_valid_exception/src/main/java/com/springboot/valid_exception/data/dto
'Development > Spring' 카테고리의 다른 글
[Spring] 스프링 액추에이터(Actuator) (0) | 2024.03.10 |
---|---|
[Spring] 스프링 부트의 예외 처리 방식 (0) | 2024.03.08 |
[Spring] 영속성 전이(Cascade)와 고아 객체(Orphan) (0) | 2024.03.05 |
[Spring] 연관 관계 매핑하기 (1) | 2024.03.05 |
[Spring] JPA Auditing, BaseEntity (0) | 2024.03.03 |