커스텀 어노테이션을 통한 검증(feat. @Constraint)

Minjae An·2023년 12월 13일
0

Spring Web

목록 보기
2/9

🫦 커스텀 어노테이션

사용자 입력의 유효성을 보다 세부적으로 검증해야할 때, 커스텀 어노테이션을 이용하여 예외 검증 로직을 구성할 수 있다.

이 때, hibernate에서 제공하는 @Constraint 를 사용할 수 있다.

🙆‍♂️ 커스텀 어노테이션의 이점

어노테이션의 가장 큰 이점은 간결함이다.

로직 흐름에 대한 컨텍스트가 응축돼 있어 적재적소에 사용된다면 불필요한 반복 코드가 줄어든다. 따라서 개발자가 비즈니스 로직에 보다 집중할 수 있게 돕는다.

또한 @Constraint 를 활용한 커스텀 어노테이션들은 ConstraintViolationException 을 발생시키기 때문에 해당 예외에 대한 핸들러를 통해 예외에 관한 일관성 있는 응답을 구성할 수도 있다.

🤷‍♂️ 커스텀 어노테이션과 Validator 구현

커스텀 어노테이션을 사용하기 위해 다음 작업이 필요하다.

  • @Constraint 활용 커스텀 어노테이션 생성
  • ConstraintValidator 를 이용한 Validator 생성
  • 검증하고자 하는 DTO에 어노테이션 부착

먼저 아래와 같이 커스텀 어노테이션을 생성한다. @ConstraintvalidatedBy 에는 검증을 수행할 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) {
	// ...
	}
}

🙇 주의할 점

어노테이션의 의도는 숨어있기 때문에 내부적으로 어떤 동작을 하게 되는지 명확하지 않다면 로직 플로우를 이해하기 어렵다.

하물며 ‘커스텀’ 어노테이션은 그 부담을 가중시킬 수 있다. 어노테이션 추가가 당장의 작업 속도를 끌어올릴 순 있지만, 장기적 관점에서 시의적절한 것인지를 공감할 수 있어야 한다.

코드가 간결해진다는 장점 하나만 보고 커스텀 어노테이션을 남용하지 않아야 한다.
반복적으로 사용하지 않고, 특정 요청 외에는 사용할 일이 없어 보이는 유효성 검사라면 단순
메서드로 만들어 처리하는 것이 더 좋을 것이다.

🧑‍🎨 결론

커스텀 어노테이션을 잘 이용하면 불필요한 반복코드가 줄어들고, 비즈니스 로직에 더 집중할 수 있다는 장점이 있다.

다만, 의도와 목적을 명확히 하여 공감대를 이룬 후 추가하는 것이 좋다.

참고

profile
내가 쓴 코드가 남의 고통이 되지 않도록 하자

0개의 댓글