스프링 타입 컨버터

상훈·2024년 3월 23일

http 요청에서 쿼리 파라미터로 데이터를 넘겨 받는 경우에는 숫자를 받더라도 int 타입이 아닌 문자열로 데이터를 받게 된다

따라서 파라미터의 값을 숫자로 사용하기로 위해서는 타입 변환이 필요하다.

  • 예시

  • // hello?data=10
    
    @GetMapping("/hello")
    public String hello(HttpServletRequest request){
      String data = request.getParameter("data");
      Integer intValue = Integer.valueOf(data);
      return "ok"
    }
    
    // 10 출력

하지만 스프링을 사용하면 위와 같이 직접 변환할 필요가 없는데 그 이유는 스프링의 @RequestParam으로 파라미터 데이터를 받게 되면 알아서 형 변환을 진행해주기 때문이다

  • 예시

  • // hello?data=10
      
    @GetMapping("/hello")
    public String hello(@RequestParam Integer data){
      System.out.println("data = " + data)
      return "ok"
    }
    
    // 10 출력

@RequestParam, @ModelAttribute, @PathVariable 등 에서도 타입 변환이 적용된다.

여기에서 타입 변환이란 문자를 숫자로, 숫자를 문자로, 숫자를 논리형 타입으로 변경하는 것 전부 가능하다.

그런데 만약 개발을 진행할 때 새로운 타입을 만들어서 변환하고 싶을 때는 어떻게 하면 될까?

그럴 때는 스프링 컨버터 인터페이스를 사용하면 된다!

// converter interface
package org.springframework.core.convert.converter;

public interface Convert<S, T> {
  T convert(S source);
}

타입 컨버터 - Converter

  • 문자 -> 숫자

    public class StringToIntegerConverter implements Converter<String, Integer> {
        @Override
        public Integer convert(String source) {
            return Integer.valueOf(source);
        }
    }
  • 숫자 -> 문자

    public class IntegerToStringConverter implements Converter<Integer, String> {
        @Override
        public String convert(Integer source) {
            return String.valueOf(source);
        }
    }
    

숫자와 문자로만 봤을 때는 굳이 Converter를 사용해야 하는 이유를 잘 모르겠지만 실제로는 이러한 간단한 타입 변환 말고도 다양한 상황에서 변환을 해줄 상황이 있다.

예를 들면 ip를 받았을 때 이를 ip와 port로 구분하고 싶을 때이다.

  • input
    • "127.0.0.1:8080"
  • ip
    • "127.0.0.1"
  • port
    • 8080

이처럼 input을 ip와 port로 변환하고 싶을 때, 그때그때 계속해서 형 변환을 하는 것은 매우 귀찮기 때문에 간단하게 converter interface 구현체를 만들어 사용한다면 쉽게 형변환이 가능한다.

@Slf4j
public class StringToIpPortConverter implements Converter<String, IpPort> {
    @Override
    public IpPort convert(String 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를 만들었지만 실제 사용할 때 굉장히 많은 컨버터를 만들었다면 이때 컨버터를 하나하나 찾아서 타입 변환에 사용하는 것은 매우 귀찮을 것이다. 따라서 스프링은 개별 컨버터를 모아두고 그것들을 묵어서 편리하게 사용할 수 있는 ConversionService라는 기능을 제공한다.

컨버전 서비스 - 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);
}

위에 내용을 직접 예제 코드로 작성해서 확인하면 다음과 같다.

  1. ConversionService 인터페이스의 구현체인 DefaultConversionService 객체 생성
  2. 우리가 만든 컨버터 등록
  3. 컨버터 사용
public class ConversionServiceTest {

    @Test
    void conversionService() {
        //등록
        DefaultConversionService conversionService = new DefaultConversionService();
        conversionService.addConverter(new StringToIntegerConverter());
        conversionService.addConverter(new StringToIpPortConverter());
        conversionService.addConverter(new IpPortToStringConverter());
        conversionService.addConverter(new IntegerToStringConverter());

        //사용
        Assertions.assertThat(conversionService.convert("10", Integer.class)).isEqualTo(10);
        Assertions.assertThat(conversionService.convert(10, String.class)).isEqualTo("10");
        Assertions.assertThat(conversionService.convert("127.0.0.1:8080", IpPort.class)).isEqualTo(new IpPort("127.0.0.1", 8080));
    }
}

이때 코드에서 알 수 있듯이 ConversionService는 등록과 사용이 명확하게 분리되어있다.

즉 사용자 입장에서는 타입 컨버터를 전혀 몰라도 타입을 변환할 수 있으며 오로지 컨버전 서비스 인터페이스에만 의존하게 된다.

이 때 인터페이스 분리 원칙이 적용된 것을 확인할 수 있다. (ISP: Interface Segregation Principle)

실제 DefaultConversionService는 다음 두 인터페이스를 구현한다.

  • ConversionService : 컨버터 사용
  • ConverterRegistry : 컨버터 등록
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.core.convert.support;

import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.ConverterRegistry;

public interface ConfigurableConversionService extends ConversionService, ConverterRegistry {
}

사용 방법

우리가 만든 컨버터를 실제 사용할 때는 converter를 등록해서 사용하면 된다. 스프링은 내부에서 ConversionService를 제공하기 때문에 우리는 WebMvcConfigurer가 제공하는 addFormatters를 사용해서 우리가 만든 컨버터를 등록하면 스프링이 내부에서 사용하는 ConversionService에 우리가 만든 Converter를 등록해준다.

@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";
}

뷰에서 컨버터 적용

  • 타임리프에서 컨버터 적용 방법
    • *{{}}
    • th:field

포맷터 - Formatter

  • "1,000" 라는 문자를 "1000" 이라는 숫자로 변경
  • 날짜 객체를 문자인 "2021-0101 10:50:11" 와 같이 출력하거나 그 반대의 상황

Converter는 범용적이며 (객체 -> 객체) Formatter는 문자에 특화(객체 -> 문자, 문자 -> 객체) + 현지화

포맷터는 위에 컨버터처럼 포매터 인터페이스를 구현해서 사용할 수 있다.

  • parser : 객체를 문자로 변경
  • printer : 문자를 객체로 변경

포맷터 구현

@Slf4j
public class MyNumberFormatter implements Formatter<Number> {
    @Override
    public Number parse(String text, Locale locale) throws ParseException {
        log.info("text={}, locale={}", text, locale);
        // "1,000" -> 1000
        NumberFormat format = NumberFormat.getInstance(locale);
        Number parse = format.parse(text);
        return parse;
    }

    @Override
    public String print(Number object, Locale locale) {
        log.info("object={}, locale={}", object, locale);
        NumberFormat instance = NumberFormat.getInstance(locale);
        String format = instance.format(object);
        return format;
    }
}

포맷터 적용

@Configuration
public class WebConfigure implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
//        registry.addConverter(new IntegerToStringConverter());
        registry.addConverter(new StringToIpPortConverter());
//        registry.addConverter(new StringToIntegerConverter());
        registry.addConverter(new IpPortToStringConverter());

        registry.addFormatter(new MyNumberFormatter());
    }
}

스프링 기본 포맷터

  • @NumberFormat

    • @NumberFormat(pattern = "###,###")
      private Integer number;
  • @DateTimeFormat

    • @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
      private LocalDateTime localDateTime;
profile
문송 개발자

0개의 댓글