모르지 않다는 것은 아는것과 다르다.

Spring

스프링 타입 컨버터

채마스 2022. 2. 26. 02:41

스프링과 타입 변환

  • 타입을 변환해야 하는 경우는 상당히 많다.
    • 스프링 MVC 요청 파라미터 -> ex> @RequestParam , @ModelAttribute , @PathVariable
    • @Value 등으로 YML 정보 읽기
    • XML에 넣은 스프링 빈 정보를 변환
    • 뷰를 렌더링 할 때
  • 이런게 가능한 것은 스프링이 중간에 타입 변환기를 사용해서 타입을 String Integer 로 변환해주었기 때문에 개발자는 편리하게 해당 타입을 바로 받을 수 있는 것이다.
  • 만약 개발자가 새로운 타입을 만들어서 변환하고 싶으면 아래와 같이 컨버터 인터페이스를 사용하면 된다.
    public interface Converter<S, T> {
      T convert(S source);
    }
  • 스프링은 확장 가능한 컨버터 인터페이스를 제공한다.
  • 개발자는 스프링에 추가적인 타입 변환이 필요하면 이 컨버터 인터페이스를 구현해서 등록하면 된다.
    • 이 컨버터 인터페이스는 모든 타입에 적용할 수 있다.
    • 필요하면 X Y 타입으로 변환하는 컨버터 인터페이스를 만들고, 또 Y X 타입으로 변환하는 컨버터 인터페이스를 만들어서 등록하면 된다.





컨버터 인터페이스 사용하기

  • 컨버터 인터페이스를 사용해서 타입 컨퍼터를 구현하면 아래와 같다.
    @Slf4j
    public class StringToIntegerConverter implements Converter<String, Integer> {
        @Override
        public Integer convert(String source) {
            log.info("convert source={}", source);
            return Integer.valueOf(source);
        } 
    }
  • 사용자 정의 타입으로도 구현할 수 있다.
    @Getter
    @EqualsAndHashCode
    public class IpPort {
      private String ip;
      private int port;

      public IpPort(String ip, int port) {
          this.ip = ip;
          this.port = port;
      }
    }
    @Slf4j
    public class StringToIpPortConverter implements Converter<String, IpPort> {
        @Override
        public IpPort convert(String source) {
            log.info("convert source={}", source);
            String[] split = source.split(":");
            String ip = split[0];
            int port = Integer.parseInt(split[1]);

            return new IpPort(ip, port);
        }
    }
  • 위와 같이 문자로 들어온 정보를 원하는데로 커스터 마이징해서 객체로 반환할 수 있다.
  • 하지만 여전히 사용자가 번거롭게 구현해 줘야한다.
  • 스프링은 용도에 따라 다양한 방식의 타입 컨버터를 제공한다.
    • Converter , ConverterFactory , GenericConverter 의 구현체를 찾아보면 수 많은 컨버터를 확인할 수 있다.
      • Converter 기본 타입 컨버터
      • ConverterFactory 전체 클래스 계층 구조가 필요할 때
      • GenericConverter 정교한 구현, 대상 필드의 애노테이션 정보 사용 가능
      • ConditionalGenericConverter 특정 조건이 참인 경우에만 실행
  • 하지만 여전히 구현체를 찾아야한다는 번거로움이 존재한다.
  • 컨버전 서비스을 사용하면 이러한 고민을 해결할 수 있다.
    @Slf4j
    public class StringToIpPortConverter implements Converter<String, IpPort> {
        @Override
        public IpPort convert(String source) {
            return objectMapper.readValue(source, IpPort.class);
        }
    }
  • json 형식으로 데이터를 받아서, 위와 같이 objectMapper 를 사용해서 IpPort 로 바인행 해주는 방식도 실무에서는 많이 사용한다.

 

컨버전 서비스

  • 타입 컨버터를 하나하나 직접 찾아서 타입 변환에 사용하는 것은 매우 불편하다.
  • 스프링은 개별 컨버터를 모아두고 그것들을 묶어서 편리하게 사용할 수 있는 기능을 제공한다.
  • ConversionService 인터페이스는 아래와 같다.
    public interface ConversionService {
        boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType);
        boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType);

        <T> T convert(@Nullable Object source, Class<T> targetType);
        Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
    }
  • 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());
        }
    }
  • 스프링은 내부에서 ConversionService 를 제공한다.
  • WebMvcConfigurer 가 제공하는 addFormatters() 를 사용해서 추가하고 싶은 컨버터를 등록하면 된다.
  • 스프링은 내부에서 사용하는 ConversionService 에 컨버터를 추가해준다.
    @GetMapping("/hello-v2")
    public String helloV2(@RequestParam Integer data) {
      System.out.println("data = " + data);
      return "ok";
    }
  • 만약 helloV2에 요청을 문자로 보낸다면 registry.addConverter(new StringToIntegerConverter()) 에 등록된 StringToIntegerConverter가 호출 된다.
  • 하지만 StringToIntegerConverter 를 등록하기 전에도 이 코드는 잘 수행되었다.
    • 스프링이 내부에서 수 많은 기본 컨버터들을 제공하기 때문이다.
    • 컨버터를 추가하면 추가한 컨버터가 기본 컨버터 보다 높은 우선순위를 가진다.
    @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";
    }
  • 만약 ipPort에 요청으로 문자 "127.0.0.1:8080" 를 보낸다면 StringToIpPortConverter가 호출 되어 객체로 변환 된다.
  • 자세한 동작과정은 아래와 같다.
    @RequestParam 은 @RequestParam 을 처리하는 ArgumentResolver 인 RequestParamMethodArgumentResolver 에서 ConversionService 를 사용해서 타입을 변환한다.





포맷터 - Formatter

  • Converter 는 입력과 출력 타입에 제한이 없는, 범용 타입 변환 기능을 제공한다.
  • 하지만 개발자 입장에서는 문자를 다른 타입으로 변환하거나, 다른 타입을 문자로 변환하는 상황이 대부분이다.
  • 예를들어 1000 문자를 "1,000" 이렇게 1000 단위에 쉼표를 넣어서 출력하거나, 또는 "1,000" 라는 문자를 1000 이라는 숫자로 변경해야 한다.
  • 또한 추가로 날짜 숫자의 표현 방법은 Locale 현지화 정보가 사용될 수 있다.
  • 이렇게 객체를 특정한 포멧에 맞추어 문자로 출력하거나 또는 그 반대의 역할을 하는 것에 특화된 기능이 바로 포맷터( Formatter )이다.
  • Converter vs Formatter
    • Converter 는 범용(객체 객체)
    • Formatter 는 문자에 특화(객체 문자, 문자 객체) + 현지화(Locale) -> Converter 의 특별한 버전
  • Formatter 인터페이스는 아래와 같다.
    public interface Printer<T> {
        String print(T object, Locale locale);
    }
    public interface Parser<T> {
        T parse(String text, Locale locale) throws ParseException;
    }
    public interface Formatter<T> extends Printer<T>, Parser<T> {
    }
  • String print(T object, Locale locale) : 객체를 문자로 변경한다.
  • T parse(String text, Locale locale) : 문자를 객체로 변경한다.
  • 숫자 1000 을 문자 "1,000" 로 만드는 것을 포맷터를 이용해서 구현하면 아래와 같다.
    @Slf4j
    public class MyNumberFormatter implements Formatter<Number> {
        @Override
        public Number parse(String text, Locale locale) throws ParseException {
            log.info("text={}, locale={}", text, locale);
            NumberFormat format = NumberFormat.getInstance(locale);
            return format.parse(text);
        }

        @Override
        public String print(Number object, Locale locale) {
            log.info("object={}, locale={}", object, locale);
            return NumberFormat.getInstance(locale).format(object);
        }
    }
  • "1,000" 처럼 숫자 중간의 쉼표를 적용하려면 자바가 기본으로 제공하는 NumberFormat 객체를 사용하면 된다. 이 객체는 Locale 정보를 활용해서 나라별로 다른 숫자 포맷을 만들어준다.
  • parse() 를 사용해서 문자를 숫자로 변환한다.
  • print() 를 사용해서 객체를 문자로 변환한다.





포맷터를 지원하는 컨버전 서비스

  • FormattingConversionService 는 포맷터를 지원하는 컨버전 서비스이다.
  • FormattingConversionService 는 ConversionService 관련 기능을 상속받기 때문에 결과적으로 컨버터도 포맷터도 모두 등록할 수 있다.
  • DefaultFormattingConversionService 는 FormattingConversionService 에 기본적인 통화, 숫자 관련 몇가지 기본 포맷터를 추가해서 제공한다.
  • 테스트 코드를 통해서 사용법을 알아보면 아래와 같다.
public class FormattingConversionServiceTest {
    @Test
    void formattingConversionService() {
        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
        //컨버터 등록
        conversionService.addConverter(new StringToIpPortConverter());
        conversionService.addConverter(new IpPortToStringConverter()); 

        //포맷터 등록
        conversionService.addFormatter(new MyNumberFormatter());

        //컨버터 사용
        IpPort ipPort = conversionService.convert("127.0.0.1:8080", IpPort.class);
        assertThat(ipPort).isEqualTo(new IpPort("127.0.0.1", 8080)); //포맷터 사용
        assertThat(conversionService.convert(1000,   String.class)).isEqualTo("1,000");
        assertThat(conversionService.convert("1,000", Long.class)).isEqualTo(1000L);
    }
}
  • DefaultFormattingConversionService를 사용해서 포맷터를 지원하는 컨버전 서비스를 구현한다.
  • 위와 같이 컨버터와 포맷터를 등록해서 사용할 수 있다.
    @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());

            //추가
            registry.addFormatter(new MyNumberFormatter());
        }
    }
  • 우선순위는 컨버터가 우선하므로 포맷터가 적용되지 않고, 컨버터가 적용된다.





스프링에서 기본적으로 제공하는 기본 포맷터

  • @NumberFormat : 숫자관련 형식 지정할때 사용한다.
  • @DateTimeFormat : 날짜관련 형식을 지정할때 사용한다.
  • 아래와 같이 구현할 수 있다.
    @Data
    static class Form {
        @NumberFormat(pattern = "###,###") 
        private Integer number;

        @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        private LocalDateTime localDateTime;
    }






메시지 컨버터( HttpMessageConverter ) 와 착각하지 말자!

  • 메시지 컨버터( HttpMessageConverter )에는 컨버전 서비스가 적용되지 않는다.
  • HttpMessageConverter 의 역할은 HTTP 메시지 바디의 내용을 객체로 변환하거나 객체를 HTTP 메시지 바디에 입력하는 것이다.
  • 만약 JSON을 객체로 변환하는 메시지 컨버터는 내부에서 Jackson 같은 라이브러리를 사용한다.
    • JSON 결과로 만들어지는 숫자나 날짜 포맷을 변경하고 싶으면 해당 라이브러리가 제공하는 설정을 통해서 포맷을 지정해야 한다.
    • 결과적으로 이것은 컨버전 서비스와 전혀 관계가 없다.
  • 컨버전 서비스는 @RequestParam , @ModelAttribute , @PathVariable , 뷰 템플릿 등에서 사용할 수 있다.




REFERENCES

  • 김영한님의 스프링 MVC 2편

'Spring' 카테고리의 다른 글

의존관계 주입  (0) 2022.02.26
싱글톤 컨테이너  (0) 2022.02.26
서블릿 구조  (0) 2022.02.26
서블릿 예외처리  (0) 2022.02.26
빈 스코프  (0) 2022.02.26