ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [김영한 스프링] 51. 스프링 타입 컨버터 - 타입 컨버터(Converter) & 컨버전 서비스(ConversionService)
    Spring/스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 2023. 10. 10. 23:02

    타입 컨버터 - Converter

     

    타입 컨버터를 사용하려면 org.springframework.core.convert.converter.Converter 인터페이스를 구현하면 된다. 

     

    주의
    Converter라는 이름의 인터페이스가 많으니 조심해야 한다.
    org.springframework.core.convert.converter.Converter를 사용해야 한다.

     

     

    컨버터 인터페이스

    package org.springframework.core.convert.converter;

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

     

     

    StringToIntegerConverter - 문자를 숫자로 변환하는 타입 컨버터

    package hello.typeconverter.converter;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.core.convert.converter.Converter;
    
    @Slf4j
    public class StringToIntegerConverter implements Converter<String, Integer> {
    
    
        @Override
        public Integer convert(String source) {
    
            log.info("convert source={}", source);
    
            return Integer.valueOf(source);
        }
    }

    main/java/hello/typeconverter/converter/StringToIntegerConverter 생성

     

    String Integer로 변환하기 때문에 소스가 String이 된다. 이 문자를 Integer.valueOf(source)를 사용해서 숫자로 변경한 다음에 변경된 숫자를 반환하면 된다.

     

     

    IntegerToStringConverter - 숫자를 문자로 변환하는 타입 컨버터

    package hello.typeconverter.converter;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.core.convert.converter.Converter;
    
    @Slf4j
    public class IntegerToStringConverter implements Converter<Integer, String> {
    
    
        @Override
        public String convert(Integer source) {
    
            log.info("convert source={}", source);
    
            return String.valueOf(source);
        }
    }

    main/java/hello/typeconverter/converter/IntegerToStringConverter 생성

     

    숫자를 문자로 변환하는 타입 컨버터이다. 앞의 컨버터와 반대의 일을 한다. 이번에는 숫자가 입력되기 때문에 Integer가 된다. String.valueOf(source)를 사용해서 문자로 변경한 다음 변경된 문자를 반환하면 된다.

     

     

    ConverterTest - 타입 컨버터 테스트 코드

    package hello.typeconverter.converter;
    
    import org.assertj.core.api.Assertions;
    import org.junit.jupiter.api.Test;
    
    import static org.assertj.core.api.Assertions.*;
    
    public class ConverterTest {
    
        @Test
        void stringToInteger() {
            StringToIntegerConverter converter = new StringToIntegerConverter();
            Integer result = converter.convert("10");
            assertThat(result).isEqualTo(10);
        }
    
        @Test
        void integerToString() {
            IntegerToStringConverter converter = new IntegerToStringConverter();
            String result = converter.convert(10);
            assertThat(result).isEqualTo("10");
        }
    }

    test/java/hello/typeconverter/converter/ConverterTest 생성

     

     

    실행

     

     

    사용자 정의 타입 컨버

    타입 컨버터 이해를 돕기 위해 조금 다른 컨버터를 준비해 보았다.

    127.0.0.1:8080과 같은 IP, PORT를 입력하면 IpPort 객체로 변환하는 컨버터를 만들어보자

     

     

    IpPort

    package hello.typeconverter.type;
    
    import lombok.EqualsAndHashCode;
    import lombok.Getter;
    
    @Getter
    @EqualsAndHashCode
    public class IpPort {
    
        private String ip;
        private int port;
    
        public IpPort(String ip, int port) {
            this.ip = ip;
            this.port = port;
        }
    }

    main/java/hello/typeconverter/type/IpPort 생성

     

    롬복의 @EqualsAndHashCode를 넣으면 모든 필드를 사용해서 equals(), hashcode()를 생성한다. 따라서 모든 필드의 값이 같다면 a.equals(b)의 결과가 참이 된다.

     

     

    StringToIpPortConverter - 컨버터

    package hello.typeconverter.converter;
    
    import hello.typeconverter.type.IpPort;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.core.convert.converter.Converter;
    
    @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);
        }
    }

    main/java/hello/typeconverter/converter/StringToIpPortConverter 생성

     

    127.0.0.1:8080같은 문자를 입력하면 IpPort 객체를 만들어 반환한다.

     

     

    IpPortToStringConverter

    package hello.typeconverter.converter;
    
    import hello.typeconverter.type.IpPort;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.core.convert.converter.Converter;
    
    @Slf4j
    public class IpPortStringConverter implements Converter<IpPort, String> {
        @Override
        public String convert(IpPort source) {
    
            log.info("convert source={}", source);
    
            // IpPort 객체 -> 127.0.0.1:8080
            return source.getIp() + ":" + source.getPort();
        }
    }

    main/java/hello/typeconverter/converter/IpPortStringConverter 생성

     

    IpPort 객체를 입력하면 127.0.0.1:8080같은 문자를 반환한다.

     

     

    ConverterTest - IpPort 컨버터 테스트 추가

    package hello.typeconverter.converter;
    
    import hello.typeconverter.type.IpPort;
    import org.assertj.core.api.Assertions;
    import org.junit.jupiter.api.Test;
    
    import static org.assertj.core.api.Assertions.*;
    
    public class ConverterTest {
    
        @Test
        void stringToInteger() {
            StringToIntegerConverter converter = new StringToIntegerConverter();
            Integer result = converter.convert("10");
            assertThat(result).isEqualTo(10);
        }
    
        @Test
        void integerToString() {
            IntegerToStringConverter converter = new IntegerToStringConverter();
            String result = converter.convert(10);
            assertThat(result).isEqualTo("10");
        }
    
        @Test
        void stringToIpPort() {
            IpPortStringConverter converter = new IpPortStringConverter();
            IpPort source = new IpPort("127.0.0.1", 8080);
            String result = converter.convert(source);
            assertThat(result).isEqualTo("127.0.0.1:8080");
        }
    
        @Test
        void ipPortToString() {
            StringToIpPortConverter converter = new StringToIpPortConverter();
            String source = "127.0.0.1:8080";
            IpPort result = converter.convert(source);
            assertThat(result).isEqualTo(new IpPort("127.0.0.1", 8080));
    
        }
    }

     

     

    실행

     

    타입 컨버터 인터페이스가 단순해서 이해하기 어렵지 않을 것이다.

    그런데 이렇게 타입 컨버터를 하나하나 직접 사용하면, 개발자가 직접 컨버팅 하는 것과 큰 차이가 없다.

    타입 컨버터를 등록하고 관리하면서 편리하게 변환 기능을 제공하는 역할을 하는 무언가가 필요하다.

     

    참고
    스프링은 용도에 따라 다양한 방식의 타입 컨버터를 제공한다.

    Converter -> 기본 타입 컨버터
    ConverterFactory -> 전체 클래스 계층 구조가 필요할 때
    GenericConverter -> 정교한 구현, 대상 필드의 애노테이션 정보 사용 가능
    ConditionalGenericConverter -> 특정 조건이 참인 경우에만 실행

    자세한 내용은 공식 문서를 참고하자.
    https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#coreconvert

     

    참고
    스프링은 문자, 숫자, 불린, Enum등 일반적인 타입에 대한 대부분의 컨버터를 기본으로 제공한다. IDE에서 Converter, ConverterFactory, GenericConverter의 구현체를 찾아보면 수많은 컨버터를 확인할 수 있다.

     

     

    컨버전 서비스 - ConversionService

     

    이렇게 타입 컨버터를 하나하나 직접 찾아서 타입 변환에 사용하는 것은 매우 불편하다. 그래서 스프링은 개별 컨버터를 모아두고 그것들을 묶어서 편리하게 사용할 수 있는 기능을 제공하는데, 이것이 바로 컨버전 서비스(ConversionService)이다.

     

     

    ConversionService 인터페이스

    package org.springframework.core.convert;

    import org.springframework.lang.Nullable;

    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);
    }

    컨버전 서비스 인터페이스는 단순히 컨버팅이 가능한가? 확인하는 기능과, 컨버팅 기능을 제공한다.

     

     

    ConversionServiceTest - 컨버전 서비스 테스트 코드

    package hello.typeconverter.converter;
    
    import hello.typeconverter.type.IpPort;
    import org.assertj.core.api.Assertions;
    import org.junit.jupiter.api.Test;
    import org.springframework.core.convert.support.DefaultConversionService;
    
    import static org.assertj.core.api.Assertions.*;
    
    public class ConversionServiceTest {
    
        @Test
        void conversionService() {
    
            // 등록
            DefaultConversionService conversionService = new DefaultConversionService();
            conversionService.addConverter(new StringToIntegerConverter());
            conversionService.addConverter(new IntegerToStringConverter());
            conversionService.addConverter(new StringToIpPortConverter());
            conversionService.addConverter(new IpPortStringConverter());
    
            // 사용
            assertThat(conversionService.convert("10", Integer.class)).isEqualTo(10);
    
            assertThat(conversionService.convert(10, String.class)).isEqualTo("10");
            
            IpPort ipPort = conversionService.convert("127.0.0.1:8080", IpPort.class);
            assertThat(ipPort).isEqualTo(new IpPort("127.0.0.1", 8080));
    
            String ipPortString = conversionService.convert(new IpPort("127.0.0.1", 8080), String.class);
            assertThat(ipPortString).isEqualTo("127.0.0.1:8080");
        }
    }

    test/java/hello/typeconverter/converter/ConversionServiceTest 생성

     

    DefaultConversionService는 ConversionService 인터페이스를 구현했는데, 추가로 컨버터를 등록하는 기능도 제공한다.

     

    등록과 사용 분리

    컨버터를 등록할 때는 StringToIntegerConverter같은 타입 컨버터를 명확하게 알아야 한다. 반면에 컨버터를 사용하는 입장에서는 타입 컨버터를 전혀 몰라도 된다. 타입 컨버터들은 모두 컨버전 서비스 내부에 숨어서 제공된다. 따라서 타입을 변환을 원하는 사용자는 컨버전 서비스 인터페이스에만 의존하면 된다. 물론 컨버전 서비스를 등록하는 부분과 사용하는 부분을 분리하고 의존관계 주입을 사용해야 한다.

     

    컨버전 서비스 사용

    Integer value = conversionService.convert("10", Integer.class);

     

    인터페이스 분리 원칙 - ISP(Interface Segregation Principle)

    인터페이스 분리 원칙은 클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다.

     

    DefaultConversionService는 다음 두 인터페이스를 구현했다.

    • ConversionService : 컨버터 사용에 초점
    • ConverterRegistry : 컨버터 등록에 초점

     

    이렇게 인터페이스를 분리하면 컨버터를 사용하는 클라이언트와 컨버터를 등록하고 관리하는 클라이언트의 관심사를 명확하게 분리할 수 있다. 특히 컨버터를 사용하는 클라이언트는 ConversionService만 의존하면 되므로, 컨버터를 어떻게 등록하고 관리하는지는 전혀 몰라도 된다. 결과적으로 컨버터를 사용하는 클라이언트는 꼭 필요한 메서드만 알게 된다. 이렇게 인터페이스를 분리하는 것을 ISP 라 한다.

     

    ISP 참고 :

    https://ko.wikipedia.org/wiki/%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4_%EB%B6%84%EB%A6%AC_%EC%9B%90%EC%B9%99

     

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

     

     

    실행

     

     

    출처 : https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2

     

    스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의

    웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있

    www.inflearn.com

Designed by Tistory.