스프링 MVC-스프링 타입 컨버터
스프링의 형 변환
스프링에서는 기본적으로 여러 타입간의 형 변환을 위한 컨버터들을 제공한다. HTTP 쿼리 파라미터들은 모두 String으로 전달되지만, 우리가 @RequestParam, @PathVariable 등의 어노테이션을 사용할 때 Integer 등의 타입으로 받을 수 있는 것도 스프링이 이러한 컨버터들을 사용하여 타입을 변환해주기 때문이다.
하지만 기본형과 객체, 객체와 문자열 등 스프링에서 기본적으로 제공하지 않는 형 변환이 필요하다면 일일이 변환시키는 것보다 스프링 타입 컨버터를 사용하는 것이 효율적이다. 스프링 타입 컨버터에는 형 변환에 사용되는 Converter와 문자열의 파싱에 특화된 Formatter가 있다.
스프링 타입 컨버터
Converter는 Converter 인터페이스를 구현하여 사용한다.
public interface Converter<S, T> {
T convert(S source);
}
예제를 위한 적당한 객체, IpPort가 있다고 가정해보자.
public class IpPort {
private String ip;
private int port;
public IpPort(String ip, int port) {
this.ip = ip;
this.port = port;
}
}
이제 Converter를 구현하여 문자열을 IpPort로 변환하는 컨버터를 만들어보자.
@Slf4j
public class StringToIpPortConverter implements Converter<String, IpPort> {
@Override
public IpPort convert(String source) {
log.info("convert source={}",source);
//127.0.0.1:8080
String[] split = source.split(":");
String ip = split[0];
int port = Integer.parseInt(split[1]);
return new IpPort(ip,port);
}
}
역의 경우에도 비슷하게 구현할 수 있다. Converter에는 이외에도 ConverterFactory, GenericConverter 등 용도에 따라 다양한 방식의 타입 컨버터를 제공하고, 뒤로 갈수록 더 구체적이고 사용하기 어렵다.
이렇게 만든 컨버터를 직접 호출하여 사용하는 것은 직접 형 변환하는 것과 큰 차이가 없다. 스프링은 컨버터를 등록하여 편리하게 사용할 수 있는 객체를 제공하는데 그것이 ConversionService이다.
ConversionService는 컨버트가 가능한지 확인하는 기능과 컨버팅 기능이 담긴 인터페이스로, 주로 컨버터 등록 기능까지 담긴 구현체인 DefaultConversionService를 사용한다.
사용 방식은 아래 코드와 같이 사용한다.
public class ConversionServiceTest {
@Test
void conversionService() {
DefaultConversionService cs=new DefaultConversionService();
cs.addConverter(new StringToIntegerConverter());
cs.addConverter(new IntegerToStringConverter());
cs.addConverter(new IpPortToStringConverter());
cs.addConverter(new StringToIpPortConverter());
assertThat(cs.convert("10", Integer.class)).isEqualTo(10);
assertThat(cs.convert(10, String.class)).isEqualTo("10");
assertThat(cs.convert("127.0.0.1:8080", IpPort.class)).isEqualTo(new IpPort("127.0.0.1",8080));
assertThat(cs.convert(new IpPort("127.0.0.1",8080), String.class)).isEqualTo("127.0.0.1:8080");
}
}
ConversionService에 컨버터들을 등록한 후 컨버트하는 테스트 코드이다. convert() 메서드를 통해 컨버트하는데 매개변수로 변환을 요하는 값과 변환 타입을 받는다.
메서드 내부에서 두 매개변수의 타입을 받아 변환이 가능한 컨버터가 있는지 확인하고 컨버팅해준다.
원래는 ConversionService에 컨버터들을 등록하는 부분은 다른 스크립트에서 실행하고 ConversionService만 받아서 사용한다.
DefaultConversionService는 컨버터를 등록하는 ConverterRegistry, 컨버팅을 실행하는 ConversionService 두 인터페이스를 구현하였다. 이를 통해 컨버터를 사용하는 클라이언트는 ConversionService에 의존하고, 컨버터를 등록하는 클라이언트는 ConverterRegistry에만 의존하는 ‘인터페이스 분리 원칙’을 준수하였다.
스프링의 컨버터 사용
스프링에서는 @RequestParam 등 여러 곳에서 컨버터를 사용하는 것을 알고 있을 것이다.
우리는 WebMvcConfigurer 인터페이스의 addFormatters() 메서드를 오버라이드 함으로써 스프링 내부에서 사용하는 컨버터 목록에 우리가 만든 컨버터를 추가할 수 있다.
위와 같은 방식으로 컨버터를 등록하면 따로 ConversionService를 가져오지 않아도 스프링이 자동으로 컨버팅을 실행해준다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToIntegerConverter());
registry.addConverter(new IntegerToStringConverter());
registry.addConverter(new StringToIpPortConverter());
registry.addConverter(new IpPortToStringConverter());
}
}
위와 같이 스프링에 컨버터들을 등록해주고,
@GetMapping("/ip-port")
public String ipPort(@RequestParam IpPort ipPort) {
System.out.println("ipPort ip= " + ipPort.getIp());
System.out.println("ipPort port= " + ipPort.getPort());
return "ok";
}
위와 같이 사용하면 분명 @RequestParam으로 받은 쿼리 파라미터는 String임에도 객체인 IpPort로 스프링이 성공적으로 컨버팅해주는 것을 확인할 수 있다.
Formatter
Formatter는 문자열을 파싱하는데 특화된 컨버터이다. 웹의 특성 상 숫자가 ‘1,000’과 같이 표시되거나, 날짜가 ‘2021-05-02 22:01:57’과 같이 표시되는 경우가 많다.
이렇게 특정한 형식을 가진 문자열을 우리는 Formatter를 통해 원하는 타입으로 쉽게 변환할 수 있다.
Formatter는 Formatter<> 인터페이스를 구현하여 사용한다.
@Slf4j
public class MyNumberFormatter implements Formatter<Number> {
@Override
public Number parse(String text, Locale locale) throws ParseException {
log.info("text={}, locale={}",text,locale);
NumberFormat instance = NumberFormat.getInstance(locale);
Number num = instance.parse(text);
return num;
}
@Override
public String print(Number object, Locale locale) {
log.info("object={}, locale={}",object,locale);
NumberFormat instance = NumberFormat.getInstance();
return instance.format(object);
}
}
parse()로 문자열을 객체로, print()로 객체를 문자열로 변환한다.
이렇게 만든 Formatter는 Formatter를 지원하는 ConversionService인 FormattingConversionService에 등록하여 사용할 수도 있고, Converter를 스프링에 등록할 때와 같이 WebMvcConfigurer 인터페이스의 addFormatters() 메서드를 오버라이드하여 등록할 수도 있다.
스프링이 제공하는 Formatter
Formatter는 기본적으로 위와 같이 작동하지만, 스프링은 여타 기능과 같이 어노테이션을 통한 유연하고 강력한 Formatter를 제공한다.
숫자 관련 포맷을 처리하는 @NumberFormat과 날짜 관련 포맷을 처리하는 @DateTimeFormat가 있는데, 사용 방식은 아래와 같다.
@Data
static class Form {
@NumberFormat(pattern = "###,###")
Integer number;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
LocalDateTime localDateTime;
}
위 객체를 출력해보면 지정된 포맷으로 출력되는 것을 확인할 수 있다.
※주의 : 메세지 컨버터에는 컨버팅이 적용되지 않는다. @ResponseBody, ResponseEnitity 등을 사용할 때 내부에서 사용되는 메세지 컨버터는 Jackson 같은 라이브러리를 사용하기 때문에 스프링의 컨버터와는 무관하다. 따라서 해당 기능을 사용하며 포맷을 지정하고 싶다면 해당 라이브러리의 기능을 사용해야 한다.
정리
스프링 내부의 형 변환에 사용되는 컨버터, 포매터에 대해 배웠다. 내부에서 형 변환이 어떤 식으로 이루어지는지, 그리고 어떻게 확장하여 사용할 수 있는지 배울 수 있었다.
다음 포스팅에서는 스프링의 파일 업로드, 다운로드에 대해 알아보도록 하겠다.