개발을 하다 보면 타입을 변환해야 하거나 특정 포맷에 맞춰 변환해야 하는 경우가 상당히 많다.
특히 웹 개발 관점에서 보자면 HTTP를 통한 데이터 통신은 전부 문자열(String)으로 되어있기 때문에 숫자나 날짜 등의 입력이나 출력 시 변환이 필수적으로 들어가게 된다. 이렇게 필수적으로 들어가는 변환을 매번 직접 하기에는 상당히 귀찮고 번거로울 것이다.
그렇기에 Spring에서는 이러한 기능을 수행하는 Converter와 Formatter라는 것을 지원하고 있다.
해당 기능들은 다음과 같은 동작에서 적용되어 사용된다.
- Spring MVC request parameter
- @RequestParam
- @ModelAttribute
- @PathVariable
- 설정 파일의 값 (properties, yml 등)
- @Value
- XML내 스프링 빈 정보
- View 렌더링 과정
간단한 예로 @RequestParam으로 Integer 타입을 받아오는 경우를 생각해 보면 쉽게 이해할 수 있을 것이다. 파라미터로 넘어오는 값은 원래 String 타입인데 Converter가 동작하여 Interger 타입으로 변환해주기 때문에 별다른 변환 작업을 수행하지 않고 곧바로 Integer 타입으로 사용이 가능한 것이다.
그렇다면 어디에서 어떻게 쓰이는지 알았으니 이제 Converter와 Formatter에 대해 좀 더 자세히 알아보도록 하겠다.
Converter
컨버터의 기능을 간단하게 요약하자면 "객체" -> "객체"로의 변환이라고 할 수 있다. 범용성 높은 변환 기능을 제공한다고 보면 된다.
위에서 언급한 @RequestParam 예제를 통해 알 수 있듯 Spring에서는 이미 문자, 숫자, boolean, Enum 등 여러 개의 Converter를 만들어 제공해 주고 있기에 편하게 사용이 가능하며, 사용자 정의 컨버터도 지원을 하고 있으므로 사용자 정의 컨버터를 구현하고 싶다면 Spring에서 제공하는 "org.springframework.core.convert.converter" 패키지에 정의되어 있는 "Converter<S, T>" 인터페이스를 구현하면 된다.
참고로 기본적인 Converter 인터페이스뿐 아니라 여러 특정 상황에 사용할 수 있는 Converter 인터페이스를 제공하고 있다. 이러한 Converter 인터페이스에 대해선 해당 글에서는 다루지 않으므로 자세한 내용은 공식 문서를 참고하는 것을 추천한다. (공식 문서 링크)
사용자 정의 컨버터 구현에 대해선 간단한 예제 코드로 알아보록 하겠다.
// 변환 테스트를 위한 클래스
import lombok.Data;
@Data
public class TestForm {
private String string;
private Integer integer;
public TestForm(String string, Integer integer) {
this.string = string;
this.integer = integer;
}
}
----
// 사용자 정의 컨버터 구현, 문자열에서 "_"을 기준으로 문자열과 숫자로 변환하는 사용자 정의 컨버터
import org.springframework.core.convert.converter.Converter;
public class StringToTestConverter implements Converter<String, TestForm> {
@Override
public TestForm convert(String source) {
// "Hello_123123" -> "Hello", 123123 변환
String[] split = source.split("_");
return new TestForm(split[0], Integer.valueOf(split[1]));
}
}
----
// 사용자 정의 컨버터 구현, 문자열에서 "_"을 기준으로 문자열과 숫자로 변환하는 사용자 정의 컨버터
import org.springframework.core.convert.converter.Converter;
public class TestConverterToString implements Converter<String, TestForm> {
@Override
public TestForm convert(String source) {
// "Hello", 123123 -> "Hello_123123" 변환
return source.getString() + "_" + source.getInteger();
}
}
이후 해당 컨버터를 new 연산자를 통해 생성 후 convert() 메서드를 호출하여 사용할 수 있으나 이러면 굳이 컨버터로 구현을 할 필요가 없을 것이다. 따라서 이러한 컨버터들을 모아서 쉽게 사용하도록 지원해 주는 기능이 필요하게 될 텐데 그것이 바로 ConversionService 인터페이스다.
ConversionService Interface 코드
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);
@Nullable
<T> T convert(@Nullable Object source, Class<T> targetType);
@Nullable
Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
}
해당 인터페이스는 변환이 가능한지 확인하는 기능과 변환하는 기능을 제공하고 있으며, Spring에서는 DefaultConversionService라는 ConversionServcie 구현체를 제공한다. 그렇기에 해당 구현체에 사용자 정의 컨버터를 등록하여 사용하면 된다.
// 컨버전 서비스 생성
DefaultConversionService conversionService = new DefaultConversionService();
// 컨버터 등록
conversionService.addConverter(new StringToTestConverter());
conversionService.addConverter(new TestConverterToString());
// 컨버터 사용
TestForm testForm = conversionService.convert("Test String_12321", TestForm.class);
String string = conversionService.convert(new TestForm("Hello converter", 22331), String.class);
이렇게 컨버전 서비스를 사용하게 된다면 어딘가에서 컨버터들을 컨버전 서비스에 등록을 해두고 필요한 곳에서는 컨버전 서비스를 주입 받아서 사용할 수 있게 되므로써 사용하는 측에서는 어떠한 컨버터가 있는지 모르더라도 쉽게 사용할 수 있게 된다.
참고로 DefaultConversionService에 ConversionService 인터페이스에는 없는 addConverter() 메서드가 존재하는 이유는 ConversionService 인터페이스와 더불어 ConversionRegistry 인터페이스도 함께 구현한 구현체이기 때문이다. 이는 ISP를 잘 반영하여 등록(ConversionService)과 사용(ConversionRegistry)으로 나누었으며, 이를 하나의 구현체에서 구현했다고 보면 된다.
위 방법처럼 따로 컨버전 서비스를 만들어 사용하는 방법도 있겠으나, 대부분 Spring에서 사용하는 컨버전 서비스에 등록하여 사용하는 경우가 많을 것 이기에 Spring이 사용하는 컨버전 서비스에 등록하는 방법에 대해서 예제 코드로 알아보도록 하겠다.
package hello.typeconverter.converter;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToTestConverter());
registry.addConverter(new TestConverterToString());
}
}
간단하게 WebMvcConfigure 인터페이스의 addFormatters() 메서드를 오버라이드 하여 컨버터를 등록하게 되면 스프링 컨버전 서비스에 등록되어 글 어미에 언급했던 동작들에 반영된다. 당연하게도 등록한 컨버터가 기본적으로 제공되는 컨버터에 비해 우선순위가 높다.
Formatter
포맷터는 문자열 변환에 특화된 컨버터라고 보면 이해하기 편하다.
생각을 해보면 문자열에서 객체로의 변환 또는 객체에서 문자열로의 변환이 대다수라는 것을 알 수 있을 것이다. 그렇기에 해당 기능에 특화된 포맷터가 등장을 하였다고 보면 된다.
포맷터의 경우 이름 그대로 특정 포맷에 대한 정보와 Locale에 대한 정보를 변환에 사용할 수 있도록 되어있다. 간단하게 예를 들자면 "한국"에서 숫자를 표현할 때 "1,000,000" 처럼 3자리 마다 쉼표를 추가하여 표현하는데 타 국가에서는 다르게 표현하는 경우도 존재한다. 즉, 특정 지역(Locale)에서 사용하는 형식(format)에 맞춰 문자열로 변환하는 기능이라고 보면 된다. (숫자 1000000을 문자열 "1,000,000"으로 변환 또는 반대의 경우)
이러한 포맷터는 "Formatter<T>" 인터페이스를 구현하여 사용자 정의 포맷터를 만들 수 있으며 컨버터와 마찬가지로 컨버전 서비스에 등록하여 사용하면 된다. 포맷터의 경우 컨버전 서비스의 구현체로 "DefaultFormattingConversionService"를 사용한다. 해당 구현체는 컨버터와 포맷터 둘 다 등록할 수 있는 구현체이며 각각 "addConverter()", "addFormatter()" 메서드로 등록할 수 있다. 등록한 컨버터 및 포맷터를 사용하는 방법은 "convert()" 메서드 하나로 사용하면 된다. (컨버터와 포맷터의 우선순위는 컨버터가 더 높다.)
사용자 정의 포맷터를 Spring에 등록하여 사용하려면 아래와 같은 방법을 사용하면 된다.
package hello.typeconverter.converter;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
// 컨버터 등록과 동일한데 메서드만 다르다. addConverter() -> addFormatter()
registry.addFormatter(new CustomFommater());
}
}
참고 사항
컨버터와 포맷터는 메시지 컨버터(HttpMessageConverter)에는 적용되지 않는다. 메시지 컨버터는 HTTP body의 내용을 객체로 변환하거나 객체를 HTTP body에 입력하는 역할이다. 이러한 역할을 수행하는 대표적인 라이브러리로 JSON으로 변환을 시켜주는 Jackson 등이 있다. 이러한 메시지 컨버터 기능에 변환이 필요하다면 사용하는 라이브러리에서 제공하는 기능을 사용해야 한다. (Jackson data format 등으로 검색)
결론은 메시지 컨버터(HttpMessageConverter)와 컨버전 서비스(ConversionService)는 전혀 상관없는 기능이므로 JSON 변환 시 컨버전 서비스가 동작 안 하는 것은 버그가 아니다.
'Develop > TIL(Today I Learned)' 카테고리의 다른 글
JPA에 대한 끄적임 (0) | 2023.03.08 |
---|---|
Spring - 클래스 초기화 방법 비교 (PostConstruct, EventListener) (0) | 2023.03.06 |
Spring MVC - 예외 처리(API) (0) | 2023.02.23 |
Spring MVC - 예외 처리(Error page) (0) | 2023.02.20 |
Servlet Filter & Spring Interceptor (0) | 2023.02.20 |