본문 바로가기
Spring/스프링 MVC 활용

#10 스프링 타입 컨버터

by 히포파타마스 2021. 8. 3.

스프링 타입 컨버터

 

개발을 하다 보면, 타입을 변환해야 하는 경우가 상당히 많다.

개발자가 이런 타입 변환 작업을 하나하나 해야 한다면, 매우 힘들 것이다.

 

스프링은 타입 변환이 필요하면 스프링 타입 컨버터를 이용해서 타입 변환을 필요한 곳에 자동으로 처리해준다.

 

스프링 타입 컨버터에 의한 변환은 다음과 같은 경우에 적용된다.

 

● 스프링 MVC 요청 파라미터

◎ @RequestParam, @ModelAttribute, @PathVariable

 

● 뷰 렌더링

 

● @Value 등으로 YML 정보 읽기

 

생각해보면 @ModelAttribute 같은 애노테이션을 사용하면 파라미터가 자동으로 내가 지정한 객체의 필드의 타입에 맞춰서 넣어진다.

이처럼 문자 → 특정 타입의 변환 과정이 자동으로 이루어지는 것은 스프링 타입 컨버터가 적용되었기 때문이다.

 

 

 

 

1. 타입 컨버터 - Converter

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

 

스프링이 제공하는 컨버터를 사용하려면 org.springframework.core.convert.converter.Converter 인터페이스를 구현하면 된다.

 

[컨버터 인터페이스]

package org.springframework.core.convert.converter; 

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

convert는 S를 T 타입으로 변환하는 메서드이다.

 

위 예제의 컨버터 인터페이스로 사용자 정의 컨버터를 만들어 본다.

구체적으로는 127.0.0.1:8080과 같은 IP, PORT를 입력하면 IpPort 객체로 변환하는 컨버터를 만들어 본다.

 

[IpPort]

@Getter
@EqualsAndHashCode
public class IpPort {
	private String ip;
	private int port;
	
	public IpPort(String ip, int port) {
		this.ip = ip;
		this.port = port;
	} 
}

롬복의 @EqualsAndHashCode를 넣으면 모든 필드를 사용해서 equals(), hashcode()를 생성한다.

따라서 모든 필드의 값이 같다면 a.equals(b)의 결과가 참이 된다.

 

[StringToIpPortConverter]

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

문자를 받아 IpPort 객체로 변환한다. 

 

[IpPortToStringConverter]

@Slf4j
public class IpPortToStringConverter implements Converter<IpPort, String> {

	@Override
	public String convert(IpPort source) {
		log.info("convert source={}", source);
		return source.getIp() + ":" + source.getPort(); 
	}
}

IpPort를 문자 형태로 변환한다.

 

 

 

■ 컨버전 서비스 - ConversionService

컨버터 객체를 직접 사용해서 타입 변환을 하는 것은 매우 불편하다. 

스프링은 개별 컨버터를 모아두고 그것들을 묶어서 편리하게 사용할 수 있는 기능을 제공하는데, 이것이 바로 컨버전 서비스(ConversionService)이다.

 

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

예를 들어, @ModelAttribute를 사용할 때, 이 기능을 사용해서 타입을 변환한다.

 

[WebConfig - 컨버터 등록]

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

스프링은 내부에서 ConversionService를 제공한다.

우리는 WebConfigurer가 제공하는 addFormatter()를 사용해서 addConverter()로 추가하고 싶은 컨버터를 등록하면 된다.

이러면 스프링은 내부에서 사용하는 ConversionService에 컨버터를 추가해준다.

 

[컨트롤러 - 컨버터 사용]

@GetMapping("/ip-port")
public String ipPort(@RequestParam IpPort ipPort) {
	System.out.println("ipPort IP = " + ipPort.getIp());     
	System.out.println("ipPort PORT = " + ipPort.getPort());
	return "ok"; 
}

@RequestParam을 사용해서 파라미터를 IpPort 객체로 받는다.

원래는 문자를 IpPort 객체로 변환하는 컨트롤러는 없기 때문에 @RequestParam을 사용해서 파라미터를 IpPort로 받을 수 없다.

때문에 @ModelAttribute를 사용해야 하지만 문자를 IpPort로 변환하는 컨버터를 추가해주었기 때문에 @RequestParam을 사용해도 문자를 IpPort로 변환할 수 있다.

 

[컨버터 사용 - 실행 결과]

StringToIpPortConverter    : convert source=127.0.0.1:8080 
ipPort IP = 127.0.0.1
ipPort PORT = 8080

StirngToIpPortConverter가 실행됬음을 알 수 있고 정상적으로 파라미터가  IpPort로 변환되었다.

 

 

□ 뷰 템플릿에 컨버터 적용

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

 

[컨트롤러 - 뷰 템플릿 컨버터 적용]

@GetMapping("/converter-view")
public String converterView(Model model) {        
	model.addAttribute("number", 10000);
	model.addAttribute("ipPort", new IpPort("127.0.0.1", 8080));
	return "converter-view"; 
}

모델에 숫자와 IpPort를 담아 뷰에 전달한다.

 

[뷰 - 뷰 템플릿 컨버터 적용]

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

<body> 
<ul>
	<li>${number}: <span th:text="${number}" ></span></li>
	<li>${{number}}: <span th:text="${{number}}" ></span></li> 
	<li>${ipPort}: <span th:text="${ipPort}" ></span></li>
	<li>${{ipPort}}: <span th:text="${{ipPort}}" ></span></li> 
</ul>

</body>
</html>

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

타임리프는 스프링이 제공하는 컨버전 서비스를 사용하기 때문에, 따로 컨버터들을 다시 등록할 필요는 없다.

 

[실행 결과 - 뷰 템플릿 컨버터 적용]

• ${number}: 10000
• ${{number}}: 10000
• ${ipPort}: hello.typeconverter.type.IpPort@59cb0946
• ${{ipPort}}: 127.0.0.1:80

Integer같은 경우 타임리프가 숫자를 문자로 자동으로 변환하기 때문에 컨버터를 적용할 때와 하지 않을 때가 같다.

IpPort는 컨버터가 적용되지 않으면 객체 정보가 그대로 문자로 출력된다. 컨버터를 적용시키면 IpPort 객체가 의도한 대로 문자로 잘 변형된 것을 확인할 수 있다.

 

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

즉, th:field="{IpPort}" 와 같이 사용하면 IpPort에 자동으로 타입 컨버터가 적용된다.

 

 

 

 

2. 포매터 - Formatter

개발을 하다보면 숫자 1000을 문자로 바꿀 때, "1,000"와 같이 특별한 포맷을 적용시키고 싶을 수 있다.

숫자를 문자로 바꾸는 경우만 해도 통화, 날짜 시간 등 여러 포맷이 존재한다.

즉, 객체를 문자로 변환할 때나 문자를 객체로 변환할 때 문자에 특별한 포맷이 적용되거나 적용해야 하는 경우가 있다.

 

포매터(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> { 
}

● String print(T object, Local local) : 객체 T를 Local을 적용해, 문자로 변경한다.

● T parse(String text, Local local) : 문자 text를 Local을 적용해, 객체로 변경한다.

 

숫자 1000을 문자 "1,000"로 변환(1000 단위로 쉼표)하고, 반대의 기능도 하는 포매터를 만들어 본다.

 

[MyNumberFormatter]

@Slf4j
public class MyNumberFormatter implements Formatter<Number> {
	@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); 
}

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

자바가 기본으로 제공하는 NumberFormat을 사용하면 "1,000"과 같은 포맷 형식을 적용할 수 있다.

NumberFormat은 Local 정보를 활용해서 나라별로 다른 숫자 포맷을 만들어준다.

 

Number 타입은 Integer, Long과 같은 숫자 타입의 부모 클래스이다.

 

 

 

■ FormattingConversionService

컨버전 서비스에는 컨버터만 등록할 수 있다. 포맷터는 등록할 수 없다.

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

 

FormattingConversionService는 포맷터를 지원하는 컨버전 서비스이다.

※ 스프링 부트는 DefaultFormattingConversionService(구현체)를 상속받은 WebConversionService를 내부에서 사용한다.

 

[WebConfig - 포맷터 추가]

@Configuration
public class WebConfig implements WebMvcConfigurer {
	@Override
	public void addFormatters(FormatterRegistry registry) {
		registry.addConverter(new StringToIpPortConverter());
		registry.addConverter(new IpPortToStringConverter());
		
		//추가
		registry.addFormatter(new MyNumberFormatter());
	}
}

컨버터와 비슷하게 addFormatters에 addFormatter로 포맷터를 컨버전 서비스(WebConversionService)에 추가할 수 있다.

 

 

 

■ 스프링 기본 제공 포맷터 - @NumberFormat & @DateTimeFormat

포맷터는 기본 형식이 지정되어 있기 때문에, 객체의 각 필드마다 다른 형식으로 포맷을 지정하기는 어렵다.

 

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

 

● @NumberFormat : 숫자 관련 형식 지정 포맷터 사용

● @DateTimeFormat :  날짜 관련 형식 지정 포매서 사용

 

[@NumberFormat & @DateTimeFormat 적용]

@Data
static class Form {
	@NumberFormat(pattern = "###,###") 
	private Integer number;

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

각 애노테이션에 포맷을 지정해주면 해당 포맷대로 포맷터가 적용된다.

 

예를 들어, 위 예시의 Form 객체를 뷰 템플릿에 적용해서 렌더링 하면 다음과 같은 결과를 얻을 수 있다.

 

[뷰 렌더링 결과 - @NumberFormat & @DateTimeFormat]

• ${form.number}: 10000
• ${{form.number}}: 10,000
• ${form.localDateTime}: 2021-01-01T00:00:00
• ${{form.localDateTime}}: 2021-01-01 00:00:00

 

 

※ 메시지 컨버터(HttpMessageConverter)에는 컨버전 서비스가 적용되지 않는다.

객체를 JSON으로 변환할 때 메시지 컨버터가 사용되는데 변환 기능은 오로지 메시지 컨버터 내부에서 사용되는 라이브러리(Jackson)에 의존한다.

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

'Spring > 스프링 MVC 활용' 카테고리의 다른 글

Spring ArgumentResolver  (0) 2022.08.19
#11 파일 업로드  (0) 2021.08.03
#9 API 예외 처리  (0) 2021.08.03
#8 예외 처리와 오류 페이지  (0) 2021.08.02
#7 로그인 처리 - 필터, 인터셉터  (0) 2021.08.01

댓글