[Spring Boot] Custom validator annotation 만들기
API를 만들다보면 종종 클라이언트가 보낸 값을 검사하는 과정이 필요해진다. 그럴때 검증하는 로직들을 매 비즈니스 로직마다 반복적으로 넣지 말고, 어노테이션을 만들어서 사용해보도록 하자.
ConstraintValidator 가 뭔데 ?
ConstraintValidator은 jakarta.validation에서 제공하는 유효성 검증 인터페이스이다.
ConstraintValidatorsns Controller 진입 전인, Interceptor에서 동작한다.
이 반식은 똑같은 코드를 여러 번 반복해서 작성할 필요가 없으며, 코드 통일성 유지할 수 있어 개발 효율성을 높일 수 있다는 장점이 있다. 객체 지향 관점에서 바라봤을때도, 결합도를 낮추고 응집력을 높일 수 있다는 점에서 좋다.
ConstraintValidator에서 유효성 검증을 실패하는 경우, 발생하는 Exception은 다음과 같다.
MethodArgumentNotValidException
발생 경우
- @RequestBody 내부에서 처리에 실패한 경우
- @Validated, @Valid에서 처리되지 못 한 경우
상태코드
- HTTP 상태코드 400으로 처리된다.
이번에 진행중인 프로젝트에서 선택할 수 있는 최대 감정 개수가 5로 제한하는 요구사항이 있다. 이부분을 validator으로 만들어보자.
ConstraintValidator를 이용해서 Custom Validator 생성하기
public class MaxEmotionValidator
implements ConstraintValidator<MaxEmotionCheck, List<EmotionOfEpisodeDto>> {
private static final int MAX_EMOTIONS = 5;
@Override
public boolean isValid(
List<EmotionOfEpisodeDto> emotionList, ConstraintValidatorContext context) {
if (getTotalEmotions(emotionList) > MAX_EMOTIONS) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(
"선택할 수 있는 감정은 최대 " + MAX_EMOTIONS + "개 입니다.")
.addConstraintViolation();
return false;
}
return true;
}
private int getTotalEmotions(List<EmotionOfEpisodeDto> emotionList) {
return emotionList.stream().mapToInt(emotion -> emotion.details().size()).sum();
}
}
ConstraintValidator
를 상속받은 후 isValid
를 오버라이드 해서 유효성 검증 로직을 작성해주면 된다. 유효성 검증에 성공할 경우 true, 실패할 경우 false를 리턴하면 된다.
코드 설명
context.disableDefaultConstraintViolation();
- 기본 오류 메시지를 비활성화
context.buildConstraintViolationWithTemplate(...)
- 사용자 정의 오류 메시지를 설정
addConstraintViolation();
- 설정한 사용자 정의 메시지를 유효성 검사 결과에 추가
Custom Validator을 이용한 Annotation 만들기
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MaxEmotionValidator.class)
public @interface MaxEmotionCheck {
String message() default "선택할 수 있는 감정은 최대 5개 입니다.";
Class[] groups() default {};
Class[] payload() default {};
}
@Target
에는 여러 옵션이 있는데, 필자는 필드에 어노테이션을 사용해 줄 것이기에, FIELD
옵션을 사용했다@Constraint(validatedBy = ...)
에는 이전에 우리가 작성한 Custom Validator class를 입력해주면 된다.
실제로 적용해보기
public record DiaryRequest(
String episode,
String thoughtOfEpisode,
@NotEmpty @MaxEmotionCheck @UniqueEmotionTypeCheck
List<EmotionOfEpisodeDto> emotionOfEpisodes,
String resultOfEpisode,
String empathyResponse) { }