[Spring] Converter와 ArgumentResolver를 지혜롭게 활용하기

임현규·2023년 4월 22일
0

개인 공부

목록 보기
3/11

Converter

Converter 분석

Converter 용도

바인딩한 데이터가 일치하지 않는 경우 실행에 필요한 컨트롤러 메서드의 인자 타입을 맞추기 위해 바인딩한 데이터 타입을 변경하는 용도로 사용한다.

내가 만든 Converter 사용하기

우선 Converter<S, T>의 구현체를 구현한다.

public class StringToFoodConverter implements Converter<String, Food> {
	@Override
	public Food convert(String source) {
		return Food.valueOf(source.trim().toUpperCase());
	}
}

위와 같이 정의한 후에는 등록작업을 수행한다.

@Configuration
public class WebConfig implements WebMvcConfigurer {

	@Override
	public void addFormatters(FormatterRegistry registry) {
		registry.addConverter(new StringToFoodConverter());
	}
}

@Configuration은 선언 후 WebMvcConfigurer를 선언 후 addFormatters를 override하여 registry에 converter를 등록한다

ConverterFactory

좀 더 여러 타입에 적용하고 싶은 경우 ConverterFactory를 사용할 수 있다.

예를 들면 Enum을 예로 들 수 있는데, 기존의 Enum은 valueOf(source)지만 우리는 source의 영소문자, 대문자, 좌우 빈 공백에 상관없이 적용하고 싶다. 그러나 매번 Enum타입에 적용하기에는 굉장히 피곤한 작업이므로 이런 경우 ConverterFactory를 활용할 수 있다.

@SuppressWarnings({"rawtypes"})
public class StringToEnumConverterFactory implements ConverterFactory<String, Enum> {

	@SuppressWarnings("unchecked")
	@Override
	public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
		return new StringToEnumConverter(targetType);
	}

	private static class StringToEnumConverter<T extends Enum<T>> implements Converter<String ,T> {

		private final Class<T> enumType;

		public StringToEnumConverter(Class<T> enumType) {
			this.enumType = enumType;
		}

		@Override
		public T convert(String source) {
			return Enum.valueOf(this.enumType, source.trim().toUpperCase());
		}
	}

해당 ConverterFactory를 String -> Enum 으로 인자를 변경하는 작업을 수행한다. StringToEnumConverter를 내부 정적 클래스를 활용해 정의하는데 enumType 인자를 받도록 했다. 이는 enum의 모든 타입에 대해서 Converter를 생성하기 위함이다.

@Configuration
public class WebConfig implements WebMvcConfigurer {

	@Override
	public void addFormatters(FormatterRegistry registry) {
		registry.addConverterFactory(new StringToEnumConverterFactory());
	}
}

그 이후 간단하게 addFormatters에 addConverterFactory를 이용해 converterFactory를 등록해주면 된다.

ArgumentResolver

ArgumentResolver 분석

ArgumentResolver 용도

ArgumentResolver는 HandlerMethod의 컨트롤러 메서드에 필요한 인자를 바인딩할 때 사용한다.

@RequestParam을 예로 들면 RequestParamMethodArgumentResolver를 이용해서 HttpServletMethod의 쿼리 스트링 정보를 String type으로 가져와서 method 파라미터에 바인딩한다.

기존에 제공하는 ArgumentResolver 외에 필요한 데이터 바인딩을 구현해야 한다면 직접 구현해서 등록하면 된다.

내가 만든 ArgumentResolver 사용하기

@Header라는 어노테이션을 만들어서 header key에 관련된 값을 String인자로 받아서 Controller 메서드에 활용하고 싶다고 가정하고 코드를 작성해보겠다

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Header {

	String name();
}

어노테이션을 정의하자

public class HeaderHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {

	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		return parameter.getParameterAnnotation(Header.class) != null;
	}

	@Override
	public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
		NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
		HttpServletRequest httpServletRequest = (HttpServletRequest)webRequest.getNativeRequest();
		Header annotation = parameter.getParameterAnnotation(Header.class);
		assert annotation != null;
		String headerName = annotation.name();
		return httpServletRequest.getHeader(headerName);
	}
}

supportsParamter에는 어노테이션을 인식하기 위한 로직을 짜주고 resolveArgument에는 HttpServletRequest에서 header의 value값을 가져와 String 타입으로 받아온다.

@Configuration
public class WebConfig implements WebMvcConfigurer {

	@Override
	public void addFormatters(FormatterRegistry registry) {
		registry.addConverterFactory(new StringToEnumConverterFactory());
	}

	@Override
	public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
		resolvers.add(new HeaderHandlerMethodArgumentResolver());
	}
}

Configuration에 등록해준다.

단순히 ArgumentResolver를 구현하는 것의 문제점

ArgumentResolver를 구현해서 등록하면 ArgumentResolver는 잘 동작한다. 그러나 Converter는 제대로 동작하지 않는다.

그러나 만약 enum 타입으로 인자를 받고 @Header 어노테이션을 사용하면 인자가 일치하지 않는다는 에러를 보게 된다.

@RestController
@RequestMapping("/api")
public class HelloController {

	@GetMapping("/hello")
	public String hello(@Header(name = "food") Food food) {
		return food.name();
	}
}

내부적으로 ArgumentResolver가 리턴한 타입이 맞지 않는 경우 Converter를 호출하는 로직은 AbstractNamedValueMethodArgumentResolver의 resolveArgument에 구현되어 있다. 그래서 Converter가 적용가능한 @PathVariable이나 @RequestParam 모두 AbstractNamedValueMethodArgumentResolver를 상속받아 설계되었다. ArgumentResolver가 해당 타입을 상속하지 않은 경우 Converter를 활용할 수 없다.

💡 Converter에 호환되는 안전한 ArgumentResolver 설계하기

안전하게 ArgumentResolver를 사용하려면 Spring에서 잘 구현한 템플릿인 AbstractNamedValueMethodArgumentResolver를 상속해서 구현하면 된다.

public class HeaderHandlerMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {

	@Override
	protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
		Header annotation = parameter.getParameterAnnotation(Header.class);
		return annotation != null ? new HeaderNamedValueInfo(annotation) : new HeaderNamedValueInfo();
	}

	@Override
	protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) {
		HttpServletRequest httpServletRequest = (HttpServletRequest)request.getNativeRequest();
		Header annotation = parameter.getParameterAnnotation(Header.class);
		assert annotation != null;
		String headerName = annotation.name();
		return httpServletRequest.getHeader(headerName);
	}

	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		return parameter.getParameterAnnotation(Header.class) != null;
	}

	private static class HeaderNamedValueInfo extends NamedValueInfo {

		public HeaderNamedValueInfo() {
			super("", false, ValueConstants.DEFAULT_NONE);
		}

		public HeaderNamedValueInfo(Header annotation) {
			super(annotation.name(), annotation.required(), ValueConstants.DEFAULT_NONE);
		}
	}
}

첫번 째는 createNamedValueInfo이다. 이는 AbstractNamedValueInfo에서 사용하며 annotation 속성에서 name , required , defaultvalue 속성의 값을 추출해 저장한다. 이는 resolveArgument에서 활용한다. 이를 활용해 내가 정의한 어노테이션의 속성 입력에 따라 Spring에서 구현한 로직의 도움을 받을 수 있다.

두 번쨰 resolveName은 핵심 로직을 구현하는 부분이다.

세 번째는 supportsParameter로 해당 ArgumentResolver가 지원하는 MethodParameter인가 확인하는 메서드이다. 해당 메서드의 구현은 @Header라는 이름이 붙은 어노테이션 파라미터에 적용하고 싶기 때문에 내가 정의한 어노테이션이 parameter에 있는지 확인한다.

profile
엘 프사이 콩그루

0개의 댓글