티스토리 뷰

반응형

(참고: Spring Boot, Thymeleaf, Gradle을 기준으로 작성된 글입니다.)

 

웹 애플리케이션의 경우 클라이언트가 보내는 데이터와 서버에서 다시 반환하는 데이터 간의 타입이 불일치하는 경우가 빈번하게 발생한다, 예를 들어 String으로 보낸 데이터를(HTTP 요청 파라미터의 경우 모두 문자 타입으로 처리됨) int타입으로 변환해야 한다던지, 보낸 데이터를 개발자가 지정한 객체에 담아서 받는 상황이 대표적인 경우다

 

Converter Interface

이러한 변환 과정을 편리하게 사용할 수 있도록 스프링은 기본적으로 컨버터를 적용시키지만 개발자가 직접 원하는 타입으로 변환을 위하여 컨버터 인터페이스의 구현체를 만들 수도 있다

package org.springframework.core.convert.converter;

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

위의 컨버터 인터페이스는 스프링에서 제공하는 확장 가능한 컨버터 인터페이스다, 즉 이 컨버터 인터페이스를 구현해서 등록하게 되면 추가적인 타입 변환을 할 수 있는 것이다 (과거에는 PropertyEditor를 이용해서 타입을 변환하였으나, 동시성 문제 때문에 현재는 Converter를 대부분 사용하고 있다고 한다)

 

• Integer -> String

컨버터 인터페이스를 구현하기 위한 방법은 굉장히 단순하다 S -> T 로 변환되는 개념이기 때문에 아래와 같이 작성하게 되면 숫자를 문자로 변환하는 타입 컨버터가 된다(단순히 동작 원리의 이해를 위한 것이지, 아래와 같이 단순하게 숫자 -> 문자의 경우 기본 컨버터가 지원한다는 점 참고)

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

 

• String -> Object

단순히 숫자를 문자로 변환하는 것을 넘어서 문자를 내가 원하는 객체의 형식으로 컨버팅 할 수도 있다

@Getter
@EqualsAndHashCode
public class IpPort {

    private String ip;
    private int port;
    
    public IpPort(String ip, int port) {
        this.ip = ip;
        this.port = port;
    }
}
@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);
    }
    
}

위와 같이 작성할 경우 "127.0.0.1:8080"이라는 문자가 들어왔을 때 IpPort객체의 ip에는 : 이전의 ip가, port에는 : 이후의 포트번호가 들어가게 된다

 

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 인터페이스의 경우 단순히 컨버팅의 가능여부와 컨버팅 기능을 제공한다

@Configuration
public class WebConfig implements WebMvcConfigurer {
  
    @Override
    public void addFormatters(FormatterRegistry registry) {
         registry.addConverter(new IntegerToStringConverter());
         registry.addConverter(new StringToIpPortConverter());
    }
    
}

사용하기 위해서는 Configuration 클래스를 하나 만들어 위와 같이 만든 컨버터를 직접 등록하면 스프링이 기본적으로 제공하는 컨버터보다 높은 우선순위를 가지고 있기 때문에 해당 컨버터들이 작동하게 된다

 

View Template

뷰 템플릿에 렌더링 하는 과정에 컨버터를 사용할 수도 있다, 타임리프의 경우 ${...}의 변수 표현식을 사용하여 내용을 렌더링하는데 컨버팅 서비스를 적용하기 위해서는  ${{...}}로 괄호를 하나 더 붙여 사용하게 된다

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body> 

     <span th:text="${ipPort}"></span>
     <span th:text="${{ipPort}}"></span>
     
</body>
</html>

변수 표현식 : ${ ... }

컨버전 서비스 적용 : ${{ ... }}

 

th:field를 사용한다면 괄호의 개수와 상관없이 컨버전 서비스가 적용된다

 

Formatter

컨버터의 경우 입력과 출력 타입에 제한이 없는 범용적인 타입 변환 기능을 제공한다, 하지만 대부분의 경우 문자를 다른 타입으로 변환하거나 다른 타입을 문자로 변환하는 상황이 대부분이다

ex. 1000(숫자) -> "1,000"(문자)

ex. "2021-01-01 01:01:11"(문자) -> 2021-01-01 01:01:11(날짜)

 

이럴 때 사용하는 것이 바로 포맷터인데, 포맷터 또한 컨버터의 개념 안에 속한다. 즉 특정한 포맷에 맞추어 문자로 출력하거나 그 반대의 역할을 하는 기능에 특화되어 있는 기능으로 특별한 버전의 컨버터라고 이해하면 된다

 

• Converter : 범용적인 사용 (객체 -> 객체)

• Fomatter : 문자에 특화 (객체 -> 문자, 문자 -> 객체) + 현지화(Locale)

 

//Fomatter 인터페이스 실제 코드
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> {
}

여기서 주목해야 할 점이 현지화라는 정보를 같이 넣을 수 있다는 것인데, 예를 들어 숫자를 표기할 때 1,000와 같은 표기법 혹은 날짜의 표기법이 지역마다 다를 수 있다 이를 Locale의 정보를 입력하여 매우 간편하게 특정 포맷으로 변환하는 것은 물론 1000(숫자) -> "1,000"(문자)와 같은 변환은 사실 생각보다 구현하는 것이 복잡하고 또 번거로운 작업이 수반되기 때문에 이러한 작업을 최소화할 수 있다

@Slf4j
public class MyNumberFormatter implements Formatter<Number> {

     //"1,000" -> 1000
     @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);
     }
     
     //1000 -> "1,000"     
     @Override
     public String print(Number object, Locale locale) {
          log.info("object={}, locale={}", object, locale);
          return NumberFormat.getInstance(locale).format(object);
     }
     
}

포맷터의 parse는 문자를 숫자로 변환하고, print의 경우 객체를 문자로 변환한다

 

포맷터 또한 Configuration 클래스에 등록하여 사용하면 된다

@Configuration
public class WebConfig implements WebMvcConfigurer {

     @Override
     public void addFormatters(FormatterRegistry registry) {
          registry.addFormatter(new MyNumberFormatter());
     }
     
}

 

@Formatter

스프링은 자바에서 기본으로 제공하는 타입들에 대해 수 많은 포맷터를 기본으로 제공한다, 하지만 포맷터의 경우 형식이 지정되어 있기 때문에 객체의 각 필드마다 다른 형식의 포맷을 지정하기는 어렵다, 이를 해결하기 위해서 스프링은 애노테이션 기반의 포맷터를 제공한다, 본글에서는 기본으로 제공하는 @NumberFormat과 @DateTimeFormat을 살펴보고자 한다

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

이렇게 각각의 필드에 애노테이션을 달고 패턴을 입력해주게 되면 해당 포맷으로 변환된다

 

JSON과 컨버팅

메시지 컨버터(HttpMessageConverter)의 경우 컨버전 서비스가 적용되지 않는다, 특히 객체를 JSON으로 변환할 때 메시지 컨버터를 사용하는데 메시지의 컨버터의 역할을 생각해보자면 HTTP 메시지 바디의 내용을 객체로 변환하거나 그 반대의 경우를 수행하는 게 제 기능이다. 예를 들어 JSON으로 변환하는 경우 메시지 컨버터는 내부에서 Jackson과 같은 라이브러리를 사용한다 즉 이 과정에서 변환하는 과정은 해당 라이브러리가 담당하는 것이지  컨버전 서비스와는 아무런 관계가 없다는 것이다, 그러하여 만약 이와 같은 작업에 포맷터를 적용하고 싶다면 해당 라이브러리가 제공하는 설정을 통해서 포맷을 설정하여야 한다

 

핵심정리

편리하게 타입을 변환할 수 있도록 스프링은 타입 컨버터라는 기능을 제공한다. 이를 통하여 숫자에서 문자로, 문자에서 숫자로 편리하게 타입을 변환할 수 있으며 직접 컨버터 인터페이스의 구현체를 구현하여 컨버팅 하고자 하는 양식을 만들 수도 있다, 또한 컨버전 서비스라는 기능을 이용하여 컨버터를 한 번에 묶어 사용하고 관리할 수 있다

 

포맷터의 경우 특별한 컨버터라고 이해하면 쉬운데, 예를 들어 1000이라는 숫자를 "1,000"와 같이 특정한 포맷으로 바꾸고 싶을 때 편리하게 사용할 수 있다, 여기에 Locale이라는 데이터를 더하여 해당 지역에서 사용하는 포맷을 적용시킬 수 있다. 이러한 포맷터의 경우 기본적으로 지정한 형식에 따르기 때문에 각각의 필드에 다른 형식을 적용하기 어려운데 이를 해결하기 위하여 스프링은 애노테이션으로 된 포맷터를 제공하여 각각의 필드에 패턴을 지정하여 해당 포맷으로의 변환을 가능케 해준다

 

JSON의 경우 메시지 컨버터가 작동하여 내부에서 해당 라이브러리를 사용하기 때문에 컨버전 서비스와 전혀 관계가 없으며, 이러한 경우에도 포맷을 설정하고 싶다면 해당 라이브러리가 제공하는 설정을 따라 포맷을 적용하여야 한다

 

 

더보기

개인 학습을 위해 작성되는 글입니다.

제가 잘못 알고 있는 점에 대한 지적 / 더 나은 방향에 대한 댓글을 환영합니다.

 

참조 링크:

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard

 

 

반응형
댓글