ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring MVC - Converter 구현 및 사용 (Converter와 ConversionService)
    Spring/Spring MVC 2022. 2. 17. 01:26
    728x90
    반응형

     

     

    Converter 구현 및 사용

     

     

     

     

     

     

     

     

     

    Converter와 ConversionService

     HTTP 요청 파라미터는 모두 문자로 넘어오지만 스프링에서 파라미터로 넘어온 문자들을 @RequestParam, @ModelAttribute, @PathVariable을 통해 문자, 숫자, Boolean, Enum등과 같은 타입을 자동으로 타입을 변환하여 바인딩을 해준다. 어떻게 타입변환이 일어나서 바인딩이 되는지 ConverterConversionService를 통해 알아보자.

     

     

    참고
    @RequestParam, @ModelAttribute, @PathVariable 등에서 사용 가능
    메시지 컨버터(JSON)에는 컨버전 서비스가 적용되지 않는다.

     

     

     

    Converter

     스프링은 자동으로 타입에 맞게 바인딩되도록 타입 변환을 해주는 인터페이스 Converter를 구현한 클래스들을 제공한다. 스프링이 제공하는 Converter 구현 클래스들을 통해 우리는 자유롭게 HTTP 요청 파라미터로 넘어온 문자들을 원하는 타입으로 바인딩하여 사용할 수 있다. 

     

     스프링에서 기본으로 제공하는 String -> Integer으로 타입 변환해주는 Converter와 사용자가 정의 타입으로 변환해주는 Converter 구현 클래스를 직접 만들어 보자.

     

     

    Converter 인터페이스

     

    • Converter 인터페이스의 T Convert(S source) 메서드를 오버라이딩하여 타입 변환
    • 쉽게 생각해서 타입 S → 타입 T 로 변환시켜 주는 로직을 구현하면 된다.

     

    @FunctionalInterface
    public interface Converter<S, T> {
    
    	@Nullable
    	T convert(S source);
    
    	default <U> Converter<S, U> andThen(Converter<? super T, ? extends U> after) {
    		Assert.notNull(after, "After Converter must not be null");
    		return (S s) -> {
    			T initialResult = convert(s);
    			return (initialResult != null ? after.convert(initialResult) : null);
    		};
    	}
    
    }

     

    Converter 구현 클래스 ( String → Integer )

     

     스프링에서 기본적으로 제공하는 컨버터 중에 StringInteger 으로 타입 변환을 해주는 컨버터를 구현하여 테스트를 해보자.

     

    @Slf4j
    public class StringToIntegerConverter implements Converter<String, Integer> {
    
        @Override
        public Integer convert(String source) {
    
            log.info("StringToIntegerConverter source = {}", source);
            return Integer.parseInt(source);
        }
    }

     

     위 코드를 테스트 해보면 타입 변환이 잘 이루어진것을 확인할 수 있다.

     

    public class ConverterTest {
    
        @Test
        void StringToInteger(){
            StringToIntegerConverter converter = new StringToIntegerConverter();
            Integer result = converter.convert("10");
            Assertions.assertThat(result).isEqualTo(10);
        }
    }

     

    Converter 구현 클래스 ( String → 사용자 정의 타입 )

     

     이번에는 사용자가 정의한 타입(클래스)으로 타입 변환을 해주는 컨버터 구현 클래스를 만들어 보자.

     

    @Getter
    @EqualsAndHashCode
    public class IpPort {
    
        private String ip;
        private int port;
    
        public IpPort(String ip, int port) {
            this.ip = ip;
            this.port = port;
        }
    }

     

     HTTP 요청 파라미터로 넘어오는 String 타입의 문자를 위에서 만든 IpPort 타입으로 변환이 되는 컨버터 구현 클래스를 만들어보자.

     

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

     

     위 코드를 테스트 해보면 타입 변환이 잘 이루어진것을 확인할 수 있다.

     

    public class IpPortConverterTest {
        @Test
        void StringToIpPort(){
            StringToIpPortConverter converter = new StringToIpPortConverter();
            String source = "127.1.2.3:8080";
            IpPort result = converter.convert(source);
            Assertions.assertThat(result).isEqualTo(new IpPort("127.1.2.3", 8080));
        }
    }

     


     

    ConversionService

     Converter들을 하나하나 직접 찾아서 타입 변환에 사용하는 것은 매우 불편하기 때문에 스프링은 컨버터들을 모아서 편리하게 사용할 수 있는 기능을 제공하는데 그것이 바로 ConversionService 이다.

     

     

    ConversionService 인터페이스

     

    • 등록된 컨버터로 타입 변환을 해주는 기능을 제공한다.

     

    public interface ConversionService {
    
    	boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType);
    	boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
    
    	@Nullable
    	<T> T convert(@Nullable Object source, Class<T> targetType);
    
    	@Nullable
    	Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
    
    }

     

    DefaultConversionService 사용

     

    • 다음 두 인터페이스를 구현한 구현체이다.
           - ConversionService : 컨버터 사용에 초점
           - ConveterRegistry : 컨버터 등록에 초점
    • 인터페이스를 등록과 사용의 분리로 ISP 원칙을 적용한 구현 클래스이다.
    • ISP 원칙이 적용되어 컨버터 사용하는 입장은 어떤 컨버터가 등록되어있는지 몰라도 된다.
      즉, 컨버터가 어떻게 등록되고 관리하는지 사용하는 입장에서는 전혀 상관없고,
      '사용'에만 초점을 맞추면 된다.

     

     위에서 만든 예시로 만든 컨버터를 등록하고 사용을 하는 테스트 코드를 작성해 보자.

     

    public class ConversionServiceTest {
    
        @Test
        void conversionService(){
    
            // 컨버전 등록
            DefaultConversionService conversion = new DefaultConversionService();
            conversion.addConverter(new StringToIntegerConverter());
            conversion.addConverter(new StringToIpPortConverter());
    
            // 컨버전 사용
            Assertions.assertThat(conversion.convert("10", Integer.class)).isEqualTo(10);
    
            IpPort ipPort = conversion.convert("127.1.2.3:8080", IpPort.class);
            Assertions.assertThat(ipPort).isEqualTo(new IpPort("127.1.2.3", 8080));
        }
    
    }

     

     DefaultConversionService를 이용하여 컨버터의 등록과 사용이 정상적으로 이루어지는 것을 확인할 수 있다.

     


     

    스프링에 Converter 등록하고 사용하기

     스프링은 내부에서 ConversionService를 사용해서 타입을 변환한다. 예를 들어 @RequestParam 같은 곳에서 이 기능을 사용하여 타입을 변환한다.

     

     

    Converter 등록

     

    • Converter를 등록하기 위해서는 WebMvcConfigurer가 제공하는 addConverter()메서드를
      사용하여 Converter를 등록할 수 있다.
    • addConverter(FormatterRegistry registry)
           - FormatterRegistry : 인터페이스 ConveterRegistry의 구현 클래스
    • 참고로 위에서 만든 컨버터(String→Integer)를 등록하면 기본 컨버터가 아닌
      등록한 컨버터가 호출된다.

     

    @Configuration
    public class WebConfig implements WebMvcConfigurer {
        @Override
        public void addFormatters(FormatterRegistry registry) {
            registry.addConverter(new StringToIntegerConverter());
            registry.addConverter(new IntegeToStringConverter());
            registry.addConverter(new StringToIpPortConverter());
            registry.addConverter(new IpPortToStringConverter());
        }
    }

     

    등록한 Converter 사용

     

    @Slf4j
    @RestController
    public class ConverterController {
    
        @GetMapping("/converter")
        public String converterV1(@RequestParam Integer data){
    
            return "ok";
        }
        @GetMapping("/converter2")
        public String converterV2(@RequestParam IpPort ipport){
    
            return "ok";
        }
    
    }

    /converter?data=10 요청

    /converter2?source=127.1.2.3:8080 요청

     

     위 로그를 보면 스프링에서 기본으로 제공하는 컨버터가 아닌 직접 만든 StringToIntegerConverter 컨버터가 사용된 것을 볼 수있다.

     


     

    컨버터 처리 과정

     

     @RequestParam@RequestParam을 처리하는 ArgumentResolverRequestParamMethodArgumentResolver에서 ConversionService를 사용해서 타입을 변환한다. 부모 클래스와 다양한 외부 클래스를 호출하는 등 복잡한 내부 과정을 거치기 때문에 대략 이렇게 처리되는 것으로 이해해도 충분하다.

     


     

    뷰 템플릿에 컨버터 적용하기

     

    타임리프의 Converter 사용

     

    • ${ ... } : 타입 그대로 사용
    • ${{ ... }} : 컨버터 사용하여 타입 변환

     

    @GetMapping("/converter-view")
    public String converterView(Model model) {
        model.addAttribute("number", 100);
        model.addAttribute("ipPort", new IpPort("127.1.2.3", 8080));
        return "view1";
    }
    <li>${number}: <span th:text="${number}" ></span></li>
    <li>${{number}}: <span th:text="${{number}}" ></span></li>
    <li>${ipPort}: <span th:text="${ipPort}" ></span></li>
    <li>${{ipPort}}: <span th:text="${{ipPort}}" ></span></li>

     

     /converter-view를 호출하면 다음과 같다

     

     

     ${{number}} 는 뷰 템플릿이 Integer 타입이 아닌 String 타입으로 변환하여 랜더링이 되어 문자 100이 출력이 된다. 또한 ${{ipPort}} 역시 타입 변환이되어 문자로 출력된 결과를 볼 수 있다. ${ipPort}는 타입 그대로 사용되기 때문에 toString이 적용된 결과가 화면에 출력된 것을 볼 수 있다.


     

    Form에서 사용되는 Converter

     

    • th:field가 자동으로 ConversionService를 이용하여 타입 변환이 된다.

     

    <form th:object="${form}" th:method="post">
        th:field <input type="text" th:field="*{ipPort}"><br/>
        th:value <input type="text" th:value="*{ipPort}"><br/>
        <input type="submit"/>
    </form>

     

    728x90
    반응형

    댓글

Designed by Tistory.