Java Category/Spring

[Spring MVC] 스프링 타입 컨버터

ReBugs 2024. 3. 24.

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


스프링 타입 컨버터

스프링 프레임워크에서 타입 컨버터(Type Converter)는 한 타입의 객체를 다른 타입으로 변환할 때 사용되는 메커니즘이다. 스프링 MVC에서는 클라이언트로부터 받은 문자열 데이터를 컨트롤러의 파라미터나 필드에 지정된 타입의 객체로 변환할 필요가 자주 있다.

 

예를 들어, URL 경로에서 가져온 문자열을 날짜 객체나 열거형, 심지어는 사용자 정의 타입으로 변환해야 할 때가 그러하다. 이럴 때 스프링의 타입 컨버터 기능이 유용하게 사용된다.

 

@GetMapping("/hello-v2")
 public String helloV2(@RequestParam Integer data) {
     System.out.println("data = " + data);
     return "ok";
}

http://localhost:8080/hello-v2?data=10

HTTP 쿼리 스트링으로 전달하는 data=10 부분에서 10은 숫자 10이 아니라 문자 10이다.

스프링이 제공하는 @RequestParam 을 사용하면 이 문자 10을 Integer 타입의 숫자 10으로 편리하게 받을 수 있다.

이것은 스프링이 중간에서 타입을 변환해주었기 때문이다.

이러한 예는 @ModelAttribute , @PathVariable 에서도 확인할 수 있다.

 

@ModelAttribute 타입 변환 예시

@ModelAttribute UserData data
 class UserData {
   Integer data;
}

 

@PathVariable 타입 변환 예시

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

URL 경로는 문자다. /users/10 → 여기서 10도 숫자 10이 아니라 그냥 문자 "10"이다. data를 Integer 타입으로 받을 수 있는 것도 스프링이 타입 변환을 해주기 때문이다.

 

스프링의 타입 변환 적용 예

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

 


스프링과 타입 변환

만약 개발자가 새로운 타입을 만들어서 변환하고 싶다면 컨버터 인터페이스를 확장하면 된다.

 

타입 컨버터 - Converter

컨버터 인터페이스

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

 

Converter 인터페이스: 가장 기본적인 타입 변환 인터페이스로, S 타입을 T 타입으로 변환하는 하나의 메소드 convert를 정의한다.

 

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

이 컨버터 인터페이스는 모든 타입에 적용할 수 있다.

 

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

 

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

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

 

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

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

 

사용자 정의 타입 컨버터

127.0.0.1:8080 과 같은 IP, PORT를 입력하면 IpPort 객체로 변환하는 컨버터

 

IpPort

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

 

StringToIpPortConverter

@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);
     }
}

 

IpPortToStringConverter

@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/docs/current/reference/html/core.html#core- convert

스프링은 문자, 숫자, 불린, Enum등 일반적인 타입에 대한 대부분의 컨버터를 기본으로 제공한다.
IDE에서 Converter , ConverterFactory , GenericConverter 의 구현체를 찾아보면 수 많은 컨버터를 확인할 수 있다.

 

컨버전 서비스 - ConversionService

타입 컨버터를 하나하나 직접 찾아서 타입 변환에 사용하는 것은 매우 불편하다.

그래서 스프링은 개별 컨버터를 모아두고 그것들을 묶어서 편리하게 사용할 수 있는 기능을 제공하는데, 이것이 바로 컨버전 서비스
( ConversionService )이다.

 

ConversionService 인터페이스

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);
}

ConversionService 인터페이스: 다양한 Converter와 Formatter를 등록하고 관리하는 중앙 인터페이스로, 실제 타입 변환 작업을 이 인터페이스를 통해 수행한다.

 

  • boolean canConvert(Class<?> sourceType, Class<?> targetType): 지정된 소스 타입에서 타겟 타입으로의 변환이 가능한지 여부를 반환한다.
  • boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType): TypeDescriptor를 사용하여 보다 세밀한 제네릭 타입 정보를 포함한 변환 가능 여부를 확인한다.
  • <T> T convert(Object source, Class<T> targetType): 소스 객체를 타겟 타입의 객체로 변환한다.
  • Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType): TypeDescriptor를 사용하여 소스 객체를 타겟 타입의 객체로 변환한다.

 

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

 

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");
    }
}

 

 

웹 애플리케이션에 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());
	}
}
@GetMapping("/ip-port")
 public String ipPort(@RequestParam IpPort ipPort) {
     System.out.println("ipPort IP = " + ipPort.getIp());
     System.out.println("ipPort PORT = " + ipPort.getPort());
     return "ok";
}

 

스프링은 내부에서 ConversionService 를 제공한다.

 

우리는 WebMvcConfigurer 가 제공하는 addFormatters() 를 사용해서 추가하고 싶은 컨버터를 등록하면 된다. 이렇게 하면 스프링은 내부에서 사용하는 ConversionService 에 컨버터를 추가해준다.

스프링은 내부에서 수 많은 기본 컨버터들을 제공한다.

컨버터를 추가하면 추가한 컨버터가 기본 컨버터 보다 높은 우선 순위를 가진다.

 

@RequestParam 은 @RequestParam 을 처리하는 ArgumentResolver인 RequestParamMethodArgumentResolver 에서ConversionService 를 사용해서 타입을 변환한다.

 

부모 클래스와 다양한 외부 클래스를 호출하는 등 복잡한 내부 과정을 거치기 때문에 대략 이렇게 처리되는 것으로 이해해도 충분하다. 

 

DefaultConversionService

ConversionService 인터페이스의 기본 구현체 중 하나이며, 다양한 일반적인 변환 케이스를 위한 컨버터들이 사전에 등록되어 있다. 이를 통해 개발자는 추가적인 컨버터 구현 없이도 많은 표준 타입 간의 변환을 쉽게 수행할 수 있다. 예를 들어, 문자열을 숫자, 날짜, 열거형 등 다양한 타입으로 변환하거나 그 반대의 변환을 지원한다.

DefaultConversionService를 사용하면 개발자는 타입 변환을 위한 복잡한 코드를 직접 작성할 필요 없이, 스프링이 제공하는 풍부한 타입 변환 기능을 바로 사용할 수 있다. 이는 특히 데이터 바인딩, 프로퍼티 편집, 설정 관리 등에서 유용하게 활용된다.

 

DefaultConversionService 사용 예제

import org.springframework.core.convert.support.DefaultConversionService;

public class ConversionExample {
    public static void main(String[] args) {
        // DefaultConversionService 인스턴스 생성
        DefaultConversionService conversionService = new DefaultConversionService();
        
        // 문자열을 Integer 타입으로 변환
        Integer intValue = conversionService.convert("123", Integer.class);
        
        // 문자열을 Boolean 타입으로 변환
        Boolean boolValue = conversionService.convert("true", Boolean.class);
        
        System.out.println(intValue); // 출력: 123
        System.out.println(boolValue); // 출력: true
    }
}

이 예제에서는 DefaultConversionService를 사용하여 문자열을 Integer와 Boolean 타입으로 간단하게 변환하는 과정을 보여준다. 이처럼 DefaultConversionService는 다양한 기본 타입 변환을 쉽게 처리할 수 있는 강력한 도구이다.

 

커스텀 컨버터 등록

DefaultConversionService는 사용자 정의 컨버터를 등록하여 사용할 수도 있다. 이는 특정 애플리케이션에 특화된 타입 변환을 필요로 할 때 유용하다.

import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.support.DefaultConversionService;

public class CustomConverterExample {
    public static void main(String[] args) {
        DefaultConversionService conversionService = new DefaultConversionService();
        
        // 사용자 정의 컨버터 등록
        conversionService.addConverter(new StringToLocalDateConverter());
        
        // 사용자 정의 컨버터를 이용한 변환
        LocalDate date = conversionService.convert("2023-03-28", LocalDate.class);
        System.out.println(date); // 출력: 2023-03-28
    }
    
    // String을 LocalDate로 변환하는 사용자 정의 컨버터
    private static class StringToLocalDateConverter implements Converter<String, LocalDate> {
        @Override
        public LocalDate convert(String source) {
            return LocalDate.parse(source);
        }
    }
}

 

등록과 사용 분리

컨버터를 등록할 때는 StringToIntegerConverter 같은 타입 컨버터를 명확하게 알아야 한다.

반면에 컨버터를 사용하는 입장에서는 타입 컨버터를 전혀 몰라도 된다. 타입 컨버터들은 모두 컨버전 서비스 내부에 숨어서 제공된다. 따라서 타입을 변환을 원하는 사용자는 컨버전 서비스 인터페이스에만 의존하면 된다.

 

물론 컨버전 서비스를 등록하는 부분과 사용하는 부분을 분리하고 의존관계 주입을 사용해야 한다.

 

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

소프트웨어 설계 원칙 중 하나로, SOLID 원칙의 한 부분이다. ISP는 "클라이언트는 자신이 사용하지 않는 메서드에 의존하면 안 된다"는 개념을 기반으로 한다. 즉, 하나의 큰 인터페이스보다는 필요한 메서드만을 제공하는 여러 개의 구체적인 인터페이스로 분리하는 것이 바람직하다는 원칙이다.


ISP의 목적
ISP의 주요 목적은 모듈 간의 결합도를 줄이고, 유지보수성과 재사용성을 향상시키는 것이다. 인터페이스가 너무 많은 역할을 담당하게 되면, 그 인터페이스를 구현하는 클래스가 필요하지 않은 메서드까지 구현해야 하는 상황이 발생한다. 이러한 상황은 코드의 복잡성을 증가시키고, 변경에 대한 유연성을 감소시킨다.


ISP를 적용하는 방법
→역할별 인터페이스 분리: 하나의 인터페이스에 여러 역할이 혼합되어 있다면, 역할별로 인터페이스를 분리해야 한다. 각 인터페이스는 하나의 명확한 책임만을 가져야 한다.
→구체적인 인터페이스 정의: 인터페이스는 사용하는 클라이언트에 최대한 밀접하게 정의되어야 한다. 클라이언트가 필요로 하는 메서드만을 포함시키도록 한다.
→인터페이스 상속과 구성 사용: 너무 많은 메서드를 가진 인터페이스는 상속을 통해 더 작은 인터페이스로 분리하거나, 구성을 사용하여 필요한 인터페이스를 조합할 수 있다.

 

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

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

 

이렇게 인터페이스를 분리하면 컨버터를 사용하는 클라이언트와 컨버터를 등록하고 관리하는 클라이언트의 관심사를 명확하게 분리할 수 있다.

 

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

 

 

뷰 템플릿에 컨버터 적용하기

타임리프는 렌더링 시에 컨버터를 적용해서 렌더링 하는 방법을 편리하게 지원한다.
이전까지는 문자를 객체로 변환했다면, 이번에는 그 반대로 객체를 문자로 변환하는 작업을 확인할 수 있다.

 

컨트롤러

@Controller
public class ConverterController {
    @GetMapping("/converter-view")
    public String converterView(Model model) {
        model.addAttribute("number", 10000);
        model.addAttribute("ipPort", new IpPort("127.0.0.1", 8080));
        return "converter-view";
    }

    @GetMapping("/converter/edit")
    public String converterForm(Model model) {
        IpPort ipPort = new IpPort("127.0.0.1", 8080);
        Form form = new Form(ipPort);
        model.addAttribute("form", form);
        return "converter-form";
    }
    @PostMapping("/converter/edit")
    public String converterEdit(@ModelAttribute Form form, Model model) {
        IpPort ipPort = form.getIpPort();
        model.addAttribute("ipPort", ipPort);
        return "converter-view";
    }
    @Data
    static class Form {
        private IpPort ipPort;
        public Form(IpPort ipPort) {
            this.ipPort = ipPort;
        }
    }
}

Model 에 숫자 10000 와 ipPort 객체를 담아서 뷰 템플릿에 전달한다.

 

converter-view.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        <ul>
            <li>${number}: <span th:text="${number}" ></span></li>
            <li>${{number}}: <span th:text="${{number}}" ></span></li>
            <li>${ipPort}: <span th:text="${ipPort}" ></span></li>
            <li>${{ipPort}}: <span th:text="${{ipPort}}" ></span></li>
        </ul>
    </body>
</html>

타임리프는 ${{...}} 를 사용하면 자동으로 컨버전 서비스를 사용해서 변환된 결과를 출력해준다. 물론 스프링과 통 합 되어서 스프링이 제공하는 컨버전 서비스를 사용하므로, 우리가 등록한 컨버터들을 사용할 수 있다.

 

  • 변수 표현식 : ${...}
  • 컨버전 서비스 적용 : ${{...}}

 

  • ${{number}} : 뷰 템플릿은 데이터를 문자로 출력한다. 따라서 컨버터를 적용하게 되면 Integer 타입 인 10000 을 String 타입으로 변환하는 컨버터인 IntegerToStringConverter 를 실행하게 된다. 이 부 분은 컨버터를 실행하지 않아도 타임리프가 숫자를 문자로 자동으로 변환히기 때문에 컨버터를 적용할 때와 하지 않을 때가 같다.
  • ${{ipPort}} : 뷰 템플릿은 데이터를 문자로 출력한다. 따라서 컨버터를 적용하게 되면 IpPort 타입을 String 타입으로 변환해야 하므로 IpPortToStringConverter 가 적용된다. 그 결과 127.0.0.1:8080 가 출력된다.

 

 

컨버터를 폼에 적용

converter-form.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        <form th:object="${form}" th:method="post">
            th:field <input type="text" th:field="*{ipPort}"><br/>
            th:value <input type="text" th:value="*{ipPort}">(보여주기 용도)<br/> 
            <input type="submit"/>
        </form>
    </body>
</html>

 

Form 객체를 데이터를 전달하는 폼 객체로 사용

 

→GET /converter/edit : IpPort 를 뷰 템플릿 폼에 출력
→POST /converter/edit : 뷰 템플릿 폼의 IpPort 정보를 받아서 출력
타임리프의 th:field 는 컨버전 서비스도 함께 적용

 

GET /converter/edit
th:field 가 자동으로 컨버전 서비스를 적용해주어서 ${{ipPort}} 처럼 적용이 되었음, 따라서 IpPort → String 으로 변환된다.

POST /converter/edit
@ModelAttribute 를사용해서 String → IpPort 로 변환된다.

 

Formatter

Formatter는 스프링 프레임워크에서 제공하는 또 다른 타입 변환 방식으로, 주로 객체를 문자열로 변환하거나 문자열을 객체로 변환할 때 사용한다. Formatter는 Converter보다 좀 더 고수준의 추상화를 제공하며, 특히 국제화(i18n) 지원이 필요한 경우 유용하다.

기본 사용법

Formatter 인터페이스를 구현하여 사용자 정의 포맷터를 만들 수 있다. 이 인터페이스는 parse 메소드와 print 메소드 두 가지를 정의한다. parse 메소드는 문자열을 객체로 변환하고, print 메소드는 객체를 문자열로 변환한다.

 

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

 

  • Printer<T> 인터페이스는 객체를 문자열로 변환하는 기능을 담당한다. print 메소드는 객체 T를 받아서 Locale에 맞는 문자열로 변환하여 반환한다. 이는 데이터를 사용자에게 보여줄 때 특히 유용하며, 로케일에 따른 날짜, 시간, 숫자 형식 변환 등에 사용될 수 있다.
  • Parser<T> 인터페이스는 문자열을 객체로 변환하는 기능을 담당한다. parse 메소드는 문자열과 Locale을 입력받아, 해당 문자열을 객체 T로 변환하여 반환한다. 이는 사용자 입력을 애플리케이션에서 사용할 수 있는 객체 형태로 변환할 필요가 있을 때 사용된다.
  • Formatter<T>는 Printer<T>와 Parser<T>의 기능을 모두 상속받으므로, 객체와 문자열 간의 양방향 변환을 담당한다. 이를 통해 개발자는 하나의 인터페이스 내에서 타입 변환 로직을 중앙화하여 관리할 수 있다. 특히, 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);
        //Number parse = format.parse(text);
        return format.parse(text);
    }
    @Override
    public String print(Number object, Locale locale) {
        log.info("object={}, locale={}", object, locale);
        //NumberFormat instance = NumberFormat.getInstance(locale);
        //String format = instance.format(object);
        return NumberFormat.getInstance(locale).format(object);
    }
}

 

"1,000" 처럼 숫자 중간의 쉼표를 적용하려면 자바가 기본으로 제공하는 NumberFormat 객체를 사용하면 된다. 이 객체는 Locale 정보를 활용해서 나라별로 다른 숫자 포맷을 만들어준다.

parse() 를 사용해서 문자를 숫자로 변환한다. 참고로 Number 타입은 Integer, Long 과 같은 숫자 타입의 부모 클래스이다.
print() 를 사용해서 객체를 문자로 변환한다.

 

테스트 코드

class MyNumberFormatterTest {
    MyNumberFormatter formatter = new MyNumberFormatter();

    @Test
    void parse() throws ParseException {
        Number result = formatter.parse("1,000", Locale.KOREA);
        assertThat(result).isEqualTo(1000L); //Long 타입 주의
    }

    @Test
    void print() {
        String result = formatter.print(1000, Locale.KOREA);
        assertThat(result).isEqualTo("1,000");
    }
}

테스트 코드가 모두 통과한 것을 볼 수 있다.

 

공식 사이트 문서
https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#format

 

 

DefaultFormattingConversionService

ConversionService에 Formatter와 Converter의 등록 및 관리 기능을 추가한 것이다. 이 클래스는 기본적으로 스프링이 제공하는 여러 기본 Converter와 Formatter를 등록하며, 개발자가 추가적으로 변환기를 등록할 수도 있다. 이를 통해 데이터 바인딩, 타입 변환, 포맷팅을 위한 일관된 방법을 제공한다.

 

주요 기능

 

  • 타입 변환: Converter 인터페이스를 구현한 변환기를 등록하여, 특정 타입에서 다른 타입으로의 변환을 지원한다.
  • 데이터 포맷팅: Formatter 인터페이스를 구현한 포맷터를 등록하여, 객체를 특정 포맷의 문자열로 변환하거나 문자열을 객체로 변환하는 기능을 제공한다.
  • 어노테이션 기반 포맷팅: @NumberFormat과 @DateTimeFormat 같은 애너테이션을 사용하여, 필드 레벨에서 직접 데이터 포맷을 지정할 수 있다.

 

FormattingConversionService 는 포맷터를 지원하는 컨버전 서비스이다.

DefaultFormattingConversionService 는 FormattingConversionService 에 기본적인 통화, 숫자 관련 몇가지 기본 포맷터를 추가해서 제공한다.

 

public class FormattingConversionServiceTest {
    @Test
    void 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);
    }
}

 

 

DefaultFormattingConversionService 상속 관계

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

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

 

포맷터 적용(등록)하기

@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());

        registry.addFormatter(new MyNumberFormatter());
    }
}

StringToIntegerConverter , IntegerToStringConverter 주석처리 → MyNumberFormatter 도 숫자 → 문자, 문자 → 숫자로 변경하기 때문에 둘의 기능이 겹친다.

우선순위는 컨버터가 우선하므로 포맷터가 적용되지 않고, 컨버터가 적용된다.

 

실행 - 객체 → 문자
http://localhost:8080/converter-view


• ${number}: 10000
• ${{number}}: 10,000

컨버전 서비스를 적용한 결과 MyNumberFormatter 가 적용되어서 10,000 문자가 출력된 것을 확인할 수 있다.

 

실행 - 문자 → 객체
http://localhost:8080/hello-v2?data=10,000

실행 로그
→MyNumberFormatter : text=10,000, locale=ko_KR
→data = 10000

"10,000" 이라는 포맷팅 된 문자가 Integer 타입의 숫자 10000으로 정상 변환 된 것을 확인할 수 있다.

 

 

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

스프링은 자바에서 기본으로 제공하는 타입들에 대해 수 많은 포맷터를 기본으로 제공한다.
IDE에서 Formatter 인터페이스의 구현 클래스를 찾아보면 수 많은 날짜나 시간 관련 포맷터가 제공되는 것을 확인 할 수 있다.

그런데 포맷터는 기본 형식이 지정되어 있기 때문에, 객체의 각 필드마다 다른 형식으로 포맷을 지정하기는 어렵다.

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

 

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

 

컨트롤러

@Controller
public class FormatterController {
    @GetMapping("/formatter/edit")
    public String formatterForm(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;
    }
}

 

뷰(타임리프)

formatter-form.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        <form th:object="${form}" th:method="post">
            number <input type="text" th:field="*{number}"><br/>
            localDateTime <input type="text" th:field="*{localDateTime}"><br/>
            <input type="submit"/>
        </form>
    </body>
</html>

 

formatter-view.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
<body>
    <ul>
        <li>${form.number}: <span th:text="${form.number}" ></span></li>
        <li>${{form.number}}: <span th:text="${{form.number}}" ></span></li>
        <li>${form.localDateTime}: <span th:text="${form.localDateTime}" ></span></li>
        <li>${{form.localDateTime}}: <span th:text="${{form.localDateTime}}" ></span></li>
    </ul>
</body>
</html>

 

 

 

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

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

 

특히 객체를 JSON으로 변환할 때 메시지 컨버터를 사용하면서 이 부분을 많이 오해하는데, HttpMessageConverter 의 역할은 HTTP 메시지 바디의 내용을 객체로 변환하거나 객체를 HTTP 메시지 바디에 입력하는 것이다.

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

 

따라서 JSON 결과로 만들어지는 숫자나 날짜 포맷을 변경하고 싶으면 해당 라이브러리가 제공하는 설정을 통해서 포맷을 지정해야 한다. 결과적으로 이것은 컨버전 서비스와 전혀 관계가 없다.
컨버전 서비스는 @RequestParam, @ModelAttribute, @PathVariable, 뷰 템플릿 등에서 사용할 수 있다.

 

 

댓글