스프링 타입 컨버터
개요
- 애플리케이션을 개발하다보면, 타입을 변환해야 하는 경우가 상당히 많다.
- 스프링은 “숫자→문자”, “문자→숫자” 변환 등의 기초적인 타입변환을 자체적으로 지원한다.
@RequestParam
,@ModelAttribute
,@PathVariable
등을 통해, 요청값을 바인딩할 때
- 이런 기초적인 타입변환 대신, 개발자가 새로운 타입을 만들어서 변환하고 싶을 때 타입 컨버터를 직접 구현하면 된다.
타입 컨버터
Converter
인터페이스
package org.springframework.core.convert.converter;
public interface Converter<S, T> {
T convert(S source);
}
- 스프링에 추가적인 타입 변환이 필요하면, 해당 인터페이스를 구현해서 등록하면 된다.
implements Converter<원타입, 변환될_타입>
컨버전 서비스
컨버전 서비스란?
Converter
를 통해 구현한 타입 변환 클래스(컨버터)를 하나씩 찾아서 사용하기엔 불편하다.- 스프링은 컨버전 서비스를 통해, 개별 컨버터를 모아두고 그것들을 묶어서 사용할 수 있는 기능을 제공한다.
-
스프링은 내부에서
ConversionService
을 제공한다.자세한 내용은 이후에 설명한다.
ConversionService
인터페이스
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);
}
canConvert()
- 컨버팅이 가능한지 확인하는 메서드
convert()
- 컨버팅 메서드
ConversionService
구현체: DefaultConversionService
클래스
- 스프링이
ConversionService
인터페이스를 구현한DefaultConversionService
를 제공한다.
DefaultConversionService
를 직접 사용하는 예시는 아래 내용 참고
Converter
인터페이스 구현과 (직접)등록 예시
- 변환 목표
- IpPort 객체 → 문자열
- 문자열 → IpPort 객체
-
IpPort
객체public class IpPort { private String ip; private int port; public IpPort(String ip, int port) { this.ip = ip; this.port = port; } public String getIp() { return ip; } public int getPort() { return port; } /** * 테스트 코드에서 값을 비교하기 위한 메서드들 */ @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; IpPort ipPort = (IpPort) o; return port == ipPort.port && Objects.equals(ip, ipPort.ip); } @Override public int hashCode() { return Objects.hash(ip, port); } }
-
IpPort→String
변환 구현:IpPortToStringConverter
클래스import org.springframework.core.convert.converter.Converter; public class IpPortToStringConverter implements Converter<IpPort, String> { @Override public String convert(IpPort source) { String ip = source.getIp(); String port = String.valueOf(source.getPort()); return ip + ":" + port; } }
-
String→IpPort
변환 구현:StringToIpPortConverter
클래스import org.springframework.core.convert.converter.Converter; public class StringToIpPortConverter implements Converter<String, IpPort> { @Override public IpPort convert(String source) { String[] ipAndPort = source.split(":"); IpPort ipPort = new IpPort(ipAndPort[0], Integer.parseInt(ipAndPort[1])); return ipPort; } }
-
컨버전 서비스 직접 사용 테스트코드:
ConversionServiceTest
클래스import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.core.convert.support.DefaultConversionService; public class ConversionServiceTest { @Test void conversionService() { //스프링이 제공하는 ConversionService 구현체 DefaultConversionService conversionService = new DefaultConversionService(); //개발자가 직접 구현한 컨버터 등록 conversionService.addConverter(new IpPortToStringConverter()); conversionService.addConverter(new StringToIpPortConverter()); Assertions.assertThat(conversionService.convert("127.0.0.1:8080", IpPort.class)) .isEqualTo(new IpPort("127.0.0.1", 8080)); Assertions.assertThat(conversionService.convert(new IpPort("127.0.0.1", 80), String.class)) .isEqualTo("127.0.0.1:80"); } }
컨버전 서비스의 특징
- 타입 변환을 원하는 사용자(개발자)는 컨버전 서비스 인터페이스에만 의존하면 된다.
- 컨버터 등록: 타입 컨버터 클래스를 명확하게 알아야 등록할 수 있다.
- 컨버터 사용: 어떤 타입 컨버터 클래스가 사용되는지 알 필요가 없다. 단지, “어떤 타입을 어떤 타입으로 변환할 것인가?” 에만 집중하면 된다.
- 따라서, 사용자가 어떤 타입 컨버터 클래스가 사용되는지 몰라도 타입변환을 할 수 있도록, 컨버전 서비스 인터페이스가 지원한다.
- 인터페이스 분리 원리 - ISP (Interface Segregation Principal)
DefaultConversionService
는 다음 두 인터페이스를 구현했다.ConversionService
인터페이스: 컨버터 사용에 초점ConversionRegistry
인터페이스: 컨버터 등록에 초점
- 이렇게 인터페이스를 분리하면 컨버터를 사용하는 클라이언트와 컨버터를 등록하고 관리하는 클라이언트의 관심사를 명확하게 분리할 수 있다.
- 특히, 컨버터를 사용하는 입장에선
ConversionService
인터페이스만 의존하면 되므로, 컨버터를 어떻게 등록하고 관리하는지는 전혀 몰라도 된다.
스프링에 Converter 적용하기
컨버터 등록: WebConfig
클래스
import ...
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 컨버터 등록
*/
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new IpPortToStringConverter());
registry.addConverter(new StringToIpPortConverter());
}
}
- 스프링은 내부에서
ConversionService
을 제공하므로,WebMvcConfigurer
가 제공하는addFormatters()
를 사용해서 추가하고 싶은 컨버터를 등록하면 된다.- 이렇게하면 스프링은 내부에서 사용하는
ConversionService
에 컨버터를 추가해준다.
- 이렇게하면 스프링은 내부에서 사용하는
컨트롤러: ConversionController
클래스
import ...
@RestController
public class ConversionController {
@GetMapping("/StringToIpPort")
public IpPort StringToIpPort(@RequestParam IpPort ipPort) {
return ipPort;
}
}
요청 결과
-
요청
-
결과
- 쿼리 스트링
ipPort=127.0.0.1:66
이 IpPort 타입으로 잘 변환되었다.
- 쿼리 스트링
처리 과정
@RequestParam
ArgumentResolver
중 해당 애너테이션을 처리하는RequestParamMethodArgumentResolver
가 처리한다.
RequestParamMethodArgumentResolver
에서ConversionService
를 사용해서 타입을 변환한다.
뷰 템플릿에 컨버터 적용
- 타임리프에서
${ {...} }
을 사용하면, 컨버터를 사용할 수 있다. - 예시 코드로 알아보자.
뷰 호출 컨트롤러: ConverterController
클래스
import ...
@Controller
public class ConversionController {
@GetMapping("/converterView")
public String converterView(Model model) {
model.addAttribute("ipPort", new IpPort("127.0.0.1", 66));
return "converter";
}
}
뷰 템플릿: converter.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!-- ${ {...} } 문법 적용 -->
<p th:text="${ {ipPort} }">ipPort</p>
</body>
</html>
호출결과
IpPort
객체를String
으로 변환해야하므로,IpPortToStringConverter
컨버터가 적용되었다.
뷰 템플릿 폼에 컨버터 적용
- 타임리프 폼에서
th:field
을 사용하면, 컨버터를 사용할 수 있다. - 예시 코드로 알아보자.
뷰 호출 컨트롤러: ConverterController
클래스
import ...
@Controller
public class ConversionController {
@GetMapping("/edit")
public String converterForm(Model model) {
model.addAttribute("form", new Form(new IpPort("127.0.0.1", 66)));
return "converterForm";
}
@PostMapping("/edit")
public String converterEdit(@ModelAttribute Form form, Model model) {
model.addAttribute(form.getIpPort());
return "converter";
}
@Data
static class Form {
private IpPort ipPort;
public Form(IpPort ipPort) {
this.ipPort = ipPort;
}
}
}
폼 뷰 템플릿: converterForm.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form th:object="${form}" method="post">
th:field = <input type="text" th:field="*{ipPort}"/>
th:value = <input type="text" th:value="*{ipPort}"/>
</form>
</body>
</html>
호출결과
th:field="*{ipPort}"
form
객체의 필드ipPort
에IpPortToStringConverter
컨버터를 적용하여 출력한다.
th:value="*{ipPort}"
form
객체의 필드ipPort
의 참조값을 그대로 출력한다. (컨버터 적용 X)
- 본 게시글은 김영한님의 강의를 토대로 정리한 글입니다.
- 더 자세한 내용을 알고 싶으신 분들이 계신다면, 해당 강의를 수강하시는 것을 추천드립니다.