Converter는 언제 작동하는 것일까?

Spring In Action(제5판)을 공부하던 중 Converter를 사용해 클라이언트에서 도메인 객체에 대한 id 값을 전달하면 id를 DB에서 도메인 객체를 조회하는 내용이 나왔다.

Converter 구현

import org.springframework.core.convert.converter.Converter;
...

@Component
@RequiredArgsConstructor
public class IngredientByIdConverter implements Converter<String, Ingredient> {

    private final IngredientRepository ingredientRepository;

    @Override
    public Ingredient convert(String id) {
        return ingredientRepository.findById(id);
    }
}

책의 예제는 타코를 만들고 주문하는 시스템이다.

위 Converter는 클라이언트에서 받은 Ingredient라는 타코에 들어가는 식자재의 id 값을 통해 DB에서 해당 식자재를 조회해 반환하는 역할을 한다. Converter를 사용하기 위해서는 Converter를 구현하면 된다.

사용

@ModelAttribute(name = "taco")
public Taco taco(){
    return new Taco();
}

@PostMapping
public String processDesign(@Valid Taco design, Errors errors, @ModelAttribute Order order){
    ...
    return "redirect:/orders/current";
}

@Data
public class Taco {
   ...
   @Size(min = 1, message = "You must choose at least 1 ingredients")
   private List<Ingredient> ingredients;
}

위와 같은 Controller가 있고 POST 방식으로 /design 요청을 보낸다고 해보자.

그러면 예를 들어 클라이언트에서 body에 { “ingredients” : [”APPLE”, “BANANA” ] 이렇게 값을 담아서 보내면 Converter가 동작해 “APPLE”, “BANANA가 ID인 Ingredient를 DB에서 조회해 반환되어 결과적으로 taco 객체의 ingredients 필드에 Ingredient 객체 배열이 할당된다.

의문

의문이 든 점은 Converter의 구현체에 대해 단순히 @Component 를 통해 bean으로 등록만 했을 뿐 Converting을 위한 추가적인 작업을 진행하지 않았음에도 기능이 작동한다는 것이었다. 분명 스프링부트 내부적으로 처리해주는 것이 있을 것이라 생각했다. 이를 이해하기 위해선 잠시 HTTP 요청이 전달되는 과정을 살펴봐야 한다.

스프링에서는 Dispatcher Servlet 객체를 통해 HTTP 요청을 각 Handler객체에 위임한다. 여기서 Handler 객체란 URL에 해당하는 Controller이다. 즉 bean으로 등록된 Controller 객체 중에 요청 URL과 일치하는 Handler를 찾아서 해당 Handler에 요청 처리를 위임하는 것이다.

// DispatcherServlet.class
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
	...
	mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
	...
}

그런데 요청을 처리하려면 HTTP 요청에 담긴 요청 정보, 즉 파라미터 등을 할당해야 한다. 따라서 요청을 처리하는 과정에서 파라미터 등을 할당하기 위한 코드가 수행된다.

 		// InvocableHandlerMethod.class
 		@Nullable
    public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
        // 이 메소드이다.
        Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);
        ...
        return this.doInvoke(args);
    }

getMethodArgumentValues()의 코드를 살펴보면 아래와 같이 파라미터를 할당하기 위해 적절한 resolver를 찾아 처리한다.

// InvocableHandlerMethod.class
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
...
	try {
	   args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
	}
...
}

스프링에 등록되어 있는 resolver들이 이렇게 많다. 그 중 현재 HTTP 요청은 클라이언트에서 POST 방식으로 <form action=”…”> 로 한 것이기 때문에 ModelAttribute로 처리되고 따라서 ServletModelAttributeMethodProcessor클래스에 해당하는 resolver 객체가 사용된다.

 

이제 파라미터에 대한 bind 작업이 시작되는데

protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
        ServletRequest servletRequest = (ServletRequest)request.getNativeRequest(ServletRequest.class);
        Assert.state(servletRequest != null, "No ServletRequest");
        ServletRequestDataBinder servletBinder = (ServletRequestDataBinder)binder;
        servletBinder.bind(servletRequest);
 }

메소드를 쭉 타고 들어가다보면 converter를 통해 파라미터의 값을 변환해주는 작업을 한다.

인자를 보면 알 수 있듯이 값의 타입을 통해 적절한 Converter 객체를 찾고 해당 객체에서 변환 작업을 수행한다.

public <T> T convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, @Nullable Object newValue, @Nullable Class<T> requiredType, @Nullable TypeDescriptor typeDescriptor) throws IllegalArgumentException {
   ...
    if (editor == null && conversionService != null && newValue != null && typeDescriptor != null) {
       TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue);
       if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {
            try {
                **return conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);**
            } catch (ConversionFailedException var14) {
                conversionAttemptEx = var14;
            }
        }
    }
	 ...       
}

 

아래 목록에 있는 Converter는 스프링 내부적으로 등록되어 있는 객체들이다.

 

위 과정을 좀 더 상세히 적으면,

  1. HTTP 요청에 담긴 식자재 배열의 타입은 String 배열이다. 이를 Taco 클래스의 ingredients 인스턴스 변수의 타입인 List<Ingredient>로 변환하는데 ArrayToCollectionConverter를 사용한다.
  2. String 배열 안에 담긴 문자열 값을 Ingredient 클래스 타입으로 변환하는 작업을 수행하는데, 이 때 앞서 만들었던 IngredientByIdConverter 가 사용된다. 즉 Converter 객체를 찾을 때 String → Ingredient 로 변환하는 Converter 객체를 찾아 convert 메소드를 수행한다.

정리

정리해보자면,

  1. 스프링 부트가 시작할 때 IngredientByIdConverter 가 bean으로 등록된다.
  2. 스프링 MVC에서 가장 핵심적인 DispatcherServlet을 통한 HTTP 요청 처리 시 비즈니스 로직 수행을 위해 HTTP 요청 파라미터 등에 대한 resolve 작업이 수행된다.
  3. Controller에서 정의한 파라미터 타입 또는 객체의 인스턴스 타입으로 맞춰주기 위해 Converting 작업이 수행되는데 String 타입을 Ingredient 클래스 타입으로 변환하는Converter를 찾는 과정에서 1번에서 등록된 객체를 찾고, 해당 객체에서 convert를 수행한다.

'Spring' 카테고리의 다른 글

스프링 비동기처리  (1) 2024.02.01
@ManyToMany에 대한 정리  (1) 2024.01.19
[토비의 스프링 3.1] 오브젝트와 의존관계  (1) 2023.12.05
Spring EhCache 간단한 적용법  (0) 2023.12.01
Spring Security의 동작방식  (0) 2023.11.30