본문 바로가기

Spring/MVC

[Spring MVC] Type Converter

이번 장에서는 스프링의 타입 컨버터에 대해서 알아본다.
모든 코드는 깃허브(링크) 에 올려두었다.


개요

개발하다보면 문자 -> 숫자, 숫자 -> 문자 변환과 같은 타입을 변환시켜야 하는 경우가 많이 발생한다.
특히 우리는 무의식중에 당연하게 사용하고 있지만 HTTP 요청 파라미터의 경우 전부 문자열로 서버에 전송된다.
하지만 우리가 원하는 타입이 숫자라면 스프링이 중간에서 변환해주기 때문에 우리는 타입 변환없이 사용이 가능하다.
단순 파라미터 뿐만 아니라 @ModelAttribute, @PathVariable, @RequestBody, @Value, XML 파싱등 많은 부분에서 스프링이 자동으로 타입을 변경시켜주고 있다.


Converter 인터페이스

스프링에서 기본적으로 지원하는 타입 변환이 아니라 개발자가 직접 타입 변환을 구현하는 경우를 위해서 컨버터 인터페이스를 제공한다.

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

이 컨버터 인터페이스는 모든 타입에 적용할 수 있다.
예를 들어 1(Integer)가 입력되면 True(Boolean) 타입으로 변경하고 싶으면 Integer -> Boolean 타입으로 변환되도록 컨버터 인터페이스를 만들어서 등록하면 된다.
반대의 경우도 필요하다면 Boolean -> Integer 타입으로 변환되도록 컨버터를 추가로 만들어서 등록하면 된다.

@Slf4j
public class IntegerToBooleanConverter implements Converter<Integer, Boolean> {
    @Override
    public Boolean convert(Integer source) {
        log.info("Integer To Boolean Source = {}", source);
        if (source > 1) {
            throw new IllegalArgumentException("1을 초과하는 숫자는 변경이 불가능합니다.");
        } else {
            return source == 1;
        }
    }
}
@Slf4j
public class BooleanToIntegerConverter implements Converter<Boolean, Integer> {
    @Override
    public Integer convert(Boolean source) {
        log.info("Boolean To Integer Source = {}", source);
        return source == Boolean.TRUE ? 1 : 0;
    }
}

사용자 정의 클래스 컨버터

제조사와 사이즈(인치) 필드를 가지고 있는 모니터(MonitorSpec) 클래스에 컨버터를 적용시켜본다.

@Getter
@EqualsAndHashCode
@AllArgsConstructor
public class MonitorSpec {
    private String manufacturer;
    private int inch;
}

@EqualAndHashCode 애노테이션을 사용하는 경우 equals(), hashcode()를 생성해주어 필드 값이 동등한 경우 equals의 결과가 true가 된다.
클라이언트는 모니터 객체의 필드를 모르기때문에 단순 문자열인 Samsung_27, LG_32와 같은 형태로 입력하고 우리는 문자열을 제조사와 사이즈로 분리해야하는 상황이다.

문자열로 입력된 모니터 정보를 MonitorSpec 객체로 변경하는 컨버터 코드는 아래와 같다.

@Slf4j
public class StringToMonitorSpecConverter implements Converter<String, MonitorSpec> {
    @Override
    public MonitorSpec convert(String source) {
        log.info("String To Monitor Spec Source = {}", source);
        String[] splits = source.split("_");
        String manufacturer = splits[0];
        int inch = Integer.parseInt(splits[1]);
        return new MonitorSpec(manufacturer, inch);
    }
}

MonitorSpec객체를 클라이언트로 전달하기 위해 문자열로 변경하는 컨버터 코드는 아래와 같다.

@Slf4j
public class MonitorSpecToStringConverter implements Converter<MonitorSpec, String> {
    @Override
    public String convert(MonitorSpec source) {
        log.info("MonitorSpec To String source = {}", source);
        return String.format("%s_%s", source.getManufacturer(), source.getInch());
    }
}

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

  • Converter: 기본 컨버터
  • ConverterFactory: 전체 클래스의 계층형 구조가 필요한 경우
  • GenericConverter: 정교한 구현체와 대상이 되는 필드의 애노테이션 정보를 사용 가능하다.
  • ConditionalGenericConverter: 특정 조건을 충족시키는 경우에만 실행된다.

Conversion Service

컨버터의 종류는 원하는 타입에 따라서 수없이 많은 컨버터가 생성될 수 있다.
하지만 필요한 컨버터를 모두 만들고 필요한 경우에 찾아서 사용하는 것은 무리가 있다.
개발자가 직접 컨버터를 찾을 필요없이 필요한 컨버터를 미리 등록시켜놓고 사용하는 시점에 적절한 컨버터를 실행시켜주는 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);
}

우리가 위에서 만든 컨버터를 등록하고 사용하는 방법은 아래와 같다.

public class ConversionServiceTest {
    @Test
    void conversionServiceTest() {
        // 등록
        DefaultConversionService conversionService = new DefaultConversionService();
        conversionService.addConverter(new IntegerToBooleanConverter());
        conversionService.addConverter(new BooleanToIntegerConverter());
        conversionService.addConverter(new MonitorSpecToStringConverter());
        conversionService.addConverter(new StringToMonitorSpecConverter());

        // 사용
        assertThat(conversionService.convert(1, Boolean.class)).isEqualTo(Boolean.TRUE);
        assertThat(conversionService.convert(Boolean.TRUE, Integer.class)).isEqualTo(1);
        MonitorSpec monitorSpec = conversionService.convert("Samsung_27", MonitorSpec.class);
        assertThat(monitorSpec).isEqualTo(new MonitorSpec("Samsung", 27));
        String monitorSpecString = conversionService.convert(new MonitorSpec("LG", 32), String.class);
        assertThat(monitorSpecString).isEqualTo("LG_32");
    }
}

DefaultConversionService를 사용하면 컨버터의 등록과 사용을 분리하게 된다.
컨버터를 등록하는 시점에는 컨버터의 타입을 정확히 알아야 하지만 사용하는 시점에서는 컨버터를 전혀 몰라도 된다.
전부 ConversionService 내부적으로 동작하여 필요한 컨버터를 제공하기 때문이다. 우리는 구현체가 아닌 ConversionService에만 의존하면 된다.

스프링과 Converter

스프링 내부에서 이미 ConversionService가 동작하고 있기 때문에 우리는 WebMvcConfigurer 인터페이스를 구현하여 컨버터를 등록해서 사용하면 된다.

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new IntegerToBooleanConverter());
        registry.addConverter(new BooleanToIntegerConverter());
        registry.addConverter(new StringToMonitorSpecConverter());
        registry.addConverter(new MonitorSpecToStringConverter());
    }
}

우리는 Integer 타입을 Boolean 타입으로 변환해주는 IntegerToBooleanConverter를 등록하였다.
하지만 스프링은 이미 기본적인 타입들을 변환해주는 컨버터를 가지고 있기 때문에 같은 역할을 하는 컨버터가 중복되게 된다.
이렇게 같은 기능을 하는 컨버터가 중복되는 경우 스프링은 개발자가 직접 등록한 컨버터가 우선적으로 사용되도록 한다.

@RequestParam을 예로 들어보면 @RequestParam을 처리하는 ArgumentResolverRequestParamMethodArgumentResolver를 사용한다.
RequestParamMethodArgumentResolverConversionService를 사용하여 사용자로 부터 전달받은 데이터의 타입을 변환한다.


타임리프 컨버터 적용

타임리프틑 렌더링 시점에 컨버터를 사용하여 렌더링 하는 방법을 지원한다.
그렇기 때문에 우리가 객체를 전달하여도 타임리프틑 문자열로 화면에 표시하게 되는 것이다.

타임리프는 변수를 표현할 때 ${...}와 같은 표현식을 사용하고 ConversionService를 적용할 때는 ${{...}}과 같은 표현식을 사용한다.

  • MonitorSpec 객체를 변수 표현식(${...})으로 출력하면 아래와 같이 출력된다.
    hello.mytypeconverter.type.MonitorSpec@12@1032
  • MonitorSpec 객체에 컨버전 서비스를 적용하는 표현식(${{...}})으로 출력하면 아래와 같이 우리가 원하는 결과가 출력된다.
    Samsung_27

만약 MonitorSpec 객체를 th:value로 출력하면 객체의 해시값이 화면에 출력된다.
반면 th:field의 경우 컨버전 서비스를 지원하기 때문에 우리가 원하는 결과물을 화면에 출력해준다.


참고한 강의:

참고한 문서:

'Spring > MVC' 카테고리의 다른 글

[Spring MVC] 파일 업로드  (0) 2022.06.04
[Spring MVC] Formatter  (0) 2022.06.03
[Spring MVC] API 예외처리 - 2  (0) 2022.06.02
[Spring MVC] API 예외처리 - 1  (0) 2022.06.02
[Spring MVC] 스프링 부트 - 오류 페이지  (0) 2022.06.02