Spring/Spring MVC

스프링 MVC - 스프링 타입 컨버터, 포맷터

taey 2024. 10. 3. 23:09

컨버터 인터페이스

package org.springframework.core.convert.converter;

public interface Converter<S, T> {
	T convert(S source);
}

 

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

개발자는 스프링에 추가적인 타입 변환이 필요하면 이 컨버터 인터페이스를 구현해서 등록하면 된다.

이 컨버터 인터페이스는 모든 타입에 적용할 수 있다. 필요하면 X → Y 타입으로 변환하는 컨버터 인터페이스를 만들고, 또 Y → X 타입으로 변환하는 컨버터 인터페이스를 만들어서 등록하면 된다.

예를 들어서, 문자로 "true"가 오면Boolean 타입으로 받고 싶으면 String → Boolean 타입으로 변환되도록 컨버터 인터페이스를 만들어서 등록하고, 반대로 적용하고 싶으면 Boolean → String 타입으로 변환되도록 컨버터를 추가로 만들어서 등록하면 된다.

 

 참고
과거에는 PropertyEditor라는 것으로 타입을 변환했다. PropertyEditor는 동시성 문제가 있어서 타입을 변환할 때마다 객체를 계속 생성해야 하는 단점이 있다. 지금은 Converter의 등장으로 해당 문제들이 해결되었고, 기능 확장이 필요하면 Converter를 사용하면 된다. 

 

 


문자를 숫자로, 숫자를 문자로  변환하는 타입 컨버터   

예시 코드

package hello.typeconverter.converter;

import lombok.extern.slf4j.Slf4j;
import 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);
     }
}
package hello.typeconverter.converter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.core.convert.converter.Converter;

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

 


사용자 정의 타입 컨버터 

 

예시 코드

IpPort Class

package hello.typeconverter.type;

import lombok.EqualsAndHashCode;
import lombok.Getter;

@Getter
@EqualsAndHashCode
public class IpPort {
     private String ip;
     private int port;
     public IpPort(String ip, int port) {
         this.ip = ip;
         this.port = port;
	}
}

 

 문자를 IpPort로

package hello.typeconverter.converter;

import hello.typeconverter.type.IpPort;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.convert.converter.Converter;

@Slf4j
public class StringToIpPortConverter implements Converter<String, IpPort> {
     @Override
     public IpPort convert(String source) {
         log.info("convert source={}", source);
         
         String[] split = source.split(":");
         String ip = split[0];
         
         int port = Integer.parseInt(split[1]);
         return new IpPort(ip, port);
     }
}

 

IpPort를 문자로

package hello.typeconverter.converter;

import hello.typeconverter.type.IpPort;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.convert.converter.Converter;

@Slf4j
public class IpPortToStringConverter implements Converter<IpPort, String> {
     @Override
     public String convert(IpPort source) {
         log.info("convert source={}", source);
         return source.getIp() + ":" + source.getPort();
     }
}

 

참고
스프링은 용도에 따라 다양한 방식의 타입 컨버터를 제공한다.

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

https://docs.spring.io/spring-framework/reference/core/validation/convert.html

 


컨버전 서비스 - 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);
}

 

컨버전 서비스 인터페이스는 단순히 컨버팅이 가능한가? 확인하는 기능과, 컨버팅 기능을 제공한다. 

 

  • DefaultConversionServiceConversionService 인터페이스를 구현했는데, 추가로 컨버터를 등록하는 기능도 제공한다.

 

등록과 사용 분리

타입 변환을 원하는 사용자는 컨버전 서비스 인터페이스에만 의존하면 된다. 물론, 컨버전 서비스를 등록하는 부분과 사용하는 부분을 분리하고 의존관계 주입을 사용해야 한다. 

 

컨버전 서비스 사용

Integer value = conversionService.convert("10", Interger.class)

 

 

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

인터페이스 분리 원칙은 클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다.

 

DefaultConversionService는 다음 두 인터페이스를 구현했다.

  • ConversionService : 컨버터 사용에 초점
  • ConversionRegistry : 컨버터 등록에 초점

 

이렇게 인터페이스를 분리하면 컨버터를 사용하는 클라이언트와 컨버터를 등록하고, 관리하는 클라이언트의 관심사를 명확하게 분리할 수 있다. 특히 컨버터를 사용하는 클라이언트는 ConversionService만 의존하면 되므로, 컨버터를 어떻게 등록하고 관리하는지는 몰라도 된다. 결과적으로 컨버터를 사용하는 클라이언트는 꼭 필요한 메서드만 알게 된다. 이렇게 인터페이스를 분리하는 것을 ISP라 한다.       

 


포맷터 - Formatter

객체를 특정한 포맷에 맞추어 문자를 출력하거나 또는 그 반대의 역할을 하는 것에 특화된 기능이 바로 포맷터(Formatter)이다. 

 

Locale

날짜 숫자의 표현 방법은 Locale 현지화 정보가 사용될 수 있다.

 

Converter vs Formatter

  • Converter는 범용(객체 → 객체)
  • Formatter는 문자에 특화(객체 → 문자, 문자 → 객체) + 현지화(Locale)

포맷터 - Formatter 만들기

포맷터(Formatter)는 객체를 문자로 변경하고, 문자를 객체로 변경하는 두 가지 기능을 모두 수행한다.

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

 

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> {
}

 

숫자 1000을 문자 "1,000"으로 바뀌는 포맷을 적용해보자.

import lombok.extern.slf4j.Slf4j;
import org.springframework.format.Formatter;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Locale;

@Slf4j
public class MyNumberFormatter implements Formatter<Number> {
     @Override
     public Number parse(String text, Locale locale) throws ParseException {
         log.info("text={}, locale={}", text, locale);
         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);
     }
}

  

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

  https://docs.spring.io/spring-framework/reference/core/validation/format.html

 


포맷터를 지원하는 컨버전 서비스

포맷터를 지원하는 컨버전 서비스를 사용하면 컨버전 서비스에 포맷터를 추가할 수 있다. 내부에서 어댑터 패턴을 사용해서 Formatter가 Converter처럼 동작하도록 지원한다.

 

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

DefaultFormattingConversionService 상속 관계

FormattingConversionServiceConversionService 관련 기능을 상속받기 때문에 결과적으로 컨버터도 포맷터도 모두 등록할 수 있다. 그리고 사용할 때는 ConversionService가 제공하는 convert를 사용하면 된다.

 

추가로 스프링 부트는 DefaultFormattingConversionService를 상속 받은WebConversionService를 내부에서 사용한다. 

 


 스프링이 제공하는 기본 포맷터 

스프링은 애노테이션 기반으로 원하는 형식을 지정해서 사용할 수 있는 매우 유용한 포맷터 두 가지를 기본으로 제공한다.

  • @NumberFormat : 숫자 관련 형식 지정 포맷터 사용, NumberFormatAnnotationFormatterFactory
  • @DataTimeFormat : 날짜 관련 형식 지정 포맷터 사용, Jsr310DateTimeFormatAnnotationFormatterFactory 

예제 코드

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

 

 


주의

메시지 컨버터(HttpMessageConverter)에는 컨버전 서비스가 적용되지 않는다.

HttpMessageConverter 의 역할은 HTTP 메시지 바디의 내용을 객체로 변환하거나 객체를 HTTP 메시지 바디에 입력하는 것이다.

예를 들어서 JSON을 객체로 변환하는 메시지 컨버터는 내부에서 Jackson 같은 라이브러리를 사용 한다.

객체를 JSON으로 변환한다면 그 결과는 이 라이브러리에 달린 것이다.

따라서 JSON 결과로 만들어지는 숫자나 날짜 포맷을 변경하고 싶으면 해당 라이브러리가 제공하는 설정을 통해서 포맷을 지정해야 한다.

 

컨버전 서비스는 @RequestParam , @ModelAttribute , @PathVariable , 뷰 템플릿 등에서 사용할 수 있다