스프링 사용자 지정(커스텀) 유효성 체크 및 예외처리

Belluga·2022년 2월 27일
1

이전 포스팅에서 Hibernate Validator를 통해 어노테이션 기반으로 제약사항을 표현하고 유효성 체크 및 예외처리를 구현하는 방법에 대해 알아보았습니다.

이번 포스팅에서는 사용자 지정(커스텀) 유효성 체크 및 예외처리 방식을 살펴보도록 하겠습니다.

기존 유효성 체크

@Getter
@AllArgsConstructor
@NoArgsConstructor
public class BookingRequestDto {

    @NotNull(message = "체크인 날짜를 지정해야 합니다.")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate checkInDate;

    @NotNull(message = "체크아웃 날짜를 지정해야 합니다.")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate checkOutDate;

    public Booking toEntity(long userId, long roomTypeId) {
        return Booking.createInstance(userId, roomTypeId, checkInDate, checkOutDate);
    }
}

위 코드는 숙박업소 예약 시 사용하는 BookingRequestDto 입니다.
javax.validation.constraints 패키지에 정의 된 @NotNull 유효성 검증 애노테이션을 사용하였습니다.

@DateTimeFormat 애노테이션은 String 으로 들어오는 날짜 데이터를 LocalDate 타입으로 deserialize 하는 과정에서 생기는 에러를 해결하기 위해 사용하였습니다.

따라서 요청 시 checkOutDate를 지정하지 않으면 400 Status 와 함께 알맞은 예외 메시지를 전달받게 됩니다.

문제점

체크아웃 날짜는 체크인 날짜를 앞설 수 없다는 비즈니스 로직이 추가되었을 때, 기존에 존재하는 애노테이션만으로는 해당 데이터의 유효성을 검증할 수 없습니다.

따라서 사용자 지정 커스텀 애노테이션을 통해 유효성을 체크해주어야 합니다.

커스텀 어노테이션 만들기

@Constraint(validatedBy = BookingPeriodValidator.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface BookingPeriod {

    String message() default "기간이 유효하지 않습니다.";

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

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

@Constraint 애노테이션을 사용하여 필드를 검증할 클래스를 정의했습니다.
message()는 사용자에게 표시되는 오류 메시지입니다.

Validator 만들기

public class BookingPeriodValidator implements ConstraintValidator<BookingPeriod, BookingRequestDto> {

    @Override
    public void initialize(BookingPeriod constraintAnnotation) {

    }

    @Override
    public boolean isValid(BookingRequestDto bookingRequestDto, ConstraintValidatorContext context) {
        return bookingRequestDto.isValidPeriod();
    }
}

ConstraintValidator 인터페이스를 구현하는 유효성 검사 클래스를 생성합니다.
첫번 째 제네릭 타입은 커스텀 애노테이션, 두번 째 제네릭 타입은 유효성을 검증할 대상이 되는 클래스를 지정합니다.

이 때 isValid 메서드를 구현해야 하는데 해당 메서드의 return 결과가 유효성 검사 통과 유무를 의미하게 됩니다.

BookingRequestDto 수정

@Getter
@AllArgsConstructor
@NoArgsConstructor
@BookingPeriod
public class BookingRequestDto {

    @NotNull(message = "체크인 날짜를 지정해야 합니다.")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate checkInDate;

    @NotNull(message = "체크아웃 날짜를 지정해야 합니다.")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate checkOutDate;

    public Booking toEntity(long userId, long roomTypeId) {
        return Booking.createInstance(userId, roomTypeId, checkInDate, checkOutDate);
    }

    public boolean isValidPeriod() {
        if (checkInDate == null || checkOutDate == null) {
            return false;
        }

        return checkOutDate.isAfter(checkInDate);
    }
}

Validator 클래스에서는 BookingRequestDto 객체 인스턴스의 isValidPeriod() 메서드의 결과에 따라 유효성 검사 통과 유무를 결정합니다.

따라서 isValidPeriod() 메서드를 적절히 구현합니다.
저는 체크아웃, 체크인 날짜가 존재해야 하며 체크아웃 날짜는 체크인 날짜를 앞설 수 없다는 제약조건을 추가하였습니다.

마지막으로 이전에 생성한 커스텀 애노테이션 @BookingPeriod을 DTO 클래스 상단에 붙여줍니다.

결과

체크인 날짜가 체크아웃 날짜를 앞서는 경우 위와 같은 에러 메시지를 반환받는 것을 확인할 수 있습니다.

Reference

https://www.baeldung.com/spring-mvc-custom-validator

0개의 댓글