티스토리 뷰

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
링크
«   2024/05   »
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 31
글 보관함