스프링 타입 컨버터(1) - converter, conversionService

2022. 4. 11. 07:06Spring 기초

애플리케이션을 개발하다 보면 타입을 변환해야 하는 경우가 매우매우 많다.

아래 코드를 보자.

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

그런데,

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

다음과 같이 파라미터 타입으로 Integer를 받는다면?
/hello-v2?data=10 으로 요청을 보낼 경우, 앞서 말했듯 HTTP 요청 파라미터는 모두 문자로 처리된다. 하지만 data를 출력해보면 Integer로 변환되었다. 이는 스프링님이 자동으로 타입 변환을 해주었기 때문이다. 자동으로 변환해주지 않는다면, 맨 처음 본 컨트롤러처럼 Integer.parseInt(), valueOf() 메서드를 써야하는 불편함이 있었을 것이다.
@RequestParam 뿐만아니라 @ModelAttribute, @PathVariable

@ModelAttribute UserData data
class UserData {
 Integer data;
}

여기에도 Integer 10이 들어와 꽂힌다.

/users/{userId}
@PathVariable("data") Integer data

URL 경로는 문자다. /users/10 여기서 10도 숫자 10이 아니라 그냥 문자 "10"이다.  하지만 여기 data에도 Integer 값이 꽂힌다. Integer 타입으로 받을 수 있는 것도 역시 스프링이 타입 변환을 해주기 때문이다.


문자를 숫자로 변경하는 예시를 들었지만, 반대로 숫자를 문자로 변경하는 것도 가능하고, Boolean 타입을 숫자로 변경하는 것도 가능하다. 만약 개발자가 새로운 타입을 만들어서 변환하고 싶으면 어떻게 하면 될까?

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

지금부터 컨버터를 구현해서 등록하는 방법에 대해 소개한다.
참고로

org.springframework.core.convert.converter.Converter 를 사용해야 한다

이 컨버터 인터페이스는 convert 메서드를 구현해줘야 한다. 가장 기본적인 String -> Integer 컨버터를 만들어보자.

public class StringToIntegerConverter implements Converter<String, Integer> 

 @Override
     public Integer convert(String source) {
         return Integer.valueOf(source);
     }
}

매우 심플하다. Converter의 지네릭스 타입에 <쏘쓰타입, 결과물 타입>으로 지정해준다. 
메서드 내부에서는 쏘쓰를 Integer로 변환하는 코드를 작성했다.

이제 Integer -> String 은 어떻게 하면 될지 감이 올 것이다.

public class IntegerToStringConverter implements Converter<Integer, String> {

     @Override
     public String convert(Integer source) {
         return String.valueOf(source);
     }
}

지네릭스 타입 맞춰서 작성해주고, String -> Integer 변환 코드를 작성하면 된다.

이런 기본적인 타입 말고, 사용자 정의 타입(클래스) 컨버터를 살펴보자.
아래는 IpPort 클래스다. IpPort -> String, String -> IpPort 컨버터 작성 예제를 소개한다.

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

 

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

String 입력값을 -> IpPort로 변환하는 컨버터다. String source를 " : " 기준으로 잘라 배열에 나눠 담은 뒤, port값은 int 타입이므로 그에 맞추어 변환하였다. 
이제 127.0.0.1:8080 같은 문자를 입력하면 IpPort 객체를 만들어 반환한다.

다음은 반대로 IpPort 타입을 String으로 변환하는 컨버터다.

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

source의 ip값, 포트값을 가져와 단순하게 +연산자로 String을 만들어 반환한다.
IpPort 객체를 입력하면 127.0.0.1:8080 같은 문자를 반환한다.

이렇게 만든 컨버터는

IpPortToStringConverter converter = new IpPortToStringConverter();
 IpPort source = new IpPort("127.0.0.1", 8080);
 String result = converter.convert(source);

이런 방식으로 사용이 가능하다.
근데 이렇게 직접 사용할 예정이라면 왜 따로 만들었을까? 싶다.  ConversionService는 컨버터들을 모아서 편리하게 사용할 수 있는 기능들을 제공한다.

이 컨버전 서비스(ConversionService)에

void conversionService() {
 //내가 만든 컨버터들을 등록한다.
 DefaultConversionService conversionService = new DefaultConversionService();
 conversionService.addConverter(new StringToIntegerConverter());
 conversionService.addConverter(new IntegerToStringConverter());
 conversionService.addConverter(new StringToIpPortConverter());
 conversionService.addConverter(new IpPortToStringConverter());

일단 내가 만든 컨버터를 등록한다(참고로 내가 만든 컨버터가 자동 적용되는 컨버터보다 우선순위가 높다.)

// 등록한 컨버터를 사용한다. 
conversionService.convert(10, String.class)
conversionService.convert("10", Integer.class);
IpPort ipPort = conversionService.convert("127.0.0.1:8080", IpPort.class);
String ipPortString = conversionService.convert(new IpPort("127.0.0.1",8080), String.class);

참고로 컨버전 서비스의 참조변수 타입인 DefaultConversionService는 ConversionService 인터페이스를 구현했는데, 추가로 컨버터를 등록하는 기능도 제공한다.

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


스프링에 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());
     }
}

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

참고로, @RequestParam 은 @RequestParam 을 처리하는 ArgumentResolver인 RequestParamMethodArgumentResolver 에서 ConversionService 를 사용한다. 

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

타임리프는 렌더링 시에 컨버터를 적용해서 렌더링 하는 방법을 편리하게 지원한다.

예를들어,

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

모델에 숫자 10000와 IpPort객체를 담아서 뷰 템플릿에 전달하면

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
 <meta charset="UTF-8">
 <title>Title</title>
</head>
    <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>
</html>

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

물론 스프링과 통합 되어서 스프링이 제공하는 컨버전 서비스를 사용하므로, 우리가 등록한 컨버터들을 사용할 수 있다.

  • 변수 표현식 : ${...}
  • 컨버전 서비스 적용 : ${{...}}

실행 결과는 다음과 같다.

• ${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 가 출력된다.