Spring

Spring - 타입 컨버터 (Converter, ConversionService)

녁이 2024. 1. 2. 02:34
728x90
반응형

스프링 타입 컨버터 소개

문자↔숫자 변환과 같이 애플리케이션을 개발하다 보면 타입을 변환해야 하는 경우가 상당히 많다.

간단한 예시를 보자.

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

파라미터로 data=10 을 보냈을 때,

request.getparameter는 전부 String 타입이므로, intValue와 같이 int 타입으로 바꿔주는 로직이 필요하다.

문자열 "10" 에서 숫자 10 으로 변환이 된다는 말이다.

 

그러면, 우리가 여태까지 자주 사용했던 @RequestParam은 어떻게 될까?

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

http://localhost:8080/hello-v2?data=10 으로 Get 요청이 오면,

"10" 이 숫자 10으로 타입이 변환되어 data에 저장된다.

즉, String 타입의 "10"를 숫자 10으로 타입 변환해주는 로직을 스프링이 자동으로 해준다는 말이다.

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


스프링의 타입 변환 적용 예

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

 

앞에서는 문자를 숫자로 변경하는 예시를 들었지만, 반대로 숫자를 문자로 변경하는 것도 가능하고, Boolean 타입을 숫자로 변경하는 것도 가능하다.

만약 개발자가 새로운 타입을 만들어서 변환하고 싶으면 어떻게 하면 될까?

 


타입 컨버터 - Converter

컨버터 인터페이스

package org.springframework.core.convert.converter;
public interface Converter<S, T> {
 T convert(S source);
}

스프링이 제공하는 S → T 로 타입 변환해주는 확장 가능한 컨버터 인터페이스 이다.

개발자는 스프링에 추가적인 타입 변환이 필요하면 이 컨버터 인터페이스를 구현해서 등록하면 된다.

 

※주의 : org.springframework.core.convert.converter.Converter 를 사용해야 한다.

 

 

먼저 가장 단순한 형태인 문자를 숫자로 바꾸는 타입 컨버터 코드를 예시로 보자.

 

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 로 타입을 변환해주는 컨버터이다.

 

@Test
 void stringToInteger() {
 StringToIntegerConverter converter = new StringToIntegerConverter();
 Integer result = converter.convert("10");
 assertThat(result).isEqualTo(10);
 }

위의 Test 코드를 작성하면, 성공적으로 출력된다.

 

그러면 이런 간단한 타입들을 위한 컨버터말고, 사용자가 직접 정의한 타입을 확인해보자.

사용자 정의 타입 컨버터

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) 의 결과가 참이 된다

(Test 코드를 작성할 때, IpPort 객체를 직접적으로 비교가 가능하다. → ip, port 필드를 가진 객체 비교 가능)

 

StringToIpPortConverter - 컨버터

@Slf4j
public class StringToIpPortConverter implements Converter<String, IpPort> {
 @Override
 public IpPort convert(String source) {
 log.info("convert source={}", 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 객체를 만들어 반환

 

이를 테스트해보자.

@Test
void stringToIpPort() {
 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));
}

@EqualsAndHashCode 덕분에 isEqualTo(new IpPort("127.0.0.1", 8080) 를 result와 직접 비교가 가능하다.

 

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

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

→ 이게 바로 뒤에서 말할 ConversionService 이다.

 

참고

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

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

 


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

}

컨버팅 가능한지 확인하는 기능컨버팅 기능을 제공

 

어떻게 사용하는지 Test 코드를 통해 확인해보자.

 @Test
 void conversionService() {
 
 //등록
 DefaultConversionService conversionService = new
DefaultConversionService();

 conversionService.addConverter(new StringToIntegerConverter());
 conversionService.addConverter(new StringToIpPortConverter());
 
 //사용
 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));

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

 

코드를 보면 알 수 있듯, 등록과 사용이 제대로 분리되어 있다.

addConverter를 통해 등록, convert(입력, 출력 타입) 를 통해 사용을 한다. 

 

DefaultConversionService 구현체

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

인터페이스의 분리를 통해, 자바의 인터페이스 분리 원칙 - ISP(Interface Segregation Principle) 을 지킨다.

 

 

스프링은 내부에서 ConversionService 를 사용해서 타입을 변환한다.

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

 


Converter 적용하기

WebConfig에 컨버터를 등록

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

addFormatters() 를 사용해서 추가하고 싶은 컨버터를 등록하면 된다.

 

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

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

 

코드를 통해 확인하자.

 

컨트롤러

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

 

resources/templates/converter-view.html

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

컨버터를 이용한 해당 타임리프 문법은 Form 에도 적용이 가능하다.

 

resources/templates/converter-form.html

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

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

</body>
</html>

 

 

결과

th:value를 사용하면 컨버터가 적용되지 않고 toString() 형태로 출력된다.

타임리프의 th:field 는 앞서 설명했듯이 id , name 를 출력하는 등 다양한 기능이 있는데, 여기에 컨버전 서비스도 함께 적용된다.

 


정리

스프링 타입 컨버터에 대해 알아보았다. Converter<>를 통해 직접 컨버터를 만들고 등록, 적용도 해보았다.

개별 컨버터를 모아두고 이를 묶어서 편리하게 사용할 수 있는 기능 ConversionService 에 대해서도 알아보았다.

다음 시간에는 컨버전서비스에 대한 내용을 추가하면서, Formatter 포맷터 에 대해서 설명하겠다.

 

728x90
반응형