Notice
Recent Posts
Recent Comments
Link
관리 메뉴

look-forest

스프링 타입 컨버터 본문

Spring/Spring MVC - 웹 개발 활용 기술

스프링 타입 컨버터

studyHub 2025. 1. 24. 18:02

<선 요약>

스프링은 내부에서 ConversionService 를 사용해서 타입을 변환한다.

사용자 정의 컨버터나 포매터를 구현할 때는 Converter, Formatter 인터페이스 구현 후,

WebMvcConfigurer의 addFormatter를 구현해 추가 등록하면 된다.


스프링 타입 컨버터 소개

HTTP 요청 파라미터는 모두 문자로 처리된다. 따라서 요청 파라미터를 자바에서 다른 타입으로 변환해서 사용하고 싶으면 숫자 타입으로 변환하는 과정을 거쳐야 한다.

@GetMapping("/hello-v1")
public String helloV1(HttpServletRequest request) {
    String data = request.getParameter("data"); //문자 타입 조회
    Integer intValue = Integer.valueOf(data); //숫자 타입으로 변경
    System.out.println("intValue = " + intValue);
    return "ok";
}

 

스프링 MVC가 제공하는 @RequestParam, @ModelAttribute , @PathVariable 를 사용하면 타입을 자동으로 바꿔준다.

 

스프링의 타입 변환 적용 예

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

어떻게 가능할까?

 

타입 컨버터 - Converter

컨버터 인터페이스

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

(이건 기본 타입 컨버터이고, 용도에 따라 다양한 방식의 타입 컨버터를 제공한다. ConverterFactory, GenericConverter 등)

package org.springframework.core.convert.converter;

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

 

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

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

 

IpPort result = new StringToIpPortConverter().convert("127.0.0.1:8080");

그런데 이렇게 타입 컨버터를 하나하나 직접 사용하면, 직접 컨버팅 하는 것과 큰 차이가 없다.

타입 컨버터를 등록하고 관리하면서 편리하게 변환 기능을 제공하는 역할을 하는 무언가가 필요하다.

 

컨버전 서비스 - ConversionService

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

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

 

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

package org.springframework.core.convert;

public interface ConversionService {
    boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType);
    boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptortargetType);
    
    <T> T convert(@Nullable Object source, Class<T> targetType);
    Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
}

 

 

스프링은 내부에서 ConversionService 를 사용해서 타입을 변환한다.
예를 들어서 앞서 살펴본 @RequestParam 같은 곳에서 이 기능을 사용해서 타입을 변환한다.

 

# 등록과 사용 분리
DefaultConversionService 는 ConversionService 인터페이스를 구현했는데, 추가로 컨버터를 등록하는 기능도 제공한다.
즉, DefaultConversionService 는 다음 두 인터페이스를 구현했다.
- ConversionService : 컨버터 사용에 초점
- ConverterRegistry : 컨버터 등록에 초점

컨버터를 등록할 때는 StringToIntegerConverter 같은 타입 컨버터를 명확하게 알아야 한다. 반면에 컨버터를 사용하는 입장에서는 타입 컨버터를 전혀 몰라도 된다. 타입 컨버터들은 모두 컨버전 서비스 내부에 숨어서 제공된다. 따라서 타입을 변환을 원하는 사용자는 컨버전 서비스 인터페이스에만 의존하면 된다. 물론 컨버전 서비스를 등록하는 부분과 사용하는 부분을 분리하고 의존관계 주입을 사용해야 한다.

인터페이스 분리 원칙 - ISP(Interface Segregation Principle)
"클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다"
이렇게 인터페이스를 분리하면 컨버터를 사용하는 클라이언트와 컨버터를 등록하고 관리하는 클라이언트의 관심사를 명확하게 분리할 수 있다. 특히 컨버터를 사용하는 클라이언트는 ConversionService 만 의존하면 되므로, 컨버터를 어떻게 등록하고 관리하는지는 전혀 몰라도 된다컨버터 등록, 관리 인터페이스가 변해도 사용 인터페이스는 변경하지 않아도 된다. 결과적으로 컨버터를 사용하는 클라이언트는 꼭 필요한 메서드만 알게된다. 이렇게 인터페이스를 분리하는 것을 ISP 라 한다.

 

 

스프링에 Converter 적용하기

컨버터 등록

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

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

 

스프링은 내부에서 수 많은 기본 컨버터들을 제공하는데, 직접 컨버터를 추가하면 추가한 컨버터가 기본 컨버터보다 높은 우선 순위를 가진다.

 

@RequestParam , @ModelAttribute , @PathVariable 에서 등록한 컨버터가 동작함을 확인할 수 있다.

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

 

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

타임리프는 렌더링 시에 컨버터를 적용해서 렌더링 하는 방법을 편리하게 지원한다.

 

타임리프는 ${{...}} 를 사용하면 자동으로 컨버전 서비스를 사용해서 변환된 결과를 출력해준다.

<li>${ipPort}: <span th:text="${ipPort}" ></span></li>     //toString()이 호출되어 주소가 찍힌다.
<li>${{ipPort}}: <span th:text="${{ipPort}}" ></span></li> //등록한 컨버터가 동작한다.

 

타임리프의 th:field 는 id , name 를 출력하는 등 다양한 기능이 있는데, 여기에 컨버전 서비스도 함께 적용된다.

컨버전 서비스를 적용하지 않으려면 th:value를 사용하자.

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

 


포맷터 - Formatter

실제 개발하다보면, 날짜 객체를 문자인 "2021-01-01 10:50:11" 와 같이 출력하거나 또는 그 반대의 상황이 자주 발생한다.

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

 

Converter vs Formatter

  • Converter 는 범용(객체 → 객체)
  • Formatter 는 문자에 특화(객체  문자, 문자  객체) + 현지화(Locale)  //Converter 의 특별한 버전

포맷터 - Formatter 만들기

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

Formatter 인터페이스를 구현하면 된다.

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
        return NumberFormat.getInstance(locale).parse(text);
    }

    @Override
    public String print(Number object, Locale locale) {
        log.info("object={}, locale={}", object, locale);
        return NumberFormat.getInstance(locale).format(object);
    }
}

 

 

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

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

@Test
void formattingConversionService() {
    //DefaultFormattingConversionService 는 FormattingConversionService 에 기본적인 통화, 숫자 관련 몇가지 기본 포맷터를 추가 제공
    DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();

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

    //포멧터 등록
    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);
}

 

 

포맷터 적용하기

포맷터를 웹 애플리케이션에 적용하기 위해서는 마찬가지로 WebMvcConfigurer의 addFormatter를 구현하면 된다.

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToIpPortConverter());
        registry.addConverter(new IpPortToStringConverter());

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

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

 

 

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

스프링은 자바에서 기본으로 제공하는 타입들에 대해 수 많은 포맷터를 기본으로 제공한다.

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

  • @NumberFormat : 숫자 관련 형식 지정 포맷터 사용, NumberFormatAnnotationFormatterFactory
  • @DateTimeFormat : 날짜 관련 형식 지정 포맷터 사용, Jsr310DateTimeFormatAnnotationFormatterFactory
static class Form {
    @NumberFormat(pattern = "###,###")
    private Integer number;

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

정리

컨버터, 포맷터 등록 방법은 다르지만, 사용할 때는 컨버전 서비스를 통해서 일관성 있게 사용할 수 있다!

 

주의!

메시지 컨버터( HttpMessageConverter )에는 컨버전 서비스가 적용되지 않는다. JSON을 객체로 변환하는 메시지 컨버터는 내부에서 Jackson 같은 라이브러리를 사용한다. 이는 컨버전 서비스와 전혀 관계가 없다. 객체를 JSON으로 변환한다면 그 결과는 이 라이브러리에 달린 것이다. 따라서 JSON 결과로 만들어지는 숫자나 날짜 포맷을 변경하고 싶으면 해당 라이브러리가 제공하는 설정을 통해서 포맷을 지정해야 한다.

 

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

 

 

 


참고 자료 & 이미지 출처
스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 (김영한 님)

 

'Spring > Spring MVC - 웹 개발 활용 기술' 카테고리의 다른 글

파일 업로드  (0) 2025.01.25
요약 - 로그인 처리  (0) 2024.08.25
요약 - 검증과 예외 처리  (0) 2024.08.24
API 예외 처리  (0) 2024.08.11
예외 처리와 오류 페이지  (0) 2024.08.10