티스토리 뷰
Spring Web Project 를 진행하면서 @Valid 어노테이션을 자주 사용하여 validation 검증을 진행하고 있습니다. 하지만 기존 javax validation 이나, hibernate 에서 제공하는 validation annotation 등으로 (@NotBlank, @Length, @URL, @NotNull 등등.. ) validation 검증을 하기에 힘든 부분이 있어 이를 해결하는 방법의 하나로 이 글을 작성하게 되었습니다.
블로그 코드는 GitHub 에서 볼 수 있습니다
보통 Spring Web Project 에서 validation 을 검증할 경우 아래와 같이 진행하게 됩니다.
Controller
@RestController
public class ContentController {
/**
* ex) /categories/it/contents?from=2018020211111
* */
@GetMapping("/categories/{category}/contents")
public String contents(@PathVariable String category, @Valid ContentRequest request) {
return "success category : " + category + ", from : " + request.getFrom();
}
}
Request
@Setter
@Getter
@NoArgsConstructor
public class ContentRequest {
/**
* from 의 길이는 14 이고 숫자여야만 한다.
* */
@Length(min = 14, max = 14)
@Pattern(regexp="[0-9]+")
private String from;
}
Controller 에서 @Valid 어노테이션을 사용하여 해당 Request 객체를 검증합니다.
검증할때에 Hibernate Validator 라이브러리에 있는 여러 어노테이션을 사용합니다.
mavenRepository gogo!
spring-boot-starter-web 에는 기본적으로 dependencie 가 걸려 있습니다~!
다시 본론으로 들어와서!
위 ContentRequest from 필드의 @Length, @Pattern 으로 from 값은 길이가 14인 숫자형태의 string 이 되었습니다.
하지만 위 어노테이션으로 validation 을 제대로 검증을 할 수 없는 문제가 생겼습니다.
@PathVariable String category 는 category 들중 특정 카테고리만 허용하여야 했고,
ContentRequest 의 String from 은 검색가능한 시간 이라는 조건이 추가 되었습니다. 검색가능한 시간은 최소 2017년 이후 부터 가능하였습니다.
그러면 여기에서 고민을 하기 시작하였습니다.
우선 @PathVariable String category 와 ContentRequest from 값을 받은 후에
Controller 레이어나 Service 레이어 혹은 카테고리 검증하는 헬퍼클래스에서 validation 을 추가로 검증할지
아니면 위 @Valid 어노테이션을 사용하여 한번에 처리 할수 있는지 고민을 하였습니다.
열심히 구글링을 하였고..
사용자가 직접 커스텀하게 어노테이션을 만들수 있다는 정보를 얻었습니다.
아래부터는 제가 직접 만든 어노테이션 입니다.
@Setter
@NoArgsConstructor
public class ContentRequest {
@NotBlank
@CategoryValid
private String category;
@DateValid
private String from;
/**
* @return Category enum 값.
* */
public Category getCategory() {
if(StringUtils.isEmpty(this.category)) {
return null;
}
return Category.valueOf(category.toUpperCase());
}
/**
* @return LocalDateTime 으로 변환된 from 값.
* */
public LocalDateTime getFrom() {
if(StringUtils.isEmpty(this.from)) {
return null;
}
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DateFormatConstant.YYYYMMDDHHMMSS);
return LocalDateTime.parse(this.from, formatter);
}
}
기존 category 필드의 @CategoryValid 가 있고
from 절에 @Length, @Pattern 대신에 @DateValid 가 생겼습니다.
@CategoryValid
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CategoryValidator.class)
public @interface CategoryValid {
// message 정의
String message() default "허용하지 않는 카테고리 입니다.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
CategoryValidator.class
public class CategoryValidator implements ConstraintValidator<CategoryValid, String> {
@Override
public void initialize(CategoryValid constraintAnnotation) { }
/**
* value 값으로 validation check 를 진행한다.
* @return 현재 허용가능한 카테고리면 true, 아니면 false
* */
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return Category.isActivation(value);
}
}
public interface ConstraintValidator<A extends Annotation, T>
위 인터페이스를 구현하는 CategoryValidator 클래스에서 isValid 부분을 재정의 하고 (true 면 검증성공, false 면 실패)
@CategoryValid 에서 default message 을 정의합니다. message 값은 검증이 실패하였을경우 나타나느 메세지 입니다.
위와 동일하게 @DateValid 입니다.
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = DateValidator.class)
public @interface DateValid {
// message 정의
String message() default "날짜는 " + DateValidator.MIN_DATE_MSG + " 보다 커야 하며, " + "포멧은 "+ DateFormatConstant.YYYYMMDDHHMMSS +" 형식으로 보내야 합니다.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class DateValidator implements ConstraintValidator<DateValid, String> {
public final static LocalDateTime MIN_DATE = LocalDateTime.of(2017,1,1,0,0,0);
public final static String MIN_DATE_MSG = "2017년 1월 1일";
@Override
public void initialize(DateValid constraintAnnotation) { }
/**
* value 값으로 validation check 를 진행한다.
* 1. 검색가능한 범위인지 확인
* 2. LocalDateTime 으로 변환되는지 확인
* */
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
//null 허용.
if(StringUtils.isEmpty(value)) {
return true;
}
//localDateTime 으로 변환되는지 검증.
try {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DateFormatConstant.YYYYMMDDHHMMSS);
LocalDateTime date = LocalDateTime.parse(value, formatter);
//최소 검색 시간 검증.
if(date.isBefore(MIN_DATE)) {
return false;
}
return true;
} catch (DateTimeParseException e) {
return false;
}
}
}
즉 @DateValid 어노테이션을 사용하면 isValid 메소드에서 데이터 검증이 이루어지고 true 를 반환하면 성공, false 를 반환하면 실패가 됩니다.
따라서 저는 isValid 메소드에 여러 로직을 넣을수가 있게 되었습니다.
아래는 실제로 @DateValid validation 이 실패하였을경우 결과 메세지 입니다.
GET http://localhost:8080/categories/it/contents?from=20160202111111
{
--- 생략 ---
,
"defaultMessage": "날짜는 2017년 1월 1일 보다 커야 하며, 포멧은 yyyyMMddHHmmss 형식으로 보내야 합니다.",
"objectName": "contentRequest",
"field": "from",
"rejectedValue": "20160202111111",
"bindingFailure": false,
"code": "DateValid"
}
],
--- 생략---
}
@CategoryValid validation 이 실패하였을 경우 입니다.
GET http://localhost:8080/categories/game/contents?from=20190202111111
{
--- 생략 ---
,
"defaultMessage": "허용하지 않는 카테고리 입니다.",
"objectName": "contentRequest",
"field": "category",
"rejectedValue": "game",
"bindingFailure": false,
"code": "CategoryValid"
}
],
--- 생략---
}
결론
@Valid 어노테이션과 함께 사용할수 있는 파라미터 검증하는 어노테이션을 개발자가 직접 커스텀하게 만들수 있으며 validation 을 더욱더 강하게 진행을 할 수 있다는걸 알게 되었습니다. 추가적으로 해당 parameter 의 validation 검증을 함에 있어 test 도 쉽게 되어 ( isValid 메소드를 test 를 진행 ) 수월하게 개발을 할 수 있었습니다!
- Total
- Today
- Yesterday
- spring
- fetch join
- SpringBoot
- 쿼리
- Docker
- spring-data-jpa
- insert
- 개발
- jpa
- orm
- JUnit
- 페치조인
- @Valid
- SonarQube
- fetchjoin
- query dsl
- Database
- ec2
- web
- QueryDSL
- error
- hibernate
- n+1
- gradle
- DEMONIZE
- Validation
- query
- Limit
- Spring Boot
- Jenkins
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |