DevBoi

[Spring] 스프링 밸리데이션 동작과정 본문

Develop/[Spring]

[Spring] 스프링 밸리데이션 동작과정

HiSmith 2023. 7. 31. 00:22
반응형

머리속에 들어있는 내용들을 정리한다.

 

1. 밸리데이션 관련 의존성 주입.

implementation 'org.springframework.boot:spring-boot-starter-validation'

 

2. 간단한 테스트

@RestController
@RequiredArgsConstructor
public class MemberController {

  private final MemberMapper memberMapper;
  @RequestMapping("/member")
  public String mapperTest(@RequestBody @Valid MemberDto memberDto){
    return "success!";
  }
}
@Getter
@Setter
@AllArgsConstructor
public class MemberDto {

  @NotBlank
  private String name;
  @NotBlank
  private String id;
  @NotBlank
  private String address;
}

@Valid를 하면, Controller이전에 AugumentResolver에서 체크가 가능하다.

구현체로는 여러가지가있는데 우선 동작 방식은 아래와 같다.

우선 DispatcherServlet이 받는다. 그리고, Request에 대한 검증을 doDispatch에서 진행한다.

HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

DispatcherServlet의 해당 로직에서, Handler들을 가져온다.

이때 mappedHandler의 값은 컨트롤러의 메소드이다.

즉, 해당 컨트롤러의 메소드를 호출하기 위한 관련 Adapter들을 가져오는 것이다.

해당 소스를 보면, HandlerAdapter들을 반복 처리한다.

이때 this.handlerAdatpers는 기본적으로 RequestMappingHandler,HandlerFunctionAdapter,HttpRequestHandlerAdapter,SImpleControllerHandlerAdatper이다.

이중, 지원되는 어댑터를 리턴한다.

 

지원되는 Adapter는 RequestMappingHandlerAdapter이고, 이에 포함된 ArgumentResolvers는 

아래와 같이 27개의 리스트를 가지게 된다 (절대적은 아니다)

@RequestBody를 사용하는 경우, 해당 RequestResponseBodyMethodBodyMethodProcessor라는 녀석의

resolveArgument라는 메소드를 처리하게 되고, 해당 소스는 아래와 같다.

@Override
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

		parameter = parameter.nestedIfOptional();
		Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
		String name = Conventions.getVariableNameForParameter(parameter);

		if (binderFactory != null) {
			WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
			if (arg != null) {
				validateIfApplicable(binder, parameter);
				if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
					throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
				}
			}
			if (mavContainer != null) {
				mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
			}
		}

		return adaptArgumentIfNecessary(arg, parameter);
	}

위 부분에서, validateIfApplicable이라는 메소드는 아래와 같다.

protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
		Annotation[] annotations = parameter.getParameterAnnotations();
		for (Annotation ann : annotations) {
			Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann);
			if (validationHints != null) {
				binder.validate(validationHints);
				break;
			}
		}
	}

위에서 DataBinder의 validate는 아래와 같다.

public void validate(Object... validationHints) {
		Object target = getTarget();
		Assert.state(target != null, "No target to validate");
		BindingResult bindingResult = getBindingResult();
		// Call each validator with the same binding result
		for (Validator validator : getValidators()) {
			if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) {
				((SmartValidator) validator).validate(target, bindingResult, validationHints);
			}
			else if (validator != null) {
				validator.validate(target, bindingResult);
			}
		}
	}

 

그리고, 해당 Validator목록을 가져올때 그중 하나가 LocalValidatorFactoryBean의 Validator이다.

따라서, DispatcherServlet -> Controller로 갈때, 해당 ArgumentResolver에서 미리 유효성 검증 작업처리가 되어주는 것이다.

 

쉽게 정리하면,

해당 Validator의 Validate 로직을 처리하여, ArgumentResolver에서 처리를 하여, 컨트롤러 앞단에서 처리가 되어 벨리데이션 처리가 되는 것이다.

 

 

 

Validator 동작과정

 

Gradle에서 implementation하면, autoconfiguration패키지내 validation 폴더생성

ValidationAutoConfiguration.class가 빈으로 등록됨

@AutoConfiguration
@ConditionalOnClass(ExecutableValidator.class)
@ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider")
@Import(PrimaryDefaultValidatorPostProcessor.class)
public class ValidationAutoConfiguration {

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	@ConditionalOnMissingBean(Validator.class)
	public static LocalValidatorFactoryBean defaultValidator(ApplicationContext applicationContext) {
		LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
		MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory(applicationContext);
		factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
		return factoryBean;
	}

	@Bean
	@ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
	public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment,
			@Lazy Validator validator, ObjectProvider<MethodValidationExcludeFilter> excludeFilters) {
		FilteredMethodValidationPostProcessor processor = new FilteredMethodValidationPostProcessor(
				excludeFilters.orderedStream());
		boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);
		processor.setProxyTargetClass(proxyTargetClass);
		processor.setValidator(validator);
		return processor;
	}

}

이때 Validator.class가 없으면, 컨디셔널 하게 LocalValidatorFactoryBean 빈이 등록됨

해당 과정때문에 실제로, 별도 선언하지 않아도, Validator 빈을 주입 요청 하면, 아래와 같은 형태로 빈을 사용할수 있게 됨

해당 validate에 대한 값을 보면 아래와 같음

별도로 지정이 가능하고 Validate 결과를 가지고있는 객체를 반환 받을 수 있음

@Vaild를 사용하면, ArgumentResolver를 통해 Controller 앞단에서 검증하여 Response를 내릴수 있지만 (Request용도)

내부 서비스 로직에 의해 생성되어 DB나 외부로 호출되는 객체에 대해서는 아래와 같이 검증이 가능함

또한 Validator를 별도로 선언하여, 커스텀하게 주입받아 사용도 가능하다.

 

전체적인 프로세스는 결국 Validator에서 바인딩 된 결과를 validate메소드에서 처리하는 것과 동일하다.

다만, 이를 언제처리하냐의 문제인 것이다.

 

Validation이 무조건 ArgumentResolve에서 처리되는 것이 절대적이지 않다.

어디서 의존성을 추가로 주입받아서, 공통소스에 녹여내느냐에 따라서 유효성 검증은 어떤 레이어에서 할수있다.

 

다만 규약적으로 편하게 Presentation 레이어인, 컨트롤러에서 처리하는 것이 깔끔하고, 로직적으로 덜 구현해도 되기 때문에 

많이 쓰는 것이다.

 

반응형