Bean Validation
클라이언트의 요청 값에 대한 유효성 검사로 잘못 입력된 값에 대한 처리를 할 수 있는 유효성 검사 표준 기술입니다. 개발을 진행하다 보면 특정 요청 값이 필수(null, 빈문자 제외), 최소 문자열 길이, 최소 값, 최대 값 등 필요한 데이터 검증이 필요할 때가 있는데 Bean Validation은 일반적으로 많이 사용하는 제약 조건들이 제공하기 때문에 쉽게 어노테이션으로 유효성 검사를 할 수 있도록 도와줍니다.
만약 Bean Validation을 사용하지 않으면 어떻게 될까요?
위 이미지에서 볼 수 있듯이 Bean Validation을 사용하지 않으면 요청 값에 대한 Validation Code를 비즈니스 로직에서 찾아야 하는 문제가 발생합니다. 만약 코드 컨벤션이 정해져 있지 않으면 Validation Code를 Controller, Service, Repository Layer 중 한 곳에서 찾아야 하고, Validation Code의 위치에 대한 코드 컨벤션이 정해져 있더라도 해당 위치(ex - Service Layer)에 비즈니스 로직에서 Validation Code를 찾아야 하기 때문에 추적하기 어려움과 애플리케이션이 복잡성이 증가, Validation Code의 중복 코드가 발생하는 단점이 생깁니다.
Bean Validation은 위에서 나타나는 단점들을 보완해주는 자바 검증 API의 표준 스펙입니다.
Bean Validation은 데이터 유효성 검사 프레임워크로 다양한 제약(Constraint)을 도메인 모델(Domain Model)에 어노테이션으로 유효성 검사를 할 수 있습니다.
- 필요한 제약 조건은 어노테이션만 추가해주면 되기 때문에 중복 코드가 발생하지 않는다
- 요청 값에 대한 제약 조건을 어노테이션으로 쉽게 처리할 수 있다
- 도메인 모델을 보면 어떤 제약 조건들이 있는지 한눈에 파악할 수 있다
1. Bean Validation
1-1. 의존성 주입
build.gradle
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-validation'
...
}
Bean Validation의 의존성 주입을 하게 되면 External Libraries에 validation이 추가된 것을 볼 수 있습니다.
제공하는 Constraint(제약 조건)들이 있는지 하이버네이트 공식문서에서 확인 가능하지만 직접 validation 패키지에서 어노테이션을 찾아보고, 어떻게 검증을 하는지 직접 코드를 보는 것을 추천합니다.
1-2. Constraint(제약 조건)
각 어노테이션의 사용 예시는 하이버네이트 공식 문서에서 확인할 수 있습니다.
Annotation | Description |
@Null | null 만 허용 |
@NotNull | null 을 허용하지 않음 단, 빈문자(""), 공백(" ")은 허용 |
@NotEmpty | 문자열 - null, 빈문자("") 를 허용하지 않음 |
Collection - null 또는 size 가 1이상만 허용 | |
배열 - null 또는 길이(length) 가 1 이상만 허용 | |
올바른 형식의 이메일인지 검사 단, null 허용 |
|
@Pattern(regexp = ) | 정규식을 이용한 유효성 검사 |
@Size(min =, max = ) | 문자열 -문자열의 길이를 제한 |
Collection, Map - 크기(size) 제한 | |
배열 - 길이(length)를 제한 | |
@Min(value = ) | 값이 지정된 최소값보다 크거나 같을 경우 허용 |
@Max(value = ) | 값이 지정된 최대값보다 작거나 같을 경우 허용 |
@Positive | 양수만 허용 단, 0은 포함하지 않음 |
@PositiveOrZero | 0 또는 양수만 허용 |
@Negative | 음수만 허용 단, 0은 포함하지 않음 |
@NegativeOrZero | 0 또는 음수만 허용 |
@Future | 현재보다 미래 시간만 허용 |
@FutureOrPresen | 현재 또는 미래 시간만 허용 |
@Past | 현재보다 과거 시간만 허용 |
@PastOrPresent | 현재 또는 과거 시간만 허용 |
@AssertFalse | false만 허용 단, null도 허용 |
@AssertTrue | true만 허용 단, null도 허용 |
2. Custom Constraint
Bean Validation은 일반적으로 많이 사용하는 제약 조건들을 제공하지만 Bean Validation에서 제공하지 않을 경우 필요한 제약 조건을 직접 만들어 사용할 수 있습니다. 방법은 총 세 가지가 있으며 상황에 따라 적절한 방법을 선택하시면 됩니다.
2-1. Bean Validation에서 제공하는 @AssertTrue 사용
@AssertTrue
와 Get 메서드를 사용하여 유효성 검사를 할 수 있습니다. 요청 값에 대한 유효성 검사를 하기 위해 get 메서드를 만들어 해당 요청 값이 유효하면 true, 유효하지 않으면 false로 return 하여 유효성 검사를 실행할 수 있습니다. 이 방법은 한 번만 사용할 경우, 즉 다른 곳에서 사용할 일이 없다고 생각되는 경우 추천합니다.
아래 예시 코드는 name과 nickName 중 적어도 1개 이상 입력을 받도록 유효성 검사를 실행한 코드입니다.
@Getter
public class CustomConstraintV0Request {
private String name;
private String nickName;
@AssertTrue(message = "이름과 닉네임 중 하나는 입력해주세요.")
public boolean getNameAndNickNameValidation() {
// 이름, 닉네임 모두 입력안했을 경우 false를 return 하여 예외 발생
return StringUtils.hasText(name) || StringUtils.hasText(nickName) ? true : false;
}
}
@RestController
public class ValidationController {
@PostMapping("/v0/custom-constraint")
public DefaultHttpResponse<Void> customConstraintV0(
@Valid @RequestBody CustomConstraintV0Request request
) {
return new DefaultHttpResponse<>(BaseCode.SUCCESS);
}
}
다시 한번 강조하지만 @AssertTrue
를 사용하는 Custom Validation은 여러 곳에서 유효성 검사를 하지 않고 한 곳에서 유효성 검사를 하는 경우에 사용하시는 것을 추천합니다. 여러 곳에서 사용하는 경우 일일이 Validation에 필요한 Get 메서드를 추가해야 하기 때문에 중복코드가 발생하고, 유지보수가 어려워집니다. 따라서 일회성 유효성 검사를 해야하는 경우에 사용하시는 것을 추천합니다.
@RequestBody
는 내부에서 ObjectMapper
를 통해 객체에 데이터를 바인딩시켜 줍니다. ObjectMapper
는 Reflection을 통해 Dto 필드에 값을 넣어주기 때문에 Getter와 Setter를 사용하여 데이터가 바인딩이 됩니다. 즉, 데이터 바인딩 과정에서 Getter들이 호출되기 때문에 Get 메서드를 만들어 유효성 검사를 실행할 수 있습니다.
관련 글
[Spring] HTTP 요청 데이터 바인딩(@RequestParam, @ModelAttribute, @RequestBody, @RequestHeader) (tistory.com)
2-2. Bean Validation에서 제공하는 @Pattern 사용
이 방법은 사용 빈도가 적고, 정규식으로 제약 조건을 해결할 수 있는 경우 사용하시는 것을 권장합니다.
@Getter
public class CustomConstraintV1Request {
@Pattern(regexp = "^\\S+$", message = "이름에 공백 문자가 없어야합니다")
private String name;
}
@RestController
public class ValidationController {
@PostMapping("/v1/custom-constraint")
public DefaultHttpResponse<Void> customConstraintV1(
@Valid @RequestBody CustomConstraintV1Request request
) {
return new DefaultHttpResponse<>(BaseCode.SUCCESS);
}
}
2-2. Custom Constraint Annotaion
사용 빈도가 많거나 복잡한 제약 조건이 필요한 경우 직접 어노테이션을 만들어 사용하는 방법입니다. 실제 실무에서 많이 사용하는 방법이므로 한 번쯤 직접 만들어서 적용해 보시는 것을 추천드립니다.
다음 조건들을 적용하여 Custom Constraint Annotation을 만들어 보겠습니다.
- 문자열에 공백 문자가 없어야 한다.
- 문자열 List에 공백 문자가 없어야 한다.
NoWhiteSpace 어노테이션 생성
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = NoWhitespaceValidator.class)
public @interface NoWhitespace {
String message() default "공백 문자를 포함할 수 없습니다.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
- @Target({ElementType.FIELD, ElementType.PARAMETER})
NoWhitespace 어노테이션을 필드와 메서드 파라미터에 적용할 수 있도록 지정합니다.
public enum ElementType {
TYPE, // class, interface(주석 interface 포함), enum, record
FIELD, // 필드
METHOD, // 메서드
PARAMETER, // 메서드 파라미터
CONSTRUCTOR, // 생성자
LOCAL_VARIABLE, // 지역 변수
ANNOTATION_TYPE, // 어노테이션
PACKAGE, // 패키지
TYPE_PARAMETER, // 타입 파라미터
TYPE_USE,
MODULE,
RECORD_COMPONENT;
}
- @Retention(RetentionPolicy.RUNTIME)
NoWhitespace 어노테이션의 정보가 런타임까지 유지되도록 설정합니다.
public enum RetentionPolicy {
SOURCE, // 컴파일러에 의해 폐기
CLASS, // 클래스 파일에 기록, VM에 의해 유지 X
RUNTIME // 클래스 파일에 기록, VM에 의해 유지 O(런타임)
}
- @Constraint(validatedBy = NoWhitespaceValidator.class)
NoWhitespace 어노테이션과 연결된 검증 로직을 가진 NoWhitespaceValidator 클래스를 지정합니다.
- String message() default "공백 문자를 포함할 수 없습니다."
검증 실패 시 출력할 오류 메시지를 정의합니다.
- Class<?>[] groups() default {}
검증 그룹을 지정할 수 있습니다. 기본적으로는 빈 배열로 설정되어 있습니다.
- Class<? extends Payload>[] payload() default {}
페이로드 데이터를 사용할 수 있도록 지원하는 기능입니다. 기본적으로는 빈 배열로 설정되어 있습니다.
NoWhitespaceValidator - 검증 로직을 수행하는 클래스 구현
public class NoWhitespaceValidator implements ConstraintValidator<NoWhitespace, Object> {
@Override
public void initialize(NoWhitespace constraintAnnotation) {
// 초기화 로직이 필요하지 않은 경우 생략 가능
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
if (value == null) {
return true; // null 값은 검증하지 않음
}
// String type 검증 처리
if (value instanceof String) {
return !((String) value).contains(" "); // 공백 문자가 포함되지 않은 경우 true 반환
}
// List type 검증 처리
if (value instanceof List) {
List<?> list = (List<?>) value;
for (Object element : list) {
if (element != null && element.toString().contains(" ")) {
return false; // 공백 문자가 포함된 요소가 있으면 검증 실패
}
}
return true;
}
return true;
}
}
사용 예시
@Getter
public class CustomConstraintV2Request {
@NoWhitespace
private String name;
@NoWhitespace
private List<String> nameList = new ArrayList<>();
}
'Backend > Spring' 카테고리의 다른 글
[Spring] @Value와 @ConfigurationProperties (0) | 2023.07.09 |
---|---|
[Spring] HTTP 요청 데이터 바인딩(@RequestParam, @ModelAttribute, @RequestBody, @RequestHeader) (0) | 2023.07.02 |
[Spring] HTTP 요청 매핑 어노테이션(@RequestMapping) (0) | 2023.07.01 |