😋 들어가기 앞서
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.validation
의 ConstraintValidator
인터페이스를 구현해주어야 한다.
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
라고 했다.
예외 핸들러에서 이 예외를 잡아주면 되는데 에러에 대한 핸들러가 이미 (23.05.17 수정)ResponseEntityExceptionHandler
에 구현되어 있어 재정의 했다.
@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편에서 언급한 것처럼 분명히 간편하고 비즈니스 로직에 집중하도록 해주지만,
어노테이션을 사용하면 의도가 숨겨지기 때문에 목적이 분명할 때 사용해야 한다.
'Web > Spring' 카테고리의 다른 글
[Spring] 트랜잭션 사용 조심하기 (2) | 2023.05.29 |
---|---|
[Spring] @ModelAttribute (2) | 2023.04.19 |
[Spring] @Valid, @Validated과 Custom Annotation (1) (0) | 2023.03.19 |
[토비의 스프링] 테스트 (2장) (0) | 2023.01.16 |
[토비의 스프링] 오브젝트와 의존관계 (1장) (0) | 2022.12.21 |
댓글