스프링의 유효성 검사는 주로 사용자가 입력한 데이터의 유효성을 검증하는 데 사용된다.
웹 애플리케이션에서 사용자로부터 받은 데이터가 정확하고 적절한 형식으로 제공되었는지 확인하기 위한 것이다.
예를 들어, 회원 가입 폼에서 사용자가 제공한 이메일 주소가 올바른 형식인지, 비밀번호가 일치하는지 등을 확인할 때 유효성 검사가 사용될 수 있다.
일반적인 애플리케이션 유효성 검사의 문제점
- 관리: 계층별로 진행하는 유효성 검사는 검증 로직이 각 클래스별로 분산돼 있어 관리가 어렵다.
- 중복: 검증 로직이 중복이 많아 여러 곳에 유사한 기능 코드가 존재할 수 있다.
- 코드 길어짐: 검증할 값이 많으면 검증 코드가 길어진다.
즉, 코드가 복잡해지고 가독성이 떨어진다.
이를 해결하기 위해 자바에서 데이터 유효성 검사 프레임워크를 제공한다.
@Bean Validation 어노테이션
해당 어노테이션은 자바에서 제공하는 표준화된 유효성 검사 기능이다.
다양한 데이터를 검증하는 기능을 제공한다.
유효성 검사에 사용하는 어노테이션은 인텔리제이의 BeanValidation 탭을 클릭하면 확인가능하다.
BeanValidation 위치: View → Tool Windows → Bean Validation
스프링 프레임워크와 함께 사용할 때, 스프링 MVC에서는 컨트롤러 메서드의 파라미터에 @Valid을 사용하여 객체를 자동으로 유효성 검사하는 대상으로 지정할 수 있다.
특징
- 해당 어노테이션을 이용하면 검증 자체를 도메인 모델에 얹는 방식으로 수행한다.
- 유효성 검사를 위한 로직과 묶인 도메인 모델을 각 계층에서 사용한다.
- 코드의 간결함: 어노테이션을 사용한 검증 방식으로 코드의 간결함을 유지할 수 있다.
Hibernate Validator
Bean Validation 명세의 구현체
- 스프링 부트에서 Hibernate Validator를 유효성 검사 표준으로 채택해서 사용
- JSR-303 명세의 구현체
- 도메인 모델에서 어노테이션을 통한 필드값 검증을 도와준다.
스프링 부트의 유효성 검사
유효성 검사는 각 계층으로 데이터가 넘어오는 시점에 해당 데이터에 대한 검사를 실시한다.
대표적인 BeanValidation 어노테이션
아래는 대표적인 유효성 검사 어노테이션이다.
- 문자열 검증
- @Null: null 값만 허용
- @NotNull: null 허용하지 않음
- @NotBlank: null, "", " " 허용하지 않음
- @NotEmpty: null,"" 허용하지 않음(" "는 허용)
- 최댓값/최솟값 검증
BigDecimal, BigInteger, int, long 등의 타입을 지원- @DemicalMax(value = "$numberString"): $numberString보다 작은 값 허용
- @DemicalMin(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: 이메일 형식 검사. ""허용
- 자릿수 범위 검증
BigDemical, 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 패키지 컨벤션 따름
- @Pattern(regexp = "^01(?:0|1|[6-9])[.-]?(\\d{3}|\\d{4}) [.-]? \\d{4}$"
, message = "올바른 휴대폰 번호 형식이 아닙니다.")
- @Pattern(regexp = " $expression"): 값이 지정된 정규식 패턴과 일치하는지 확인
참고: 기존에는 유효성 검사 기능이 spring-boot-starter-web에 포함되어 있었으나 스프링 부트 2.3버전 이후로 별도의 라이브러리로 제공하여 의존성(spring-boot-starter-validation)을 따로 추가해야 한다.
@Valid vs @Validated
import javax.validation.Valid;
import org.springframework.validation.annotation.Validated;
- @Valid: 자바에서 지원하는 유효성 검사를 수행하기 위한 어노테이션
- @Validated: 스프링 별도 유효성 검사 지원 어노테이션
- @Valid 어노테이션 기능 포함 -> @Validated 로 변경 가능하다.
@Validated
유효성 검사를 그룹으로 묶어 대상을 특정할 수 있는 기능이 있다.
결론부터 말하면 아래와 같이 설정할 수 있다.
- @Validated 어노테이션에 특정 그룹을 설정하지 않은 경우
: groups 가 설정되지 않은 필드에 대해 유효성 검사를 수행 - @Validated 어노테이션에 특정 그룹을 설정하는 경우
: 지정된 그룹으로 설정된 필드에 대해서만 유효성 검사를 수행
위의 특징으로 적절하게 설계해야 의도대로 유효성 검사를 실시 할 수 있다.
아래 예시를 함께 보자.
[예시]
그룹 생성
package com.springboot.valid_exception.data.group;
public interface ValidationGroup1 {
}
package com.springboot.valid_exception.data.group;
public interface ValidationGroup2 {
}
그룹 설정
package com.springboot.valid_exception.data.dto;
import com.springboot.valid_exception.config.annotation.Telephone;
import com.springboot.valid_exception.data.group.ValidationGroup1;
import com.springboot.valid_exception.data.group.ValidationGroup2;
import lombok.*;
import javax.validation.constraints.*;
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class ValidatedRequestDto {
@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, groups = ValidationGroup1.class)
@Max(value = 40, groups = ValidationGroup1.class)
private int age;
@Size(min = 0, max = 40)
private String description;
@Positive(groups = ValidationGroup2.class)
private int count;
@AssertTrue
private boolean booleanCheck;
}
groups 속성을 사용해 ValidationGroup1, ValidationGroup2를 설정할 수 있다.
controller
실제 그룹을 설정해서 유효성 검사를 실시할지 결정하는 것은 @Validated 어노테이션에서 한다.
@RestController
@RequestMapping("/validation")
public class ValidationController {
private final Logger LOGGER = LoggerFactory.getLogger(ValidationController.class);
@PostMapping("/validated")
public ResponseEntity<String> checkValidation(
@Validated @RequestBody ValidatedRequestDto validatedRequestDto) {
LOGGER.info(validatedRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
}
@PostMapping("/validated/group1")
public ResponseEntity<String> checkValidation1(
@Validated(ValidationGroup1.class) @RequestBody ValidatedRequestDto validatedRequestDto) {
LOGGER.info(validatedRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
}
@PostMapping("/validated/group2")
public ResponseEntity<String> checkValidation2(
@Validated(ValidationGroup2.class) @RequestBody ValidatedRequestDto validatedRequestDto) {
LOGGER.info(validatedRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
}
@PostMapping("/validated/all-group")
public ResponseEntity<String> checkValidation3(
@Validated({ValidationGroup1.class,
ValidationGroup2.class}) @RequestBody ValidatedRequestDto validatedRequestDto) {
LOGGER.info(validatedRequestDto.toString());
return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
}
}
request 데이터1
{
"age": -1, // group1
"booleanCheck”: true,
"count": -1, // group2
“description”: "Validation 실습 데이터입니다.",
email": “abc@wikibooks.co.kr",
“name”: "Flature”,
“phoneNumber”: "910-1234-5678"
}
age와 count 변수에 대한 유효성 검사를 통과하지 못하는 데이터로 각 메소드를 호출하면
- 메소드 결과
- checkValidation: 통과
- checkValidation2: age 에러
- checkValidation3: count 에러
- checkValidation4: age, count 에러
request 데이터2
{
"age": 30, // group1
"booleanCheck”: false,
"count": 30, // group2
“description”: "Validation 실습 데이터입니다.",
email": “abc@wikibooks.co.kr",
“name”: "Flature”,
“phoneNumber”: "910-1234-5678"
}
booleanCheck 유효성 검사를 통과하지 못하는 데이터로 각 메소드를 호출하면
- 메소드 결과
- checkValidation: booleanCheck에러
- checkValidation2: 통과
- checkValidation3: 통과
- checkValidation4: 통과
커스텀 Validation 추가
java나 spring이 제공하는 유효성 검사 어노테이션으로는 부족할 때 ConstrainValidator와 커스텀 어노테이션을 조합해 병도의 유효성 검사 어노테이션을 생성할 수 있다.
@Pattern어노테이션으로 동일한 정규식을 자주 쓰는 경우가 흔한 사례다.
아래 전화번호 형식 유효성 검사 어노테이션을 생성한 예시를 보자.
1. ConstraintValidator 인터페이스를 구현하는 클래스를 생성한다.
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class TelephoneValidator implements ConstraintValidator<...
}
2. 인터페이스 선언 시, 어떤 어노테이션 인터페이스인지 타입을 지정해야 한다.
- Telephone 어노테이션 인터페이스 생성
package com.springboot.valid_exception.config.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
// 예제 10.9
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = TelephoneValidator.class)
public @interface Telephone {
String message() default "전화번호 형식이 일치하지 않습니다.";
Class[] groups() default {};
Class[] payload() default {};
}
- 타입 지정
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
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})$");
}
}
설정하고 나면 인텔리제이 IDEA의 [Bean Validation] 탭에 @Telephone이 생긴다.
- 적용하기
@Telephone
private String phoneNumber;
부가 정보
@Target - 어노테이션을 어디서 선언할 수 있는지 정의
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
public @interface 어노테이션인터페이스명 {
...
}
@Target은 해당 어노테이션이 적용될 수 있는 대상을 지정하는 데 사용된다.
- 위치: 어노테이션 인터페이스 위
- 특정 유형의 요소에만 어노테이션을 적용할 수 있도록 제한할 수 있다.
- ElementType (@Target 어노테이션의 매개변수)
- 어노테이션을 적용할 수 있는 대상 유형을 정의
- PACKAGE: 패키지 레벨에서 어노테이션을 사용할 때 이 유형을 사용
- ex) 패키지에 대한 정보를 주석으로 추가하려는 경우
- TYPE: 클래스, 인터페이스, enum 등의 선언에 어노테이션을 적용
- CONSTRUCTOR: 클래스의 생성자에 어노테이션을 추가
- FIELD: 클래스 내부의 필드, 즉 변수에 어노테이션을 적용
- METHOD: 클래스 내부의 메소드에 어노테이션을 적용
- ANNOTATION_TYPE: 다른 어노테이션을 정의할 때 어노테이션을 적용해서 제한
- LOCAL_VARIABLE: 메소드 내부에 선언된 로컬 변수에 어노테이션을 적용
- PARAMETER: 메소드나 생성자의 매개변수에 어노테이션을 적용
- TYPE_PARAMETER: 제네릭 클래스나 메소드의 제네릭 타입의 매개변수에 어노테이션을 적용
- TYPE_USE: 타입 사용 위치에 어노테이션을 적용
- ex) 변수의 선언, 형변환, instanceof 연산자 등에서 타입 정보를 사용하는 위치에 어노테이션을 추가할 수 있다.
- Java 8 이상에서 도입
- PACKAGE: 패키지 레벨에서 어노테이션을 사용할 때 이 유형을 사용
- 어노테이션을 적용할 수 있는 대상 유형을 정의
이러한 ElementType은 어노테이션을 어디에 적용할 수 있는지를 명시하여 코드의 가독성과 유지보수성을 향상시킨다.
예를 들어, 만약 어노테이션이 메소드에만 적용되어야 한다면
- @Target(ElementType.METHOD)와 같이 선언
- 이렇게 하면 해당 어노테이션이 메소드에만 적용될 수 있다.
@Retention - 어노테이션이 실제로 적용되고 유지되는 범위
...
@Retention(RetentionPolicy.RUNTIME)
public @interface Telephone {
...
}
@Retention 어노테이션은 어노테이션이 소스 코드, 클래스 파일, 런타임 중 어느 시점까지 유지되어야 하는지를 지정하는 데 사용된다.
- RetentionPolicy (@ Retention 어노테이션의 매개변수)
- RetentionPolicy는 어노테이션이 유지되는 시점을 결정
- SOURCE: 소스 코드까지만 어노테이션 정보가 유지(컴파일 전까지만 유지)
- 즉, 컴파일된 클래스 파일에는 어노테이션 정보가 포함되지 않는다.
- 컴파일러에게만 어노테이션 정보를 제공하는 데 사용됩니다.
- CLASS: 컴파일러가 클래스를 참조할 때까지 유지(클래스 파일까지 어노테이션 정보가 유지)
- 하지만 런타임에는 어노테이션 정보가 사용되지 않는다.
- 리플렉션을 통해 클래스 파일에서 어노테이션 정보를 읽을 수 있게 한다.
- RUNTIME: 컴파일 이후에도 JVM에 의해 계속 런타임까지 어노테이션 정보 참조
- 즉, 런타임에 리플렉션을 사용하여 어노테이션 정보를 읽을 수 있다.
- 리플렉션이나 로깅에 많이 사용되는 정책
- 런타임 환경에서 동적으로 어노테이션 정보를 사용해야 할 때 사용
- SOURCE: 소스 코드까지만 어노테이션 정보가 유지(컴파일 전까지만 유지)
- RetentionPolicy는 어노테이션이 유지되는 시점을 결정
일반적으로 @Retention(RetentionPolicy.RUNTIME)을 사용하여 어노테이션 정보를 런타임까지 유지하는 것이 가장 일반적인 사용 방식이다. 이는 어노테이션을 사용하여 런타임 시 동적으로 프로그램을 구성하거나 검증할 수 있는 기능을 제공하기 때문에 매우 유용하다.
@Constraint - ConstraintValidator를 구현한 클래스와 매핑
import javax.validation.Constraint;
...
@Constraint(validatedBy = TelephoneValidator.class)
public @interface Telephone {
...
}
어노테이션 인터페이스 내부
...
public @interface Telephone {
String message() default "전화번호 형식이 일치하지 않습니다.";
Class[] groups() default {};
Class[] payload() default {};
}
- 어노테이션 인터페이스 내부에 message(), groups(), payload() 요소를 정의해야 한다.
- message(): 유효성 검사가 실패할 경우 반환되는 메시지
- groups(): 유효성 검사를 사용하는 그룹으로 설정
- payload(): 사용자가 추가 정보를 위해 전달하는 값
정규식(Regular Expression)
특정한 규칙을 가진 문자열 집합을 표현하기 위해 쓰이는 형식
- ^: 문자열의 시작
- ^abc는 문자열의 시작이 'abc'인 경우와 매치됩니다.
- $: 문자열의 끝
- xyz$는 문자열의 끝이 'xyz'인 경우와 매치됩니다.
- .: 줄바꿈 문자를 제외한 모든 문자(임의의 한 문자)
- a.c는 'abc', 'axc', 'a7c'와 매치되지만 'ab\nc'와는 매치되지 않습니다.
- *: 바로 앞에 있는 문자가 0번 이상 반복됨
- ab*는 'a', 'ab', 'abb', 'abbb' 등과 매치됩니다.
- +: 바로 앞에 있는 문자가 1번 이상 반복됨
- ab+는 'ab', 'abb', 'abbb' 등과 매치되지만 'a'와는 매치되지 않습니다.
- ?: 바로 앞에 있는 문자가 0번 또는 1번 나타남
- ab?는 'a'와 'ab'와 매치됩니다.
- [,]: 문자 집합. 대괄호 안에 들어가는 문자 중 하나와 매치
- 두 문자 사이는 - 기호로 범위 표현
- [abc]는 'a', 'b', 'c' 중 하나와 매치됩니다.
- {, }: 바로 앞에 있는 문자나 문자 집합의 반복 횟수 또는 범위
- a{3,5}는 'aaa', 'aaaa', 'aaaaa'와 매치됩니다.
- (, ): 괄호 안의 문자열을 하나의 그룹으로 묶어서 처리
- |: '또는'의 의미를 갖는 메타 문자
- a|b는 'a' 또는 'b'와 매치됩니다.
- \: 확장문자,이스케이프 문자로, 특수 문자의 의미를 제거하거나 특수 문자 그 자체를 나타내는 데 사용
- \.은 '.' 문자 자체를 의미합니다.
- \b: 단어 경계
- 단어 경계는 문자와 공백 사이의 경계를 의미합니다.
- \B: 단어 경계가 아닌 부분, 단어가 아닌 것에 대한 경계
- \A: 문자열의 시작, 입력의 시작 부분
- ^와 달리 다중행 모드에서도 항상 문자열의 시작부분과 매치됩니다.
- \G: 이전 매치의 끝
- 첫 번째 매치에서는 문자열의 시작을 나타냅니다.
- \Z: 문자열의 끝 또는 줄바꿈 문자 바로 전의 위치, 종결자가 있는 경우 입력의 끝
- \z: 입력의 끝
- \s: 공백 문자
- 공백, 탭, 줄바꿈 문자 등을 포함합니다.
- \S: 공백 문자가 아닌 문자(^\s와 동일)
- \w: 알파벳, 숫자, 밑줄 문자 등을 포함
- \W: 알파벳이나 숫자가 아닌 문자(^\w와 동일)
- \d: 숫자[0-9]와 동일하게 취급
- \D: 숫자를 제외한 모든 문자(^0-9와 동일)
'spring > spring' 카테고리의 다른 글
[Spring][Exception] Failed to configure a DataSource (0) | 2024.03.04 |
---|---|
[Spring][스프링 부트 핵심 가이드] 예외처리 (0) | 2024.02.28 |
[Spring][Error][Kotlin][jpa] required a bean of type ... that could not be found. (0) | 2024.02.12 |
[Spring][Error] creating bean with name 'jpaAuditingHandler (0) | 2024.02.11 |
[Spring][Error][Swagger]documentationPluginsBootstrapper (0) | 2024.02.11 |