티스토리 뷰

컨트롤러에서 값을 받을 때 보통 아래와 같이 'javax.validation.constraints' 제공하는 어노테이션들을 사용해서 파타미터의 값을 체크한다.

@NotNull
@NotEmpty

 

 

하지만 이는 단순 체크를 위한 어노테이션으로 우리가 특수한 상황에(예를 들어 파라미터로 받는 DTO의 inner DTO의 특정 필드는 null이면 안 된다든지 특정 값 내에 있어야한다든지 등) 맞춰 내가 원하는 대로 값을 체크해주지 않는다.

그래서 이와 같이 여러 가지 경우를 고려해서 값을 체크해야하는 경우 Service 단에서 파라미터 안의 값을 꺼내 예외 처리를 해주곤 하는데,

constraintValidator와 custom annotation을 구현하면 아예 Controller로 데이터가 들어오기 전인 Interceptor 단계에서 값을 체크하고 예외 처리를 할 수 있다.

 

 

ConstraintValidator 구현하기

ConstraintValidator는 값의 유효성을 체크하는 어노테이션을 제공하는 'javax.validation'에서 제공하는 인터페이스이다.

ConstraintValidator 값을 체크하거나 유효성을 체크할 필드에 붙일 어노테이션을 init하는 함수와 클래스 형태를 명시해 놓은 것으로 안에는 별다른 기능은 없다.

public interface ConstraintValidator<A extends Annotation, T> {
	default void initialize(A constraintAnnotation) {
	}

	boolean isValid(T value, ConstraintValidatorContext context);
}

 

 

ConstraintValidator는 아래와 같이 Implements해서 사용할 수 있다.

ConstraintValidator<${유효성 검사를 할 어노테이션}, ${유효성 검사를 할 Class}>

따라서 만일 TestData라는 DTO 클래스에 대해 유효성 검사를 하고 싶으면 아래와 같이 TestValid라는 검사를 할 TestData에 붙일 커스텀 어노테이션과 유효성 검사를 할 클래스인 TestData를 넣어주면 된다.

public class TestValidator implements ConstraintValidator<TestValid, TestData> {
...

 

 

예를 들어 TestData안에 있는 int value1이 무조건 -1~1에 포함되어야 한다고 할 때 아래와 같이 isValid 안에 예외 처리를 정의할 수 있다.

public class TestValidator implements ConstraintValidator<TestValid, TestData> {
    @Override
    public void initialize(TestValid addressValid) {
    }

    @Override
    public boolean isValid(TestData data, ConstraintValidatorContext context) {
    	context.disableDefaultConstraintViolation(); // 기본 메시지 제거
        
        if(data.getValue1() < -1 && data.getValue1 > 1) {
        	context.buildConstraintViolationWithTemplate("value1 must between -1 and 1")
                    .addConstraintViolation();
        	return false;
        }

        return true;
    }
}

 

 

값의 유효성을 검사하는 isValid를 좀 더 자세히 보면 TestData를 받아서 조건문으로 처리를 하고 에러 메시지를 context.buildConstraintViolationWithTemplate(${message}).addConstraintVaiolation()로 넘겨준다.

그리고 false를 반환하는데 false를 반환할 경우 MethodArgumentNotValidException을 발생시키고 isValid에 지정해놓은 메시지가 요청한 곳으로 반환된다.

org.springframework.web.bind.MethodArgumentNotValidException

 

 

만일 context.disableDefaultConstraintViolation();를 넣지 않을 경우 어노테이션에 지정한 message까지 뒤에 덧붙여져서 전달되므로 isValid에서 에러 메시지를 재정의 한다면 꼭 넣어서 어노테이션의 메시지를 제거 시켜주는 것이 좋다.

 

- 기본

잘못된 파라미터 입니다., [DEBUG] errorMessage: testData: Invalid value1, testData: Annotation message

 

- disableDefaultConstraintViolation() 호출 시

잘못된 파라미터 입니다., [DEBUG] errorMessage: testData: Invalid value1

 

 

Custom Annotation 구현

constraintValidator를 구현했다면 유효성 검사를 할 필드를 특정지을(constraintValidator를 적용할) 범위를 지정할(?) 커스텀 어노테이션을 생성해야 한다.  constraintValidator를 사용할 어노테이션은 아래의 기본 값들을 가지고 있어야 한다. 참고로 이 값들은 @NotNull, @NotEmpty와 같이 javax.validation에서 제공하는 유효성 검사 어노테이션에도 있는 값들이다.

@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = TestValidator.class)
public @interface TestValid {
	String message() default "Invalid TestData";

	Class<?>[] groups() default {};

	Class<? extends Payload>[] payload() default {};
}

 

  • message: 유효성 검사 false시 반환할 기본 메시지
  • groups: 어노테이션을 적용할 특정 상황(예를 들어 특정 Class 시 어노테이션 동작)
  • payload: 심각한 정도 등 메타 데이터를 정의해 넣을 수 있음

 

솔직히 payload 값에 대해서는 와닿지 않는다 잘 사용을 안할뿐더러 구글링을 해도 예제가 심각한 정도를 표현하기 위해 INFO, DEBUG 등과 같이 자기가 정의한 레벨 정도를 넣는 것 밖에 없기 때문이다.

하지만 Payload를 extends 받는 것으로 봐서 유효성 검사에 사용할 메타 데이터를 넣을 수 있는 정도로 이해했다.

(groups와 payload에 대해서는 따로 찾아보고 테스트해서 글을 작성해야겠다.)

 

 

@Target과 @Retention은 해당 어노테이션을 어디에서 사용할지와 어느 단계까지 유지할 것인지에 대한 설정으로 아래 글을 참고 바란다.

https://pamyferret.tistory.com/47?category=878247 

 

custom annotation(커스텀 어노테이션) 만들기

개발을 하다보면 AOP 적용을 위해서 별도로 커스텀 어노테이션을 만들어야할 일이 생긴다. 물론 AOP 적용을 패턴을 통해 적용시킬 수 있지만 같은 공통 기능을 사용해야하는 메소드들에 공통점이

pamyferret.tistory.com

 

 

여기서 중요한 것은 @Constraint() 안에 아래와 같이 아까 정의해둔 맞는 constraintValidator를 넣어야한다는 것이다.

@Constraint(validatedBy = TestValidator.class)
public @interface TestValid {
...

 

 

constraintValidator와 유효성 검사를 사용할 어노테이션은 서로 매핑되는 관계로 이루어져 있어야하며 validatedBy에는 한 가지 class가 아닌 아래와 같이 배열 형태로 여러가지 유효성 검사기(?)를 넣을 수 있다.

@Constraint(validatedBy = {TestValidator1.class, TestValidator2.class})
public @interface TestValid {
...

 

 

 

유효성 검사 사용 결과

constraintValidator와 유효성 검사할 필드를 특정지을 커스텀 어노테이션을 생성했다면 아래와 같이 어노테이션을 사용하면 해당 값에 대해 유효성 검사를 실시한다.

public class TestParam {
	@TestValid
        TestData data;
      
        ...
}

 

 

아래와 같이 Controller가 정의된 상황에서 data의 value1에 -1~1에 포함되지 않는 값을 집어넣고 api를 요청한다면 MethodArgumentNoValidException이 발생하게 된다.

public class TestController {
@RequestMapping(value="/test", method = RequestMethod.POST, produces = "application/json")
    public CodeResponse test(@RequestBody @Valid TestParam param) {
        ...
    }
}

 

 

 

만일 파라미터 앞에 @Valid 어노테이션을 넣지 않을 경우 파라미터 안에 유효성을 체크하는 어노테이션들은 다 작동을 안 하므로 꼭 @Valid 어노테이션을 넣어줘야 한다. 컨트롤러에서 파라미터를 받기 전에 미리 유효성 체크를 하고 그 유효성 체크 과정에 구현해 넣은 constraintValidator도 동작을 한다.

 

 

 

 

이번에 유효성 체크를 constraintValidator를 구현해서 interceptor 단계에서 파라미터의 유효성 검사를 하다보니 확실히 커스텀 어노테이션을 많이 활용해야겠다는걸 깨달았다.

지금까지는 커스텀 어노테이션을 사용해본적이 없어서 커스텀 어노테이션이 이렇게 다양하게 쓰일 줄 몰랐다는... 정말 우물 안의 개구리였던 것 같다.

 

 

 

✋ @Constraint Document

https://docs.oracle.com/javaee/7/api/javax/validation/Constraint.html

 

Constraint (Java(TM) EE 7 Specification APIs)

ConstraintValidator classes must reference distinct target types for a given ValidationTarget If two ConstraintValidators refer to the same type, an exception will occur. At most one ConstraintValidator targeting the array of parameters of methods or const

docs.oracle.com

 

✋ ConstraintValidator Document

https://docs.oracle.com/javaee/7/api/javax/validation/ConstraintValidator.html

 

ConstraintValidator (Java(TM) EE 7 Specification APIs)

Implements the validation logic. The state of value must not be altered. This method can be accessed concurrently, thread-safety must be ensured by the implementation.

docs.oracle.com

 

 

 

 

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
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
글 보관함