SpringBoot

[Spring Boot] Custom validator annotation 만들기

myeongju 2024. 11. 8. 17:50
반응형

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를 리턴하면 된다.

 

코드 설명

  1. context.disableDefaultConstraintViolation();
    • 기본 오류 메시지를 비활성화
  2. context.buildConstraintViolationWithTemplate(...)
    • 사용자 정의 오류 메시지를 설정
  3. 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) { }

 

반응형