1. 개요

나중에 데이터를 처리할 때 예기치 않은 오류를 방지하기 위해 API에 대한 입력 유효성 검사를 구현하는 것이 종종 유용합니다.

안타깝게도 Spring 5에서는 어노테이션 기반 엔드포인트에서와 같이 기능적 엔드포인트에서 유효성 검사를 자동으로 실행할 방법이 없습니다. 수동으로 관리해야 합니다.

그래도 Spring에서 제공하는 몇 가지 유용한 도구를 사용하여 리소스가 유효한지 쉽고 깔끔하게 확인할 수 있습니다.

2. 스프링 유효성 검사 사용

실제 유효성 검사를 시작하기 전에 작동하는 기능 엔드포인트로 프로젝트를 구성하는 것으로 시작하겠습니다.

다음과 같은 RouterFunction 이 있다고 상상해 보십시오 .

@Bean
public RouterFunction<ServerResponse> functionalRoute(
  FunctionalHandler handler) {
    return RouterFunctions.route(
      RequestPredicates.POST("/functional-endpoint"),
      handler::handleRequest);
}

이 라우터는 다음 컨트롤러 클래스에서 제공하는 핸들러 기능을 사용합니다.

@Component
public class FunctionalHandler {

    public Mono<ServerResponse> handleRequest(ServerRequest request) {
        Mono<String> responseBody = request
          .bodyToMono(CustomRequestEntity.class)
          .map(cre -> String.format(
            "Hi, %s [%s]!", cre.getName(), cre.getCode()));
 
        return ServerResponse.ok()
          .contentType(MediaType.APPLICATION_JSON)
          .body(responseBody, String.class);
    }
}

보시다시피 이 기능적 엔드포인트에서 우리가 하는 일은  CustomRequestEntity 개체로 구성된 요청 본문에서 받은 정보의 형식을 지정하고 검색하는 것뿐입니다.

public class CustomRequestEntity {
    
    private String name;
    private String code;

    // ... Constructors, Getters and Setters ...
    
}

이것은 잘 작동하지만 이제 입력이 일부 주어진 제약 조건을 준수하는지 확인해야 한다고 가정해 보겠습니다. 예를 들어 어떤 필드도 null이 될 수 없으며 코드는 6자리 이상이어야 합니다.

우리는 이러한 어설션을 효율적으로 만들고 가능한 경우 비즈니스 논리에서 분리하는 방법을 찾아야 합니다.

2.1. 유효성 검사기 구현

이 Spring 참조 문서 에 설명된 대로 Spring의  유효성 검사기 인터페이스를 사용하여 리소스 값을 평가할 수 있습니다 .

AdChoices
광고
public class CustomRequestEntityValidator 
  implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return CustomRequestEntity.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(
          errors, "name", "field.required");
        ValidationUtils.rejectIfEmptyOrWhitespace(
          errors, "code", "field.required");
        CustomRequestEntity request = (CustomRequestEntity) target;
        if (request.getCode() != null && request.getCode().trim().length() < 6) {
            errors.rejectValue(
              "code",
              "field.min.length",
              new Object[] { Integer.valueOf(6) },
              "The code must be at least [6] characters in length.");
        }
    }
}

유효성 검사기 가 작동 하는 방식 에 대해서는 자세히 설명하지 않습니다  . 개체의 유효성을 검사할 때 모든 오류가 수집된다는 것을 아는 것으로 충분합니다  . 빈 오류 수집은 개체가 모든 제약 조건을 준수함을 의미합니다 .

이제 Validator 가 준비되었으므로 실제로 비즈니스 로직을 실행하기 전에 유효성 검사  를 명시적으로 호출해야 합니다 .

2.2. 유효성 검사 실행

처음에는 HandlerFilterFunction 을 사용하는  것이 우리 상황에 적합하다고 생각할 수 있습니다.

그러나 핸들러에서와 마찬가지로 해당 필터에서 MonoFlux 와  같은 비동기 구성 을 처리한다는 점을 명심해야 합니다 .

즉, 게시자 ( Mono 또는 Flux 개체)에 액세스할 수 있지만 궁극적으로 제공할 데이터에는 액세스할 수 없습니다.

따라서 우리가 할 수 있는 최선의 방법은 핸들러 함수에서 실제로 처리할 때 본문의 유효성을 검사하는 것입니다.

계속해서 유효성 검사 논리를 포함하여 처리기 메서드를 수정해 보겠습니다.

public Mono<ServerResponse> handleRequest(ServerRequest request) {
    Validator validator = new CustomRequestEntityValidator();
    Mono<String> responseBody = request
      .bodyToMono(CustomRequestEntity.class)
      .map(body -> {
        Errors errors = new BeanPropertyBindingResult(
          body,
          CustomRequestEntity.class.getName());
        validator.validate(body, errors);

        if (errors == null || errors.getAllErrors().isEmpty()) {
            return String.format("Hi, %s [%s]!", body.getName(), body.getCode());
        } else {
            throw new ResponseStatusException(
              HttpStatus.BAD_REQUEST,
              errors.getAllErrors().toString());
        }
    });
    return ServerResponse.ok()
      .contentType(MediaType.APPLICATION_JSON)
      .body(responseBody, String.class);
}

요컨대, 요청 본문이 우리의 제한 사항을 준수하지 않는 경우 우리 서비스는 이제 ' 잘못된 요청 ' 응답을 검색합니다.

목표를 달성했다고 말할 수 있습니까? 이제 거의 다 왔습니다. 유효성 검사를 실행하고 있지만 이 접근 방식에는 많은 단점이 있습니다.

유효성 검사를 비즈니스 논리와 혼합하고 있으며 설상가상으로 입력 유효성 검사를 수행하려는 핸들러에서 위의 코드를 반복해야 합니다.

이를 개선해보도록 하겠습니다.

3. DRY 접근 방식 작업

더 깔끔한 솔루션을 만들기 위해 요청을 처리하는 기본 절차가 포함된 추상 클래스를 선언하는 것으로 시작하겠습니다 .

입력 유효성 검사가 필요한 모든 핸들러는 이 추상 클래스를 확장하여 기본 체계를 재사용하므로 DRY(반복하지 않음) 원칙을 따릅니다.

우리는 제네릭을 사용하여 모든 바디 유형과 해당 유효성 검사기를 지원할 수 있을 만큼 유연하게 만들 것입니다.

public abstract class AbstractValidationHandler<T, U extends Validator> {

    private final Class<T> validationClass;

    private final U validator;

    protected AbstractValidationHandler(Class<T> clazz, U validator) {
        this.validationClass = clazz;
        this.validator = validator;
    }

    public final Mono<ServerResponse> handleRequest(final ServerRequest request) {
        // ...here we will validate and process the request...
    }
}

이제 표준 절차로 handleRequest 메서드를 코딩해 보겠습니다.

public Mono<ServerResponse> handleRequest(final ServerRequest request) {
    return request.bodyToMono(this.validationClass)
      .flatMap(body -> {
        Errors errors = new BeanPropertyBindingResult(
          body,
          this.validationClass.getName());
        this.validator.validate(body, errors);

        if (errors == null || errors.getAllErrors().isEmpty()) {
            return processBody(body, request);
        } else {
            return onValidationErrors(errors, body, request);
        }
    });
}

보시다시피 아직 만들지 않은 두 가지 방법을 사용하고 있습니다.

먼저 유효성 검사 오류가 있을 때 호출되는 것을 정의해 보겠습니다.

protected Mono<ServerResponse> onValidationErrors(
  Errors errors,
  T invalidBody,
  ServerRequest request) {
    throw new ResponseStatusException(
      HttpStatus.BAD_REQUEST,
      errors.getAllErrors().toString());
}

이것은 기본 구현일 뿐이며 하위 클래스에서 쉽게 재정의할 수 있습니다.

마지막으로  processBody 메서드를 정의되지 않은 상태로 설정합니다. 이 경우 진행 방법을 결정하기 위해 자식 클래스에 맡깁니다 .

abstract protected Mono<ServerResponse> processBody(
  T validBody,
  ServerRequest originalRequest);

이 수업에서 분석해야 할 몇 가지 측면이 있습니다.

우선, 제네릭을 사용함으로써 자식 구현은 기대하는 콘텐츠 유형과 이를 평가하는 데 사용할 유효성 검사기를 명시적으로 선언해야 합니다.

이것은 또한 메소드의 서명을 제한하기 때문에 구조를 견고하게 만듭니다.

런타임 시 생성자는 실제 유효성 검사기 개체와 요청 본문을 캐스팅하는 데 사용되는 클래스를 할당합니다.

여기 에서 전체 수업을 볼 수 있습니다 .

이제 이 구조를 어떻게 활용할 수 있는지 살펴보겠습니다.

3.1. 핸들러 적응하기

가장 먼저 해야 할 일은 이 추상 클래스에서 핸들러를 확장하는 것입니다.

이렇게 하면 부모의 생성자를 사용하고 processBody 메서드 에서 요청을 처리하는 방법을 정의해야 합니다  .

@Component
public class FunctionalHandler
  extends AbstractValidationHandler<CustomRequestEntity, CustomRequestEntityValidator> {

    private CustomRequestEntityValidationHandler() {
        super(CustomRequestEntity.class, new CustomRequestEntityValidator());
    }

    @Override
    protected Mono<ServerResponse> processBody(
      CustomRequestEntity validBody,
      ServerRequest originalRequest) {
        String responseBody = String.format(
          "Hi, %s [%s]!",
          validBody.getName(),
          validBody.getCode());
        return ServerResponse.ok()
          .contentType(MediaType.APPLICATION_JSON)
          .body(Mono.just(responseBody), String.class);
    }
}

알 수 있듯이 자식 처리기는 이제 리소스의 실제 유효성 검사를 방해하지 않기 때문에 이전 섹션에서 얻은 것보다 훨씬 간단합니다.

4. Bean 유효성 검사 API 어노테이션 지원

이 접근 방식을 사용하면 javax.validation 패키지 에서 제공 하는 강력한 Bean 유효성 검사의 어노테이션 을 활용할 수도 있습니다 .

예를 들어 어노테이션이 달린 필드가 있는 새 엔터티를 정의해 보겠습니다.

public class AnnotatedRequestEntity {
 
    @NotNull
    private String user;

    @NotNull
    @Size(min = 4, max = 7)
    private String password;

    // ... Constructors, Getters and Setters ...
}

이제 LocalValidatorFactoryBean에서 제공  하는 기본 Spring Validator 가 주입된 새 핸들러를 간단히 만들 수 있습니다 .

public class AnnotatedRequestEntityValidationHandler
  extends AbstractValidationHandler<AnnotatedRequestEntity, Validator> {

    private AnnotatedRequestEntityValidationHandler(@Autowired Validator validator) {
        super(AnnotatedRequestEntity.class, validator);
    }

    @Override
    protected Mono<ServerResponse> processBody(
      AnnotatedRequestEntity validBody,
      ServerRequest originalRequest) {

        // ...

    }
}

컨텍스트에 다른 유효성 검사기 빈이 있는 경우 @Primary 어노테이션 을 사용하여 명시적으로 선언해야 할 수도 있음 을 명심해야 합니다  .

@Bean
@Primary
public Validator springValidator() {
    return new LocalValidatorFactoryBean();
}

5. 결론

요약하면 이 게시물에서는 Spring 5 기능 엔드포인트에서 입력 데이터의 유효성을 검사하는 방법을 배웠습니다.

우리는 비즈니스 논리와 논리가 섞이지 않도록 하여 유효성 검사를 적절하게 처리하는 좋은 접근 방식을 만들었습니다.

물론 제안된 솔루션이 모든 시나리오에 적합하지 않을 수 있습니다. 상황을 분석하고 필요에 맞게 구조를 조정해야 합니다.

전체 작업 예제를 보려면 GitHub 저장소 에서 찾을 수 있습니다 .

Generic footer banner