사용자 입력의 유효성을 보다 세부적으로 검증해야할 때, 커스텀 어노테이션을 이용하여 예외 검증 로직을 구성할 수 있다.
이 때, hibernate에서 제공하는 @Constraint
를 사용할 수 있다.
어노테이션의 가장 큰 이점은 간결함이다.
로직 흐름에 대한 컨텍스트가 응축돼 있어 적재적소에 사용된다면 불필요한 반복 코드가 줄어든다. 따라서 개발자가 비즈니스 로직에 보다 집중할 수 있게 돕는다.
또한 @Constraint
를 활용한 커스텀 어노테이션들은 ConstraintViolationException
을 발생시키기 때문에 해당 예외에 대한 핸들러를 통해 예외에 관한 일관성 있는 응답을 구성할 수도 있다.
Validator
구현커스텀 어노테이션을 사용하기 위해 다음 작업이 필요하다.
@Constraint
활용 커스텀 어노테이션 생성ConstraintValidator
를 이용한 Validator 생성먼저 아래와 같이 커스텀 어노테이션을 생성한다. @Constraint
의 validatedBy
에는 검증을 수행할 Validator 클래스를 지정한다.
@Constraint(validatedBy = NotEqualValidator.class)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface NotEqual {
String message() default "상행 역과 하행 역은 같을 수 없습니다.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String upStationId();
String downStationId();
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@interface List {
NotEqual[] value();
}
}
Validator는 ConstraintValidator
인터페이스를 구현하는데 initialize
메서드와 isValid
메서드를 오버라이딩한다.
package wooteco.subway.line;
import org.springframework.beans.BeanWrapperImpl;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class NotEqualValidator implements ConstraintValidator<NotEqual, Object> {
private String upStationId;
private String downStationId;
@Override
public void initialize(NotEqual constraintAnnotation) {
this.upStationId = constraintAnnotation.upStationId();
this.downStationId = constraintAnnotation.downStationId();
}
@Override
public boolean isValid(Object object, ConstraintValidatorContext context) {
Object upStationValue = new BeanWrapperImpl(object).getPropertyValue(upStationId);
Object downStationValue = new BeanWrapperImpl(object).getPropertyValue(downStationId);
return !upStationValue.equals(downStationValue);
}
}
initialize
어노테이션을 부착한 객체로부터 필드명을 가져와서 초기화한다.isValid
오버라이딩하여 예외 상황을 검증할 로직을 구성한다. 어노테이션이 부착된 객체를 인자롤 한다. initialize
에서 초기화했던 필드명을 통해 어노테이션이 부착된 객체로부터 필드 값을이제 요청 DTO에 어노테이션을 적용한다.
@NotEqual(upStationId = "upStationId", downStationId = "downStationId")
public class SectionRequest {
@NotNull
private Long upStationId;
@NotNull
private Long downStationId;
// ...
}
혹은 요청 DTO가 쓰이는 Controller의 파라미터에 부착할 수도 있다.
@RestController
@RequestMapping("/subway")
public class SubwayController {
// ...
@PostMapping("/section")
public ResponseEntity<SectionResponse> addSection(@RequestBody
@NotEqual(upStationId = "upStationId", downStationId = "downStationId")
SectionRequest request) {
// ...
}
}
어노테이션의 의도는 숨어있기 때문에 내부적으로 어떤 동작을 하게 되는지 명확하지 않다면 로직 플로우를 이해하기 어렵다.
하물며 ‘커스텀’ 어노테이션은 그 부담을 가중시킬 수 있다. 어노테이션 추가가 당장의 작업 속도를 끌어올릴 순 있지만, 장기적 관점에서 시의적절한 것인지를 공감할 수 있어야 한다.
코드가 간결해진다는 장점 하나만 보고 커스텀 어노테이션을 남용하지 않아야 한다.
반복적으로 사용하지 않고, 특정 요청 외에는 사용할 일이 없어 보이는 유효성 검사라면 단순
메서드로 만들어 처리하는 것이 더 좋을 것이다.
커스텀 어노테이션을 잘 이용하면 불필요한 반복코드가 줄어들고, 비즈니스 로직에 더 집중할 수 있다는 장점이 있다.
다만, 의도와 목적을 명확히 하여 공감대를 이룬 후 추가하는 것이 좋다.