본문 바로가기
Web/Spring

[Spring] @Valid, @Validated과 Custom Annotation (2)

by 희조당 2023. 3. 21.
728x90

😋 들어가기 앞서

1편에 이어서 커스텀 어노테이션을 구현하고 이에 필요한 예외를 어떻게 처리했는지 다루겠다!

유효성 처리를 위해서 세 가지 단계가 필요하다.

  • 어노테이션 구현하기
  • 유효성 검사기 구현하기
  • 발생하는 예외 다루기

🪄 어노테이션 구현하기

비밀번호를 검증하는 어노테이션을 통해서 이해해 보겠다!

어노테이션이 어떻게, 어디서, 무엇으로, 누구에게 사용될지 등의 정보는 메타 어노테이션과 필드값으로 명시해줘야 한다.

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PasswordFormatValidator.class)
public @interface PasswordFormat {
    String message() default "사용할 수 없는 비밀번호 입니다.";

    Class[] groups() default {};

    Class[] payload() default {};
}

🌀 메타 어노테이션

무엇으로, 언제까지, 누구에게 검증할지에 대한 정보를 명시해 준다. 자주 사용되는 어노테이션은 다음과 같다.

  • @Target() : 누구에게 사용할지에 대한 정보이다. Filed, Parameter, Class 등 어노테이션의 역할에 맞게 명시해 준다. 여기서는 DTO의 필드값을 검사하므로 Filed만 적어주었다.
  • @Retention() : 언제까지 어노테이션을 유지할지에 대한 정보이다. RUNTIME, CLASS, SOURCE 세 가지 옵션이 존재하는데, 애플리케이션이 동작하는 중에 계속 확인해야 하므로 RUNTIME으로 선택했다.
  • @Constraint() : 무엇으로 검증할지에 대한 정보이다. 단순하게 어노테이션을 구현했다고 검증이 되는 것이 아니라 따로 검증을 해주는 클래스를 만들어주어야 한다.

그 외에 @Documented, @Inherited, @Repeatable 등이 존재하는데 보통 3가지를 많이 사용한다.

📕 필드

그 외에 기본적인 메시지와 같은 정보를 필드값으로 넣어준다. 간단하게 알아보자.

  • message() : 검사에 실패했을 때 전달할 메시지이다. 
  • groups() : 어느 대상까지 검사를 할지에 대한 정보이다. @Validated를 사용해서 그룹을 묶을 때 사용한다.
  • payload() : 검증 시 전달될 메타정보이다. 중요도와 같은 정보를 전달할 수 있는데 자주 사용되지 않는다.

🛠️ 유효성 검사기 구현하기

위와 같은 방법으로 어노테이션을 구현했다면 이제 실질적으로 동작할 검증기를 구현해야 한다.

javax.validationConstraintValidator 인터페이스를 구현해주어야 한다.

package javax.validation;

public interface ConstraintValidator<A extends Annotation, T> {
	default void initialize(A constraintAnnotation) { }
    
	boolean isValid(T value, ConstraintValidatorContext context);
}

내부 구현을 보면 다음과 같은 메서드들이 존재한다.

  • initialize() : 검증기를 초기화하는 메서드. 디폴트 메서드이므로 따로 초기화할 일이 없다면 구현하지 않아도 무방하다.
  • inValid() : 실질적으로 유효성을 검증하는 메서드. 꼭 구현해주어야 한다.

검증기를 구현하면 다음과 같다. Pattern의 인스턴스는 비용이 높기 때문에 미리 캐싱해 두고 사용하였다.

public class PasswordFormatValidator implements ConstraintValidator<PasswordFormat, String> {
    private static final Pattern PASSWORD_PATTERN = Pattern
                        .compile("^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&])[A-Za-z\\d@$!%*#?&]{8,20}$");
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        final Matcher matcher = PASSWORD_PATTERN.matcher(value);
        return matcher.matches();
    }
}

✂️ 발생하는 예외 다루기

1️⃣ @Valide 예외

1편@Valid로 발생한 예외는 MethodArgumentNotValidException라고 했다.

예외 핸들러에서 이 예외를 잡아주면 되는데 에러에 대한 핸들러가 이미 ResponseEntityExceptionHandler에 구현되어 있어 재정의 했다. (23.05.17 수정)

@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(BAD_REQUEST)
public ErrorResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
    final String message = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
    final String caused = e.getParameter().getExecutable().toGenericString();
    log.error("ValidateFailed : {}", caused);
    return new ErrorResponse(ErrorCode.REQUEST_VALIDATION_FAIL, message);
}

커스텀 어노테이션을 통해서 많은 곳에서 유효성 검사를 진행한다.

따라서, 예외가 어디서 발생하는지 확인하기 위해서 발생 지점까지 로그로 남겨두었다!

2️⃣ @Validated 예외

한편, @Validated로 발생한 예외는 ConstraintViloationException이다.

GlobalExceptionHandler에서 간단하게 다음처럼 예외를 받아서 처리해 주면 된다.

@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(BAD_REQUEST)
public ErrorResponse handleConstraintViolationException(ConstraintViolationException e) {
    final String message = e.getMessage();
    log.error("ValidateFailed : {}", message);
    return new ErrorResponse(ErrorCode.REQUEST_VALIDATION_FAIL, message);
}

😋 정리

지금까지 @Valid@Validated의 동작과 커스텀 어노테이션을 어떻게 구현해야 하고 사용해야 하는지 알아봤다!

1편에서 언급한 것처럼 분명히 간편하고 비즈니스 로직에 집중하도록 해주지만,

어노테이션을 사용하면 의도가 숨겨지기 때문에 목적이 분명할 때 사용해야 한다.


1편 : https://codinghejow.tistory.com/362

 

-Reference

https://chat.openai.com/chat

 

😋 지극히 개인적인 블로그지만 훈수와 조언은 제 성장에 도움이 됩니다 😋

댓글