Spring에서의 타입 컨버터

hoyong.eom·2023년 7월 30일
0

스프링

목록 보기
25/59
post-thumbnail

Spring

타입컨버터

Spring에서는 @RequestParam 애노테이션을 사용해서 전달된 파라미터를 파싱해서 사용할때 아래와 같이 사용이 가능하다.

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

즉, text(String)으로 전달된 값을 자동으로 Integer로 타입 변환해서 받을 수가 있다.

이게 가능한 이유는 스프링이 중간에 타입을 변해주었기 때문이라고 한다.

RequestParam뿐만 아니라 ModelAttribute, PathVariable에서 이런 변환은 가능하다.

@ModelAttribute UserData data
class UserData {
 Integer data;
}

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

스프링에서 타입 변환을 적용한 예를 들어보면 아래와 같다.

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

새로운 타입을 만들어서 변환하고 싶은 경우

스프링과 자바에서 제공하는 타입이 아니라 개발자가 만든 타입으로 변환하고 싶은 경우에는 아래와 같이 컨버터 인터페이스를 구현하면 된다.

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

위 처럼 스프링은 확장 가능한 컨버터 인터페이스를 제공한다. 개발자는 스프링에 추가적인 타입 변환이 필요하면 이 컨버터 인터페이스를 구현해서 등록하면 된다. 위 인터페이스에서 S,Y 가 변환 타입들을 의미하게 된다.

타입 컨버터 - Converter

타입 컨버터를 사용하려면 org.springframework.core.convert.converter.Converter를 사용해야한다.

이 컨버터를 이용해서 단순히 숫자와 문자사이이를 컨버팅해주는 인터페이스를 구현해보면 아래와 같다.

public class IntegerToStringConverter implements Converter<Integer, String> {

    @Override
    public String convert(Integer source) {
        log.info("convert source={}", source);
        return String.valueOf(source);
    }
}

public class StringToIntegerConverter implements Converter<String, Integer> {

    @Override
    public Integer convert(String source) {
        log.info("convert source={}", source);
        return Integer.valueOf(source);
    }
}

위 처럼 컨버터 인터페이스 구현체를 만들고 convert 함수를 호출해주면 된다. 매우 간단하다!
하지만, 이렇게 타입 컨버터를 하나하나 직접 사용하면 개발자가 직접 컨버팅하는것과 차이가 없다.
따라서 타입 컨버터를 등록하고 관리하면서 편리하게 변환 기능을 제공하는 역할을 하는 무언가 필요하다.

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

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

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

아래의 테스트 코드를 보면 이해가 쉽다.


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

    }
}

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

등록과 사용의 분리

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

ISP 원칙

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

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

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

스프링에 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());

        //추가
        registry.addFormatter(new MyNumberFormatter());
    }
}

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

위 코드를 통해서 컨버터를 등록해주면 추가한 컨버터가 기존 컨버터 보다 우선순위를 갖게되어 실행된다.

@RequestParam은 @RequestParam을 처리하는 ArgumentResolver(말그대로 매개변수 처리자)인 RequestParamMethodArgumentResolver에서 ConversionService를 사용해서 타입을 변환한다.

포맷터 - Formatter

Converter는 입력과 출력 타입에 제한이 없는 범용,타입 변환 기능을 제공한다.
하지만 웹 애플리케이션 환경에서는 문자를 다른 타입으로 변환하거나 다른 타입을 문자로 변환하는 상황이 대부분이다.

Locale

날짜와 숫자의 표현 방벙븐 Locale 현지화 정보가 사용될 수 있다.
이렇게 객체를 특정한 포맷에 맞추어 문자로 출력하거나 또는 그 반대의 역할을 하는것에 특화된 기능이 바로 포맷터(Formatter)이다. 포맷터는 컨버전의 특별한 버전으로 이해하면 되겠다.

포맷터 - 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"으로 변환해주는 포맷은 아래와 같이 구현가능하다.

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

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

parse()를 사용해서 문자를 숫자로 변환한다.
print()를 사용해서 객체를 문자로 변환한다.

참고)
스프링은 용도에 따라 다양한 방식의 포맷터를 제공한다.

Formatter 포맷터
AnnotationFormatterFactory 필드의 타입이나 애노테이션 정보를 활용할 수 있는 포맷터

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

컨버전 서비스에는 컨버터만 등록할 수 있고 ,포맷터를 등록할 수 없다. 그런데 포맷터는 객체 -> 문자, 문자 -> 객체로 변환하는 특별하는 컨버터일 뿐이다.
포맷터를 지원하는 컨버전 서비스를 사용하면 컨버전 서비스에 포맷터를 추가할 수 있다. 내부에서 어댑터패턴을 사용해서 Formatter가 Converter처럼 동작하도록 지원한다.

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

    }
}

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

addFormatters를 이용해서 동일하게 추가해주면된다.

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

스프링은 자바에서 기본으로 제공하느 타입들에 대해 수많은 포맷터를 기본으로 제공한다.
IDE에서 Formatter 인터페이스의 구현 클래스를 찾아보면 수 많은 날짜나 시간 관련 포맷터가 제공되는것을 확인할 수 있다.
그런데 포맷터를 기본 형식이 지정되어 있기 떄문에 객체의 각 필드마다 다른 형식으로 포맷을 지정하기는 어렵다.
스프링은 이런 문제를 해결하기 위해서 애노테이션 기반으로 원하는 형식을 지정해서 사용할 수 있는 매우 유용한 포맷터 두 가지를 기본으로 제공한다.

  • @NumberFormat : 숫자 관련 형식 지정 포맷터 사용
  • @DateTimeFormat : 날짜 관련 형식 지정 포맷터 사용
@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;
    }
}

참고

아래의 강의를 공부하여 정리한 내용입니다.
김영한님의 스프링 MVC2

0개의 댓글