Search
Duplicate
📒

[Spring Study] 05-3. Converter, Formatter

상태
완료
수업
Spring Study
주제
Converter
4 more properties
참고

스프링 타입 컨버터

NOTE
스프링에서는 API를 통해 전달받은 문자열을 숫자로 변환하거나, 숫자를 문자열로 변환해야할 때가 많습니다. 스프링은 이러한 타입변환을 위해 다양한 Converter 구현체를 제공합니다.
대표적인 Converter 구현체 (@ResponseBody)
@Controller public class MyController { @InitBinder public void initBinder(WebDataBinder binder) { binder.registerCustomEditor(IpPort.class, new PropertyEditorSupport() { @Override public void setAsText(String text) throws IllegalArgumentException { setValue(new IpPort(text)); // IpPort는 String을 인자로 받는 생성자 필요 } }); } @RequestMapping("/custom") public String handleCustomObject(IpPort ipPort) { return "viewPage"; } }
Java
복사
Conver가 아닌 @InitBinder 어노테이션으로도 가능
public interface Converter<S, T> { T convert(S source); }
Java
복사
Converter 형태 (S → T)
@Getter @EqualsAndHashCode public class IpPort { private String ip; private int port; public IpPort(String ip, int port) { this.ip = ip; this.port = port; } }
Java
복사
public class StringToIpPortConverter implements Converter<String, IpPort> { @Override public IpPort convert(String source) { String[] split = source.split(":"); String ip = split[0]; int port = Integer.parseInt(split[1]); return new IpPort(ip, port); } }
Java
복사
String → IpPort
스프링의 Converter구현체의 종류는 다양하지만 대표적인 Converter는 다음과 같습니다.
기본 문자처리: StringHttpMessageConverter
기본 객체처리: MappingJackson2HttpMessageConverter
@RequestMapping, @PathVariable, @ModelAttribute와 같은 어노테이션도 대표적
스프링은 용도에 따라 다양한 방식의 타입 컨버터를 제공합니다.
Converter: 기본 타입 컨버터
ConverterFactory: 전체 클래스 계층 구조가 필요할 때
GenericConverter: 정교한 구현, 대상 필드의 애노테이션 정보 사용 가능
ConditionalGenerictConverter: 특정 조건이 참인 경우에만 실행

ConversionService 활용

NOTE
스프링의 ConversionService는 타입 변환을 체계적으로 관리할 수 있도록 돕는 인터페이스입니다. 이 서비스를 사용해 개별 컨버터들을 효율적으로 관리하고, 쉽게 호출할 수 있습니다.
DefaultConversionService는 2개의 인터페이스를 구현해 컨버터 사용과 등록이 가능하다.
ConversionService: 컨버터 사용에 초점
ConversionRegistry: 컨버터 등록에 초점
@Test void conversionService() { // 1. ConversioService 생성 DefaultConversionService conversionService = new DefaultConversionService(); // 2. Converter 등록 conversionService.addConverter(new StringToIpPortConverter()); conversionService.addConverter(new IpPortToStringConverter()); // 3. 변환 가능여부 확인 assertThat(conversionService.canConvert(String.class, IpPort.class)).isTrue(); assertThat(conversionService.canConvert(IpPort.class, String.class)).isTrue(); // 4. 사용 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(ipPort, String.class); assertThat(ipPortString).isEqualTo("127.0.0.1:8080"); }
Java
복사

스프링 컨버터 적용

NOTE
스프링에서는 @RequestParam, @PathVariable, @ModelAttribute와 같은 애노테이션과 함께 타입변환 기능을 자주 사용합니다.
@Configuration public class WebConfig implements WebMvcConfigurer { // 컨버터 등록 @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new IpPortToStringConverter()); registry.addConverter(new StringToIpPortConverter()); } }
Java
복사
@RestController public class TestController { // String -> IpPort로 자동변환됨! // ip-port?ipPort=127.0.0.1:8080 @GetMapping("/ip-port") public String ipPort(@RequestParam("ipPort") IpPort ipPort) { System.out.println("ipPort IP = " + ipPort.getIp()); System.out.println("ipPort PORT = " + ipPort.getPort()); return "ok"; } }
Java
복사

스프링 타입 포맷터

NOTE
스프링 Formatter 인터페이스는 Converter와 비슷하지만 문자에 특화되어 있고 Local을 활용한 국제화가 가능하다는 차이점이 있습니다.
public interface Formatter<T> extends Printer<T>, Parser<T> { // 객체 -> 문자 String print(T object, Locale locale); // 문자 -> 객체 T parse(String text, Locale locale) throws ParseException; }
Java
복사
Formatter 형태
public class MyNumberFormatter implements Formatter<Number> { // 숫자 -> 문자 변환 @Override public String print(Number object, Locale locale) { return NumberFormat.getInstance(locale).format(object); } // 문자 -> 숫자 변환 @Override public Number parse(String text, Locale locale) throws ParseException { return NumberFormat.getInstance(locale).parse(text); } }
Java
복사
@Test void parse() throws ParseException { Number result = formatter.parse("1,000", Locale.KOREA); assertThat(result).isEqualTo(1000L); } @Test void print() { String result = formatter.print(1000, Locale.KOREA); assertThat(result).isEqualTo("1,000"); }
Java
복사
String → IpPort
스프링은 애노테이션 기반으로 원하는 형식을 지정해서 사용할 수 있는 유용한 포맷터를 제공합니다.
@NumberFormat: 숫자 관련 형식 지정 포맷터 사용
@DateTimeFormat: 날짜 관련 형식 지정 포맷터 사용

ConversionService 활용

NOTE
Converter와 동일하게 DefaultFormattingConversionService를 통해 등록하여 사용할 수 있습니다.
@Test void conversionServiceWithFormatter() { // 1. FormattingConversionService 생성 DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(); // 2. 포맷터 등록(컨버터 등록도 가능) conversionService.addFormatter(new MyNumberFormatter()); // 3. 포맷터 사용 Number number = conversionService.convert("1,000", Number.class); assertThat(number).isEqualTo(1000); String formattedNumber = conversionService.convert(1000, String.class); assertThat(formattedNumber).isEqualTo("1,000"); }
Java
복사

Formatter와 Jackson

NOTE
Formatter는 JSON 직렬화 과정에 영향을 미치지 않습니다. 스프링의 데이터 바인딩 혹은 웹 form 제출에는 유효하지만 REST API응답에서 제대로 사용하기 위해선 Jackson 라이브러리가 제공하는 어노테이션을 사용해야 합니다.

@JsonFormat 사용

@JsonFormat은 Jackson 라이브러리가 제공하는 어노테이션으로 JSON 직렬화/역직렬화 시 날짜 및 숫자 형식을 지정하는데 사용할 수 있습니다.
@Data static class Form { @NumberFormat(pattern = "###,###") private Integer number; // 수정! @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime localDateTime; }
Java
복사
@NumberFormat의 경우 @JsonFormat이 적용되지 않으므로 다른 방법을 고려해야 합니다.

Jackson 사용자 정의 직렬화

Jackson의 사용자 정의 직렬화를 만들어서 사용할 수 있습니다.
public class CustomNumberSerializer extends JsonSerializer<Integer> { // 직렬화 구현 @Override public void serialize(Integer value, JsonGenerator gen, SerializerProvider serializers) throws IOException { NumberFormat formatter = NumberFormat.getInstance(); formatter.setGroupingUsed(true); formatter.setMinimumFractionDigits(0); formatter.setMaximumFractionDigits(0); String formattedNumber = formatter.format(value); gen.writeString(formattedNumber); } }
Java
복사
@Data static class Form { // 커스텀 직렬화 설정 @JsonSerialize(using = CustomNumberSerializer.class) private Integer number; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime localDateTime; }
Java
복사

덤프

package org.okestro.tps.api.infrastructure.config; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.okestro.tps.api.application.ticket.enums.TableEnum; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; import org.springframework.stereotype.Component; @Slf4j @Component public class StringToTableEnumConverterFactory implements ConverterFactory<String, TableEnum> { @Override public <T extends TableEnum> Converter<String, T> getConverter(Class<T> targetType) { if (!targetType.isEnum()) { throw new IllegalArgumentException("TableEnum 구현체는 Enum이어야 합니다."); } return new StringToTableEnumConverter<>(targetType); } // TableEnum => 구현 Enum 변환 @RequiredArgsConstructor private static class StringToTableEnumConverter<T extends TableEnum> implements Converter<String, T> { private final Class<T> enumType; @Override public T convert(String source) { if (source.isEmpty()) { return null; } for (T enumConstant : enumType.getEnumConstants()) { if (enumConstant.getCode().equals(source)) { log.debug("변환성공"); return enumConstant; } } String errmsg = String.format("Enum 상수에 '%s' 코드가 없습니다. enumType= %s", source, enumType.getSimpleName()); throw new IllegalArgumentException(String.format(errmsg)); } } }
Java
복사
Java
복사