[Spring] 스프링 타입 컨버터

hi·2022년 12월 18일
@GetMapping("/hello-v2")
public String helloV2(@RequestParam Integer data) {
	System.out.println("data = " + data);
	return "ok";
}

@ModelAttribute UserData data	
class UserData {
	Integer data;
}

/users/{userId}
@PathVariable("userId") Integer data

위 예시들을 보면 문자를 숫자로 편하게 받을 수 있다
이는 스프링이 중간에서 타입을 변환해주기 때문이다

스프링의 타입 변환 적용 ex)

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

직접 새로운 타입을 만들어 변환하고 싶다면?


컨버터 인터페이스

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

스프링은 확장 가능한 컨버터 인터페이스를 제공한다

  • 이 인터페이스를 구현, 등록하여 사용
  • 모든 타입에 적용 가능

타입 컨버터 Converter

org.springframework.core.convert.converter.Converter 인터페이스를 구현

ex) 문자 -> 숫자 변환

@Slf4j
public class StringToIntegerConverter implements Converter<String, Integer> {
	@Override
	public Integer convert(String source) {
		log.info("convert source={}", source);
		return Integer.valueOf(source);
 	}
}

용도에 따른 다양한 방식의 타입 컨버터

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

  • 스프링은 문자, 숫자, 불린, Enum등 일반적인 타입에 대한 대부분의 컨버터를 기본으로 제공

참고
https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#core-convert


컨버전 서비스 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);
}
  • 컨버팅이 가능한지 확인하는 기능과
    컨버팅 기능을 제공

사용 예시

public class ConversionServiceTest {

    @Test
    void conversionService() {
        //등록
        DefaultConversionService conversionService = new DefaultConversionService();
        conversionService.addConverter(new StringToIntegerConverter());
        conversionService.addConverter(new IntegerToStringConverter());
        conversionService.addConverter(new StringToIpPortConverter());
        conversionService.addConverter(new IpPortToStringConverter());

        //사용
        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));

        String ipPortString = conversionService.convert(new IpPort("127.0.0.1", 8080), String.class);
        assertThat(ipPortString).isEqualTo("127.0.0.1:8080");
    }
}
  • 컨버터 등록시 StringToIntegerConverter 같은 타입 컨버터를 명확하게 알아야 한다
  • 사용시에는 몰라도 된다. 타입 컨버터들은 모두 컨버전 서비스 내부에 숨어서 제공

인터페이스 분리 원칙 - ISP(Interface Segregation Principle)

: 클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 함

  • DefaultConversionService는 컨버터 사용, 등록에 각각 초점을 맞추어 인터페이스를 분리
  • 즉 클라이언트의 관심사를 명확하게 분리하였고, 꼭 필요한 메서드만 알게됨

ISP 참고
https://ko.wikipedia.org/wiki/%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4_%EB%B6%84%EB%A6%AC_%EC%9B%90%EC%B9%99

Converter 적용

  • 스프링은 내부에서 ConversionService를 제공
  • WebMvcConfigurer가 제공하는 addFormatters() 로 컨버터 등록
@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());
    }
}

💡 컨버터를 등록하지 않아도 스프링 내부에서 기본 컨버터를 제공

뷰 템플릿에 적용

${{...}} , th:field
컨버전 서비스 자동 적용


Formatter

Converter

  • 범용 (객체->객체)

Formatter

  • 문자에 특화 (객체->문자, 문자->객체) + 현지화 (Locale)
  • Converter의 특별한 버전

Formatter 인터페이스

Formatter는 객체를 문자로 변경하고, 문자를 객체로 변경하는 두 가지 기능을 모두 수행

public interface Printer<T> {
	String print(T object, Locale locale);
}
  
public interface Parser<T> {
	T parse(String text, Locale locale) throws ParseException;
}

public interface Formatter<T> extends Printer<T>, Parser<T> {
}

String print(T object, Locale locale) : 객체를 문자로 변경
T parse(String text, Locale locale) : 문자를 객체로 변경

  • 스프링은 용도에 따라 다양한 방식의 포맷터를 제공
    Formatter 포맷터
    AnnotationFormatterFactory 필드 타입, 애노테이션 정보를 활용할 수 있는 포맷터

참고
https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#format

Formatter 지원 컨버전 서비스

FormattingConversionService : 포맷터를 지원하는 컨버전 서비스
DefaultFormattingConversionService : FormattingConversionService 에 기본적인 통화, 숫자 관련 몇가지 기본 포맷터를 추가 제공

DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(); 

//컨버터 등록
conversionService.addConverter(new StringToIpPortConverter());
conversionService.addConverter(new IpPortToStringConverter());

//포맷터 등록
conversionService.addFormatter(new MyNumberFormatter());

//컨버터 사용
IpPort ipPort = conversionService.convert("127.0.0.1:8080", IpPort.class);
assertThat(ipPort).isEqualTo(new IpPort("127.0.0.1", 8080));

//포맷터 사용
assertThat(conversionService.convert(1000, String.class)).isEqualTo("1,000");
assertThat(conversionService.convert("1,000", Long.class)).isEqualTo(1000L);
  • FormattingConversionService는 ConversionService 관련 기능을 상속 받아 컨버터, 포맷터 모두 등록 가능. 사용할 때는 ConversionService 가 제공하는 convert
    를 사용

  • 스프링 부트는 DefaultFormattingConversionService 를 상속 받은 WebConversionService를 내부에서 사용


포맷터 적용

@Configuration
public class WebConfig implements WebMvcConfigurer { 

    @Override
    public void addFormatters(FormatterRegistry registry) { 
        registry.addFormatter(new MyNumberFormatter());
    }
}
  • 컨버터 기능이 있기 때문에 다른 컨버터가 등록되어 있다면 주석처리
  • 우선순위 컨버터 > 포맷터

스프링 제공 기본 Formatter

@NumberFormat

  • 숫자 관련 형식 지정 포맷터 사용
  • NumberFormatAnnotationFormatterFactory

@DateTimeFormat

  • 날짜 관련 형식 지정 포맷터 사용
  • Jsr310DateTimeFormatAnnotationFormatterFactory
static class Form {

	@NumberFormat(pattern = "###,###")
	private Integer number;
 
 	@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
 	private LocalDateTime localDateTime;
}

참고
https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#formatCustomFormatAnnotations


🔎 주의

  • 메시지 컨버터(HttpMessageConverter)에는 컨버전 서비스가 적용되지 않음
  • JSON 결과로 만들어지는 숫자나 날짜 포맷을 변경하고 싶으면 해당 라이브러리가 제공하는 설정을 통해서 포맷 지정
  • 컨버전 서비스는 @RequestParam , @ModelAttribute , @PathVariable , 뷰 템플릿 등에서 사용 가능

0개의 댓글