문자를 숫자로 변환하거나, 반대로 숫자를 문자로 변환해야 하는 것 처럼 애플리케이션을 개발하다 보면 타입을 변환해야 하는 경우가 상당히 많다.
HTTP 쿼리 스트링으로 전달하는 data=10 부분에서 10은 숫자 10이 아니라 문자 10이다.
스프링이 제공하는 @RequestParam 을 사용하면 이 문자 10을 Integer 타입의 숫자 10으로 편리하게 받을 수 있다.
@GetMapping("/hello-v2")
public String helloV2(@RequestParam Integer data) {
System.out.println("data = " + data);
return "ok";
}
이것은 스프링이 중간에서 타입을 변환해주었기 때문이다.
이러한 예는 @ModelAttribute , @PathVariable 에서도 확인할 수 있다
@ModelAttribute 타입 변환 예시
@ModelAttribute UserData data
class UserData {
Integer data;
}
@PathVariable 타입 변환 예시
/users/{userId}
@PathVariable("userId") Integer data
타입을 변환해야 하는 경우는 상당히 많다. 개발자가 직접 하나하나 타입 변환을 해야 한다면, 코드가 너무 늘어나고, 괴로울 것이다.
스프링이 중간에 타입 변환기를 사용해서 타입을 String Integer 로 변환해주었기 때문에 개발자는 편리하게 해당 타입을 바로 받을 수 있다. 앞에서는 문자를 숫자로 변경하는 예시를 들었지만, 반대로 숫자를 문자로 변경하는 것도 가능하고, Boolean 타입을 숫자로 변경하는 것도 가능하다.
만약 개발자가 새로운 타입을 만들어서 변환하고 싶으면 어떻게 하면 될까?
컨버터 인터페이스
package org.springframework.core.convert.converter;
public interface Converter<S, T> {
T convert(S source);
}
스프링은 확장 가능한 컨버터 인터페이스를 제공한다.
개발자는 스프링에 추가적인 타입 변환이 필요하면 이 컨버터 인터페이스를 구현해서 등록하면 된다. 이 컨버터 인터페이스는 모든 타입에 적용할 수 있다. 필요하면 X -> Y 타입으로 변환하는 컨버터 인터페이스를 만들고, 또 Y -> X 타입으로 변환하는 컨버터 인터페이스를 만들어서 등록하면 된다.
//String -> Integer
public class StringToIntegerConverter implements Converter<String, Integer> {
@Override
public Integer convert(String source) {
return Integer.valueOf(source);
}
}
//Integer -> String
public class IntegerToStringConverter implements Converter<Integer, String> {
@Override
public String convert(Integer source) {
return String.valueOf(source);
}
}
위의 예제는 실제 잘 동작한다. String을 Integer로 또 그 반대의 변환도 잘 일어났다. String을 객체로 변환할 수 도 있다. 또는 객체를 String으로 변환도 가능할 것이다.
그런데 이렇게 타입 컨버터를 하나하나 직접 사용하면, 개발자가 직접 컨버팅 하는 것과 큰 차이가 없다. 타입 컨버터를 등록하고 관리하면서 편리하게 변환 기능을 제공하는 역할을 하는 무언가가 필요하다.
참고
스프링은 문자, 숫자, 불린, Enum등 일반적인 타입에 대한 대부분의 컨버터를 기본으로 제공한다. IDE에서 Converter , ConverterFactory , GenericConverter 의 구현체를 찾아보면 수 많은 컨버터를 확인할 수 있다.
타입 컨버터를 하나하나 직접 찾아서 타입 변환에 사용하는 것은 매우 불편하다. 그래서 스프링은
개별 컨버터를 모아두고 그것들을 묶어서 편리하게 사용할 수 있는 기능을 제공하는데, 이것이 바로 컨버전 서비스( 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);
}
컨버전 서비스 인터페이스는 단순히 컨버팅이 가능한가?(canConvert) 확인하는 기능과, 컨버팅 기능(convert)을 제공한다.
@Test
void conversionService() {
//등록
DefaultConversionService conversionService = new DefaultConversionService();
conversionService.addConverter(new StringToIntegerConverter());
conversionService.addConverter(new IntegerToStringConverter());
//사용
assertThat(conversionService.convert("10",Integer.class)).isEqualTo(10);
assertThat(conversionService.convert(10, String.class)).isEqualTo("10");
}
DefaultConversionService
는 ConversionService
인터페이스를 구현했는데, 추가로 컨버터를 등록하는 기능도 제공한다.등록과 사용 분리
컨버터를 등록할 때는 StringToIntegerConverter 같은 타입 컨버터를 명확하게 알아야 한다. 반면에 컨버터를 사용하는 입장에서는 타입 컨버터를 전혀 몰라도 된다. 타입 컨버터들은 모두 컨버전 서비스 내부에 숨어서 제공된다. 따라서 타입을 변환을 원하는 사용자는 컨버전 서비스 인터페이스에만 의존하면 된다. 물론 컨버전 서비스를 등록하는 부분과 사용하는 부분을 분리하고 의존관계 주입을 사용해야 한다
컨버전 서비스 사용
Integer value = conversionService.convert("10", Integer.class)
인터페이스 분리 원칙 - ISP(Interface Segregation Principle)
인터페이스 분리 원칙은 클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다.
DefaultConversionService
는 다음 두 인터페이스를 구현했다.
이렇게 인터페이스를 분리하면 컨버터를 사용하는 클라이언트와 컨버터를 등록하고 관리하는 클라이언트의 관심사를 명확하게 분리할 수 있다. 특히 컨버터를 사용하는 클라이언트는ConversionService 만 의존하면 되므로, 컨버터를 어떻게 등록하고 관리하는지는 전혀 몰라도 된다. 결과적으로 컨버터를 사용하는 클라이언트는 꼭 필요한 메서드만 알게된다. 이렇게 인터페이스를 분리하는 것을 ISP 라 한다
@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());
}
}
스프링은 내부에서 ConversionService
를 제공한다. 우리는 WebMvcConfigurer
가 제공하는 addFormatters()
를 사용해서 추가하고 싶은 컨버터를 등록하면 된다. 이렇게 하면 스프링은 내부에서 사용하는 ConversionService
에 컨버터를 추가해준다.
추가한 컨버터가 기본 컨퍼터보다 높은 우선순위를 가진다.
일반적인 기본타입 변환들은 이미 스프링에 대부분 추가되어 있다. 보통 컨버터를 사용할때는 우리가 만든 객체들을 변환시키거나 할때 사용.
타임리프는 렌더링 시에 컨버터를 적용해서 렌더링 하는 방법을 편리하게 지원한다.
타임리프는 ${{...}} 를 사용하면 자동으로 컨버전 서비스를 사용해서 변환된 결과를 출력해준다. 물론 스프링과 통합 되어서 스프링이 제공하는 컨버전 서비스를 사용하므로, 우리가 등록한 컨버터들을 사용할 수 있다.
변수 표현식 :${...}
컨버전 서비스 적용 : ${{...}}
<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>
결과
• ${number}: 10000
• ${{number}}: 10000
• ${ipPort}: hello.typeconverter.type.IpPort@59cb0946
• ${{ipPort}}: 127.0.0.1:8080
또 한 th:field
는 앞서 설명했듯이 id , name 를 출력하는 등 다양한 기능이 있는데, 여기에 컨버전 서비스도 함께 적용된다.
th:field = *{ipPort}
라는 것을 쓰면 value는 주소가 떠야될거 같지만 converter 기능이 적용되어 127.0.0.1:8080 이라는 값이 나와진다.
또 이 값을 submit했을시 converter가 있다면 @ModelAttribute는 컨버팅 해서 문자열을 객체로 만들어서 보내준다.
웹 애플리케이션에서 객체를 문자로, 문자를 객체로 변환하는 예
화면에 숫자를 출력해야 하는데, Integer String 출력 시점에 숫자 1000 문자 "1,000" 이렇게 1000 단위에 쉼표를 넣어서 출력하거나, 또는 "1,000" 라는 문자를 1000 이라는 숫자로 변경해야 한다.
날짜 객체를 문자인 "2021-01-01 10:50:11" 와 같이 출력하거나 또는 그 반대의 상황
객체를 특정한 포멧에 맞추어 문자로 출력하거나 또는 그 반대의 역할을 하는 것에 특화된 기능이
바로 포맷터( Formatter )이다. 포맷터는 컨버터의 특별한 버전으로 이해하면 된다.
Converter vs Formatter
스프링은 용도에 따라 다양한 Formatter를 지원해준다.
Formatter
포맷터
AnnotationFormatterFactory
필드의 타입이나 애노테이션 정보를 활용할 수 있는 포맷터
FormattingConversionService
는 포멧터를 지원하는 컨버전 서비스.
-> DefaultFormattingConversionService
는 기본적인 FormattingConversionService
에 통화,숫자 관련 및 몇가지 기본 포메터를 추가해서 제공.
스프링은 자바의 기본 타입들에 대해 많은 포멧터를 제공한다. 그런데 포멧터는 기본 형식이 지정되어 있기 때문에, 객체의 각필드마다 다른 형식으로 포멧을 지정하기는 어렵다.
이런 문제를 해결하기 위해 애노테이션 기반으로 원하는 형식을 지정해서 사용할 수 있는 매우 유용한 포멧터 두 가지를 기본으로 제공한다.
@NumberFormat
: 숫자 관련 형식 지정 포멧터 사용
@DateTimeFormat
: 날짜 관련 형식 지정 포멧터 사용
실제 구현은 어렵지만, 스프링이 제공하는 기능을 사용하는 것은 매우 간단하다.
@Data
static class Form {
@NumberFormat(pattern = "###,###")
private Integer number;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime localDateTime;
}
참고 : 본 글은 김영한님의 스프링 강의를 정리한 것입니다.