4 분 소요


소개

스프링 타입 컨버터
문자를 숫자로 변환하거나, 숫자를 문자로 변환해야 하는 것 처럼 애플리케이션을 개발하다 보면 타입을 변환해야 하는 경우가 상당히 많다.

HelloController

@RestController
public class HelloController {

    @GetMapping("/hello-v1")
    public String helloV1(HttpServletRequest request) {

        String data = request.getParameter("data"); //문자 타입 조회
        Integer intValue = Integer.valueOf(data); //숫자 타입으로 변경
        System.out.println("intValue = " + intValue);
        return "ok";
    }
}

String data = request.getParameter("data")
HTTP 요청 파라미터는 모두 문자로 처리된다. 따라서, 다른 타입으로 변환하고 싶다면 다음과 같이 변환하는 과정을 거쳐야 한다.

Integer intValue = Integer.valueOf(data)

HelloController - 추가

@GetMapping("/hello-v2")
public String helloV2(@RequestParam Integer data) {
    System.out.println("data = " + data);
    return "ok";
}

HTTP 쿼리 스트링으로 전달하는 data는 문자 10이지만, 스프링이 제공하는 @RequestParam을 사용하면 문자 10을 Integer타입의 숫자 10으로 받을 수 있다.
이것은 스프링이 중간에서 타입을 변환해주었기 때문이다.

이러한 예는 @ModelAttribute, @PathVariable에서도 확인할 수 있다.

스프링의 타입 변환 적용 예

  • 스프링 MVC 요청 파라미터
    • @RequestParam, @ModelAttribute, @PathVariable
  • @Value등으로 YML 정보 읽기
  • XML에 넣은 스프링 빈 정보를 변환
  • 뷰를 렌더링 할 때

만약 개발자가 기본 제공타입이 아닌 새로운 타입을 만들어서 변환하고 싶다면 컨버터 인터페이스를 구현해서 사용하면 된다.


타입 컨버터 - Converter

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

컨버터 인터페이스

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

필요하면 X -> Y 타입으로 변환하는 컨버터 인터페이스를 만들고, 또 Y -> X타입으로 변환하는 컨버터 인터페이스를 만들어서 등록하면 된다.

StringToIntegerConverter

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

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

(IntegerToStringConverter는 비슷하니 생략…)

사용자 정의 타입 컨버터

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

IpPort

@Getter
@EqualsAndHashCode
public class IpPort {

    private String ip;
    private int port;

    public IpPort(String ip, int port) {
        this.ip = ip;
        this.port = port;
    }
}

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

StringToIpPortConverter

public class StringToIpPortConverter implements Converter<String, IpPort> {

    @Override
    public IpPort convert(String source) {

        String[] split = source.split(":");
        String ip = split[0];
        int port = Integer.parseInt(split[1]);

        return new IpPort(ip, port);
    }
}

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

IpPortToStringConverter

public class IpPortToStringConverter implements Converter<IpPort, String> {
    @Override
    public String convert(IpPort source) {
        return source.getIp() + ":" + source.getPort();
    }
}

ConverterTest

@Test
@Test
void ipPortToString() {
    IpPortToStringConverter converter = new IpPortToStringConverter();
    IpPort source = new IpPort("127.0.0.1", 8080);
    String result = converter.convert(source);
    assertThat(result).isEqualTo("127.0.0.1:8080");
}

타입 컨버터를 하나하나 직접 사용하면, 개발자가 직접 컨버팅 하는 것과 차이가 없다.
타입 컨버터를 등록하고 관리하면서 편리하게 변환 기능을 제공하는 역할을 하는 것이 필요하다.

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

공식 문서


ConversionService

스프링은 개별 컨버터를 모아두고 묶어서 편리하게 사용할 수 있는 기능을 제공하는데, 이것이 ConversionService이다.
ConversionService 인터페이스는 단순히 컨버팅이 가능한지 확인하는 기능과 컨버팅 기능을 제공한다.

사용 예

ConversionServiceTest

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

    // 사용
    IpPort ipPort = conversionService.convert("127.0.0.1:8080", IpPort.class);
    assertThat(ipPort).isEqualTo(new IpPort("127.0.0.1", 8080));

    // 나머지 생략...
}

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

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

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

  • ConversionService: 컨버터 사용에 초점
  • ConverterRegistry: 컨버터 등록에 초점 이렇게 인터페이스를 분리하는 것을 ISP라 한다.

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


스프링에 Converter 적용

WebConfig - 컨버터 등록

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

WebMvcConfigurer가 제공하는 addFormatters()를 사용해서 추가하고 싶은 컨버터 등록.
이렇게 하면 스프링은 내부에서 사용하는 ConversionService에 컨버터를 추가해준다.

@RequestParam같은 애노테이션은 컨버터를 직접 등록하기 전에도 잘 수행 되었는데, 이것은 스프링이 내부에서 많은 기본 컨버터들을 제공하기 때문이다.
컨버터를 추가하면 추가한 컨버터가 기본 컨버터 보다 높은 우선순위를 가진다.

HelloController

@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쿼리 스트링이 객체 타입으로 변환된다.

처리 과정

@RequestParam을 처리하는 ArgumentResolverRequestParamMethodArgumentResolver에서 ConversionService를 사용해서 타입을 변환한다.


뷰 템플릿에 적용

ConverterController

@Controller
public class ConverterController {

    @GetMapping("/converter-view")
    public String converterView(Model model) {
        model.addAttribute("number", 10000);
        model.addAttribute("ipPort", new IpPort("127.0.0.1", 8080));
        return "converter-view";
    }
}

converter-view.html

<!-- 생략.. -->

<body>
<ul>
    <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>
</ul>
</body>

타임리프는 ${ {...} }를 사용하면 자동으로 ConversionService를 사용해서 변환된 결과를 출력해준다.

  • 변수 표현식: ${...}
  • ConversionService 적용: ${ {...} }(붙여서 사용하지만 블로그가 정상 출력이 안되서…)

실행 결과

${number}: 10000
${ {number} }: 10000
${ipPort}: hello.typeconverter.type.IpPort@59cb0946
${ {ipPort} }: 127.0.0.1:8080

${ {ipPort} }:컨버터를 적용하게 되면 IpPort 타입을 String 타입으로 변환해야 하므로 IpPortToStringConverter가 적용된다. 그 결과 127.0.0.1:8080가 출력된다


폼에 적용

(Controller에서는 IpPort에 값을 주입한 뒤 Form객체에 담아서 Model을 통해 전달했다.)

converter-form.html

<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>

타임리프의 th:fieldConversionService도 함께 적용된다.
여기서는 th:field가 자동으로 ConversionService를 적용해주어서 ${ {ipPort} }처럼 적용이 되었다. 따라서, IpPort -> String으로 변환된다.


<출처 : 인프런 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술(김영한)>

댓글남기기