스프링 부트 3.2.1 버전을 기준으로 작성됨
클라이언트가 값을 보내면 JSON or String 등으로 값이 넘어가는데
@RequestParam
이나 @ModelAttribute
, @PathVariable
을 사용할 때 자동으로 타입을 변환해주는 걸로 알고 있다. 그렇다면 어디서 어떻게 변환되어 파라미터로 들어오는 것일까?
결과적으로 말하면 스프링이 중간에 타입을 변환해준다.
스프링의 타입 변환 적용 예
package org.springframework.core.convert.converter;
// S : 변환할 타입, T : 변환할 타입
public interface Converter<S, T> {
T convert(S source);
}
스프링은 확장 가능한 컨버터 인터페이스 제공
스프링에 추가적인 타입 변환이 필요하면 이 컨버터를 구현해서 등록하면 된다.
❗ 주의
해당 인터페이스를 사용해야함
org.springframework.core.convert.converter.Converter
@Slf4j
public class StringToIntegerConverter implements Converter<String, Integer> {
@Override
public Integer convert(String source) {
log.info("convert source={}", source);
return Integer.valueOf(source);
}
}
IP, Port를 가지는 Ip 객체
@Getter
@EqualsAndHashCode
public class IpPort {
private String ip;
private int port;
public IpPort(String ip, int port) {
this.ip = ip;
this.port = port;
}
}
Ip/Port 객체 <-> String
// Iport -> String
@Override
public String convert(IpPort source) {
log.info("convert source={}", source);
// IpPort 객체 -> "127.0.0.1:8080"
return source.getIp() + ":" + source.getPort();
}
// String -> IPort
@Override
public IpPort convert(String source) {
log.info("convert source={}", source);
// "127.0.0.1:8080"
String[] split = source.split(":");
String ip = split[0];
int port = Integer.parseInt(split[1]);
return new IpPort(ip, port);
}
스프링이 제공하는 다양한 방식의 타입 컨버터
Converter
: 기본 타입 컨버터
ConverterFactory
: 전체 클래스 계층 구조가 필요할 때
GenericConverter
: 정교한 구현, 대상 필드의 애노테이션 정보 사용 가능
ConditionalGenericConverter
: 특정 조건이 참인 경우에만 실행
타입 컨버터를 직접 찾아서 타입 변환에 사용하는 것은 매우 불편
스프링은 개별 컨버터를 모아두고 그것들을 묶어서 편리하게 사용할 수 있는 기능 제공
package org.springframework.core.convert;
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);
}
package org.springframework.core.convert.converter;
public interface ConverterRegistry {
// 컨버터 등록
void addConverter(Converter<?, ?> converter);
// 생략.. 자세한건 찾아보자
}
public class ConversionServiceTest {
@Test
void conversionService() {
// 등록
DefaultConversionService conversionService = new DefaultConversionService();
conversionService.addConverter(new StringToIntegerConverter());
conversionService.addConverter(new IntegerToStringConverter());
conversionService.addConverter(new IpPortToStringConverter());
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);
String ipPortString = conversionService.convert(new IpPort("127.0.0.1", 8080), String.class);
}
}
DefaultConversionService
는 ISP원칙을 지켰다
ConversionService
: 컨버터 사용에 초점 ConverterRegistry
: 컨버터 등록에 초점개발 입장에서는 등록 및 관리하는 ConverterRegistry
만 의존
클라이언트 입장에서는 컨버터를 사용하는 ConversionService
만 의존
컨버터를 사용하는 클라이언트는 꼭 필요한 메서드만 알게된다. -> ISP (인터페이스 분리 원칙)
스프링은 내부에서 CovnersionService
를 사용해서 타입 변환함 -> @RequestParam
웹 애플리케이션에 Converter
적용
@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());
}
}
WebMvcConfigurer
가 제공하는 addFormatters()
를 사용해서 추가하고 싶은 컨버터를 등록
-> 스프링 내부에서 사용하는 ConversionService
에 컨버터를 추가
public String helloV2(@RequestParam Integer data) {
System.out.println("data = " + data);
return "ok";
}
실행시 로그에는
StringToIntegerConverter : convert source=10 //우리가 만든 컨버터에서 출력한 로그
data = 10 이 출력된다.
@RequestParam
은 @RequestParam
을 처리하는 ArgumentResolver
인
RequestParamMethodArgumentResolver
에서 ConversionService
를 사용해서 타입을 변환
뷰 템플릿에 경우 핵심 정리해놓았다.
model
에 객체나 각종 데이터를 등록
${{...}}
를 사용하면 컨버전 서비스가 적용된다.
th:field
를 사용하는데 여기에도 컨버전 서비스가 적용됨
문자에 특화(객체 문자, 문자 객체) + 현지화(Locale)
실상황에서 대부분 문자 <-> 다른 타입
이런 경우가 많기에
문자 즉 String
에 특화된 컨버터인 포맷터를 사용해보자
날짜 숫자의 표현 방법은 Locale
현지화 정보 사용
// T 타입 객체를 문자로 변환
public interface Printer<T> {
String print(T object, Locale locale);
}
// String -> Locale을 반영하여 T 타입으로 변환
public interface Parser<T> {
T parse(String text, Locale locale) throws ParseException;
}
public interface Formatter<T> extends Printer<T>, Parser<T> {
}
1000 -> "1,000" 구현
@Slf4j
public class MyNumberFormatter implements Formatter<Number> {
@Override
public Number parse(String text, Locale locale) throws ParseException {
log.info("text={} locale={}", text, locale);
//"1,000" -> 1000
NumberFormat format = NumberFormat.getInstance(locale);
return format.parse(text);
}
@Override
public String print(Number object, Locale locale) {
log.info("object={} locale={}", object, locale);
return NumberFormat.getInstance(locale).format(object);
}
}
숫자 중간에 쉼표를 적용 -> NumberFormat
객체 사용
Locale
정보를 활용해서 나라별로 다른 숫자 포맷을 생성
FormattingConversionService
는 포맷터를 지원하는 컨버전 서비스이다.
DefaultFormattingConversionService
는 FormattingConversionService
에 기본적인 통화, 숫자 관련 몇가지 기본 포맷터를 추가 제공
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
//컨버터 등록
conversionService.addConverter(new StringToIpPortConverter());
//포맷터 등록
conversionService.addFormatter(new MyNumberFormatter());
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
//String <-> Integer 는 우선순위를 위해 삭제
registry.addConverter(new StringToIpPortConverter());
registry.addConverter(new IpPortToStringConverter());
// 추가
registry.addFormatter(new MyNumberFormatter());
}
}
추가로 스프링부트는 DefaultFormattingConversionService
를 상속받은 WebConversionService
를 내부에서 사용한다.
포맷터는 기본 형식이 지정되어있기에 객체의 각 필드마다 다른 형식 포맷 지정은 어려움
스프링은 이를 해결하기 위해 애노테이션 기반 형식 지정 포맷터를 두 가지 제공
@NumberFormat
: 숫자 관련 형식 지정 포맷터 사용 , NumberFormatAnnotationFormatterFactory
@DateTimeFormat
: 날짜 관련 형식 지정 포맷터 사용,Jsr310DateTimeFormatAnnotationFormatterFactory
@Controller
public class FormatterController {
@GetMapping("/formatter/edit")
public String fromatterForm(Model model) {
Form form = new Form();
form.setNumber(10000);
form.setLocalDateTime(LocalDateTime.now());
model.addAttribute("form", form);
return "formatter-form";
}
@PostMapping("/formatter/edit")
public String formatterEdit(@ModelAttribute Form form) {
return "formatter-view";
}
@Data
static class Form {
@NumberFormat(pattern = "###,###")
private Integer number;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime localDateTime;
}
}
form으로 데이터를 보내면
결과
메시지 컨버터 (HttpMessageConverter) 에는 컨버전 서비스가 적용되지 않는다.
객체 -> Json으로 변환할 때 메시지 컨버터는 Jackson 같은 라이브러리를 사용한다. 따라서 해당 라이브러리가 제공하는 설정을 통해 포맷을 지정해야 한다.
컨버전 서비스는 @RequestParam
, @ModelAttribute
, @PathVariable
, 뷰 템플릿 등에서 사용 가능
🔖 학습내용 출처