JAVA/Spring Boot

[Validation] @Valid 직접 실행하는 법

주코식딩 2023. 10. 10. 09:58

통상적으로 작성해둔 Validation은 Controller딴에서 ReqeustDto로 역직렬화 할 때 Spring Boot가 자동으로 해준다.

하지만 나는 RequestDto뿐만 아니라 ResposneDto나 내부적으로 사용하는 Class에서도 사용할 수 있게끔 하고 싶었다.

 

Validator를 사용해서 생성자 호출시에 유효성 검사를 하게끔 했다.

 

Exception Advice에서 처리 하던 BindException과 형태가 달라서 문제가 생겼다.

기존의 방식과 똑같이 Exception을 만들기 위한 코드도 함께 첨부한다.

 

BindException

잘못된 변수명과 그 이유에 대한 Message를 함께 출력하는 코드다.

아래 코드는 디버깅을 통해 변수명과 메세지를 찾은 뒤 맵핑시켰다. 관련 포스팅

    @ExceptionHandler(BindException.class)
    public ResponseEntity<CommonResDto<Map<String, String>>> handleBindException(BindException e) {

        Map<String, String> data = new HashMap<>();

        for (ObjectError error : e.getBindingResult().getAllErrors()) {
            data.put(((DefaultMessageSourceResolvable) Objects.requireNonNull(error.getArguments())[0]).getDefaultMessage()
                    , error.getDefaultMessage());
        }

        log.warn(data + " | EndPoint : " + e.getStackTrace()[0].toString());
        e.printStackTrace();

        return ResponseEntity.status(ResponseCode.BINDING_FAILED.getStatus())
                .body(CommonResDto.error(ResponseCode.BINDING_FAILED.getCode(), ResponseCode.BINDING_FAILED.getDefaultMessage(), data));
    }

 

RuntimeException

BindException을 throw시키면 처리되지 않은 예외라면서 귀찮게 군다..

메소드 옆에 예외를 명시하는 방법은 해당 메소드를 사용할 때마다 예외를 명시해야하는 귀찮음이 있다.

그러므로 RuntimeException으로 try catch 한 뒤 아래처럼 분기처리했다.

    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<?> handleRuntimeException(RuntimeException e) {

        if (e.getCause() instanceof BindException bindException) {
            return this.handleBindException(bindException);
        }

        this.loggingTextError(e);
        e.printStackTrace();

        return ResponseEntity.status(ResponseCode.INTERNAL_SERVER_ERROR.getStatus()).body(CommonResDto.error(ResponseCode.INTERNAL_SERVER_ERROR.getCode(), ResponseCode.INTERNAL_SERVER_ERROR.getDefaultMessage()));
    }

 

Utills Class

이제 Utils 클래스에 Validation 메서드를 생성한다.

단순히 BindException만 생성하면 그 안에 어떤 변수에서 문제가 발생했는지 ExceptionAdvice에서 알 수 없으므로 잘못된 변수를 직접 넣어주었다.

public class Utils {

    private Utils() {
        throw new APIException(ResponseCode.INTERNAL_SERVER_LOGIC_ERROR, "Utils class 생성자 호출 금지");
    }

    private static final ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
    public static final Validator validator = factory.getValidator();


    public static <T>void validate(T object) {
        Set<ConstraintViolation<T>> violations = Utils.validator.validate(object);
        BindingResult bindingResult = new BindException(object, object.getClass().getName());
        // 유효성 검사 결과 처리
        if (!violations.isEmpty()) {
            for (ConstraintViolation<T> violation : violations) {
                String propertyPath = violation.getPropertyPath().toString();
                String message = violation.getMessage();

                // BindingResult에 오류 추가
                String[] emptyArray = {};
                DefaultMessageSourceResolvable resolvable = new DefaultMessageSourceResolvable(emptyArray,propertyPath);
                FieldError error = new FieldError(object.getClass().getName(), propertyPath, null, false, null, new DefaultMessageSourceResolvable[]{resolvable}, message);
                bindingResult.addError(error);
            }

            if (bindingResult.hasErrors()) {
                throw new RuntimeException(new BindException(bindingResult));
            }
        }
    }
}

 

ResponseDto

나는 아래 처럼 생성자에서 유효성 검사를 진행해서 Controller의 @Valid가 처리하는 것과 동일하게 동작하게끔 했다.

@Getter
@NoArgsConstructor
public class TestResDto {

    @NotNull
    private String test;

    @Builder
    public TestResDto(String test) {
        this.test = test;
        Utils.validate(this);
    }
}