스프링 타입 컨버터 소개
문자↔숫자 변환과 같이 애플리케이션을 개발하다 보면 타입을 변환해야 하는 경우가 상당히 많다.
간단한 예시를 보자.
@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 포맷터 에 대해서 설명하겠다.
'Spring' 카테고리의 다른 글
Spring, Servlet - 파일 업로드(비디오, 사진 데이터 등) (1) | 2024.01.08 |
---|---|
Spring - Formatter, ConversionService (0) | 2024.01.02 |
Spring - API 예외 처리 (ExceptionHandler, ControllerAdvice) (2) | 2024.01.01 |
Spring - API 예외 처리 (HandlerExceptionResolver, ExceptionResolver ) 이론 (2) | 2024.01.01 |
Spring(스프링) - 서블릿 예외 처리 및 오류 페이지(필터, 인터셉터) (1) | 2023.12.29 |