스프링 타입 컨버터

Lilac-_-P·2023년 4월 24일
0

스프링 MVC

목록 보기
14/15

타입 변환이란?

스프링 MVC 컨트롤러에서는 메서드의 파라미터로 다양한 타입의 데이터를 받을 수 있다. 이게 가능했던 이유는 스프링의 ArgumentResolver가 메서드에 필요한 파라미터 값을 만들어서 넣어주기 때문이었는데, 그 때의 기억을 되살려보자.

HTTP 요청 메시지로 넘어오는 데이터들은 기본적으로 String 타입의 데이터이다.(생각해보면, 네트워크 상으로 들어오는 데이터 자체는 바이트 데이터겠지만, WAS 단에서 서블릿을 이용해서 HttpServlet 같은 객체로 변환되면, 그때 바이트 데이터가 String으로 변환되어 있지 않을까? 라고 추측해본다.)

스프링의 ArgumentResolver가 컨트롤러 메서드의 파라미터로 들어가는 값들을 만들어주는 것은 맞지만, HTTP 메시지가 String 타입으로 넘어오기 때문에, 메서드의 파라미터 값을 String이 아닌 다른 타입으로 받고자 한다면 String 타입을 원하는 메서드 파라미터의 타입에 맞게 변환하는 과정이 필요하다. 만약 개발자가 직접 하나하나 타입 변환을 해야한다면, 너무 할일이 많아질 것이다.

생각해보면, 우리는 이미 String 타입으로 주어진 숫자를 컨트롤러에서 Integer 같은 타입으로 받을 수 있었다. 이는 스프링이 중간에서 타입 변환기를 사용해서 타입을 적절히 변환해주었기 때문이다. 스프링은 자주 사용될 수 있는 타입 변환기를 이미 만들어서 개발자들에게 제공한다. 동시에, 사용자가 원하는 타입 변환이 있을 경우 추가적으로 이를 구현해서 사용할 수 있도록 컨버터 인터페이스를 제공한다.

컨버터 인터페이스

컨버터 인터페이스는 다음과 같다.

public interface Converter<S, T> {

	@Nullable
	T convert(S source);
}

매우 심플하다. S 타입의 데이터를 T 타입의 데이터로 변환하는 함수 하나만 작성하면 된다. 사용자가 정의한 타입을 컨버터에 사용하려고 하더라도, convert() 메서드 내부에서 적절히 변환하는 함수를 작성해주면 된다.

그런데 만약 매번 타입 컨버터를 하나하나 작성해서 각 상황에 맞는 컨버터 클래스로 객체를 생성해서 변환을 한다면, 그냥 컨버터의 convert() 메서드 내의 로직을 직접 작성하는 것과 다를바가 없다. 다양한 종류의 컨버터를 등록하고 관리하면서 편리하게 변환기능을 제공하는 역할을 하는 무언가가 필요하다.

컨버전 서비스 - ConversionService

타입 컨버터를 하나하나 직접 찾아서 타입 변환에 사용하는 것은 매우 불편하다. 그래서 스프링은 다양한 종류의 컨버터를 모아두고 이들을 묶어서 편리하게 사용할 수 있는 컨버전 서비스를 제공한다.

컨버전 서비스도 인터페이스로 제공되는데, 아래의 코드를 보자.

public interface ConversionService {

	boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType);

	boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType);

	@Nullable
	<T> T convert(@Nullable Object source, Class<T> targetType);

	@Nullable
    Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType);

}

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

컨버전 서비스를 사용하면, 컨버터를 등록할 때는 어떤 타입 컨버터를 등록하는지 명확하게 알아야하지만 컨버터를 사용할 때는 어떤 타입 컨버터가 필요한지 전혀 몰라도 된다. 타입 컨버터들은 모두 컨버전 서비스 내부에 숨어서 제공된다. 따라서 타입 변환을 원하는 사용자는 컨버전 서비스 인터페이스에만 의존하면 된다.

참고.
타입 컨버터를 등록하는 일과 타입 컨버터를 사용하는 일은 명확하게 분리할 수 있는 관심사이다.
그래서 스프링은 타입 컨버터를 사용하는 일은 ConversionService 인터페이스로, 타입 컨버터를 등록하는 일은 ConverterRegistry 인터페이스로 구현할 수 있도록 각각 인터페이스를 제공한다.
이는 ISP(Interface Segregation Principle - 인터페이스 분리 원칙)을 충족하는 설계이다.

스프링의 ArgumentResolver는 메서드 파라미터를 만들 때 타입 변환이 필요하다면, 내부적으로 ConversionService를 사용해서 타입을 변환한다. 예를 들어 @RequestParam이나 @ModelAttribute 같은 곳에서 이 기능을 사용해서 타입을 변환한다.

포맷터 - Formatter

위에서 설명한 컨버터는 입력과 출력 타입에 제한이 없는 범용 타입 변환 기능을 제공한다.

여기서 일반적인 웹 애플리케이션 환경을 생각해보자. 일반적인 웹 애플리케이션 환경에서는 문자를 다른 타입으로 변환하거나, 다른 타입을 문자로 변환하는 상황이 대부분이다.

웹 애플리케이션 환경에서 문자와 다른 타입 간의 변환에는 형식이나 포맷이 있을 수도 있다. 예를 들면, 숫자 1000을 문자로 출력할 때는 1,000과 같이 쉼표를 추가해서 출력하는 것처럼 말이다. 혹은 변환 과정에 지역 정보(Locale)가 반영되어야하는 경우도 있을 수 있다.

이렇게 객체를 특정한 포맷에 맞추어 문자로 출력하거나, 특정 포맷의 문자를 자바 코드 상에서 사용할 수 있는 객체로 변환하는 기능을 스프링은 포맷터로 제공한다. 포맷터는 컨버터의 특별한 버전이라고 이해하면 편하다.

참고.
컨버터 : 범용 (객체 <-> 객체)
포맷터 : 문자에 특화 (문자 <-> 객체) + 현지화(Locale)

아래의 포맷터 인터페이스 코드를 보자.

public interface Formatter<T> extends Printer<T>, Parser<T> {

}

public interface Printer<T> {
	String print(T object, Locale locale);
}

public interface Parser<T> {
	T parse(String text, Locale locale) throws ParseException;
}

위와 같이 포맷터는 객체를 문자로 변환하거나, 문자를 객체로 변환하는 두가지 기능을 수행한다.

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

컨버터는 컨버전 서비스에 등록하여 관리, 사용을 편리하게 할 수 있었다. 그럼 포맷터의 경우는 어떨까?

원칙적으로는 컨버전 서비스에는 컨버터만 등록할 수 있고, 포맷터를 등록할 수 없다.
그런데 생각해보면 포맷터는 문자 <-> 객체 변환을 수행하는 특별한 컨버터일 뿐이다. 그래서 스프링은 포맷터를 지원하는 컨버전 서비스를 사용하면 컨버전 서비스에도 포맷터를 추가할 수 있게 해준다. 포맷터를 지원하는 컨버전 서비스는 내부에서 어댑터 패턴을 사용해서 포맷터가 컨버터처럼 동작하도록 지원한다.

참고.
동일한 기능을 하는 컨버터와 포맷터가 동시에 등록되어있을 경우, 컨버터가 우선순위가 높다.

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

참고.
스프링이 제공하는 @NumberFormat와 @DateTimeFormat을 사용하면, 객체의 각 필드마다 다른 형식으로 포맷을 지정할 수 있다.

주의할 점

메세지 컨버터(HttpMessasgeConverter)에는 컨버전 서비스가 적용되지 않는다.
메세지 컨버터는 보통 객체 <-> JSON 변환에서 사용되는데, 메세지 컨버터의 역할은 HTTP 메시지 바디의 내용을 객체로 변환하거나 객체를 HTTP 메시지 바디에 입력하는 JSON 형식으로 변환하는 것이다. 따라서 메세지 컨버터는 내부에서 Jackson 같은 라이브러리를 사용한다. 만약 JSON으로 변환되는 결과의 포맷을 변경하고 싶다면 해당 라이브러리(Jackson)가 제공하는 설정을 통해서 포맷을 지정하는게 맞다.

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

profile
열심히 하자

0개의 댓글