본문 바로가기
[ JAVA ]/JAVA Spring

[ Spring ] Spring Type Converter - Formatter

by 환이s 2024. 1. 30.
728x90


이전 포스팅에 이어서 진행하겠습니다.

Converter에 대한 개념을 먼저 알아보시면 이해하는데 도움이 될 거 같습니다.

 

 

[ Spring ] Spring Type Converter

이전 포스팅에서 Spring Exception Handling에 대해 알아봤습니다. 오늘은 Spring Type Converter 정리를 해보겠습니다. Spring Type Converter 문자를 숫자로 변환하거나, 반대로 숫자를 문자로 변환해야 하는 것처

drg2524.tistory.com


Formatter

 

Converter는 입력과 출력 타입에 제한이 없는, 범용 타입 변환 기능을 제공한다.

 

이번에는 일반적인 웹 애플리케이션 환경을 생각해 보자.

불린 타입을 숫자로 바꾸는 것 같은 범용 기능보다는 개발자 입장에서 문자를 다른 타입으로 변환하거나,

다른 타입을 문자로 변환하는 상황이 대부분이다.

 

앞서 살펴본 예제들을 떠올려 보면 문자를 다른 객체로 변환하거나 객체를 문자로 변환하는 일이 대부분이다.


 

■ 웹 애플리케이션에서 객체를 문자로, 문자를 객체로 변환하는 예

 

  • 화면에 숫자를 출력해야 하는데, Interger -> String 출력 시점에 숫자 1000 -> 문자 "1,000" 이렇게 1000 단위에 쉼표를 넣어서 출력하거나, 또는 "1,000"라는 문자를 1000이라는 숫자로 변경해야 한다.
  • 날짜 객체를 문자인 "2024-01-30 10:50:11"과 같이 출력하거나 또는 그 반대의 상황

 

■ Locale

 

여기에 추가로 날짜 숫자의 표현 방법은 Locale 현지화 정보가 사용될 수 있다.

 

이렇게 객체를 특정한 포맷에 맞추어 문자로 출력하거나

또는 그 반대의 역할을 하는 것에 특화된 기능이 바로 포맷터(Formatter)이다.

 

포맷터는 컨버터의 특별한 버전으로 이해하면 된다.

 

■ Converter vs Formatter

 

  • Converter는 범용(객체 -> 객체)
  • Formatter는 문자에 특화(객체 -> 문자, 문자 -> 객체) + 현지화(Locale)
    • Converter의 특별한 버전

Formatter 사용법

 

포맷터(Formatter)는 객체를 문자로 변경하고, 문자를 객체로 변경하는 두 가지 기능을 모두 수행한다.

 

  • String print(T object, Locale locale) : 객체를 문자로 변경한다.
  • T parse(String text, Locale locale) : 문자를 객체로 변경한다.

 

  • Formatter Interface
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"으로 그러니까, 1000 단위로 쉼표가 들어가는 포맷을 적용해 보자.

그리고 그 반대도 처리해 주는 포맷터를 만들어보자.

 

  • MyNumberFormatter
import lombok.extern.slf4j.Slf4j;
import org.springframework.format.Formatter;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Locale;

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

 

"1,000"처럼 숫자 중간의 쉼표를 적용하려면 자바가 기본으로 제공하는 NumberFormat 객체를 사용하면 된다.

이 객체는 Locale 정보를 활용해서 나라별로 다른 숫자 포맷을 만들어준다.

 

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

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

 

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

 

잘 동작하는지 Test Code를 만들어보자.

 

  • MyNumberFormatterTest
import org.junit.jupiter.api.Test;
import java.text.ParseException;
import java.util.Locale;
import static org.assertj.core.api.Assertions.*;

class MyNumberFormatterTest {

     MyNumberFormatter formatter = new MyNumberFormatter();
     
     @Test
     void parse() throws ParseException {
    	 Number result = formatter.parse("1,000", Locale.KOREA);
    	 assertThat(result).isEqualTo(1000L); //Long 타입 주의
     }
     
     @Test
     void print() {
    	 String result = formatter.print(1000, Locale.KOREA);
    	 assertThat(result).isEqualTo("1,000");
     }
}

 

parse()의 결과가 Long이기 때문에 isEqualTo(1000L)을 통해 비교할 때 마지막에 L을 넣어주어야 한다.

 

// 실행 결과 Log

MyNumberFormatter - text=1,000, locale=ko_KR
MyNumberFormatter - object=1000, locale=ko_KR

 

참고 )

Spring은 용도에 따라 다양한 방식의 포맷터를 제공한다.

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

자세한 내용은 공식 문서를 참고하자.

 

 

Spring Field Formatting :: Spring Framework

As discussed in the previous section, core.convert is a general-purpose type conversion system. It provides a unified ConversionService API as well as a strongly typed Converter SPI for implementing conversion logic from one type to another. A Spring conta

docs.spring.io


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

 

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

그런데 생각해 보면 포맷터는 객체 -> 문자, 문자 ->  객체로 변환하는 특별한 컨버터일 뿐이다.

 

포맷터를 지원하는 컨버전 서비스를 사용하면 컨버전 서비스에 포맷터를 추가할 수 있다.

내부에서 어댑터 패턴을 사용해서 FormatterConverter처럼 동작하도록 지원한다.

 

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

DefaultFormattingConversionServiceFormattingConversionService에 기본적인 통화, 숫자 관련 몇 가지 기본 포맷터를 추가해서 제공한다.

 

import hello.typeconverter.converter.IpPortToStringConverter;
import hello.typeconverter.converter.StringToIpPortConverter;
import hello.typeconverter.type.IpPort;
import org.junit.jupiter.api.Test;
import org.springframework.format.support.FormattingConversionService;
import static org.assertj.core.api.Assertions.assertThat;

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:9090",IpPort.class);
         assertThat(ipPort).isEqualTo(new IpPort("127.0.0.1", 9090));
         
         //포맷터 사용
         assertThat(conversionService.convert(1000, String.class)).isEqualTo("1,000");
         assertThat(conversionService.convert("1,000", Long.class)).isEqualTo(1000L);
     }
}

 

■ DefaultFormattingConversionService 상속 관계

 

FormattingConversionServiceConversionService 관련 기능을 상속받기 때문에

결과적으로 컨버터도 포맷터도 모두 등록할 수 있다.

 

그리고 사용할 때는 ConversionService가 제공하는 Convert를 사용하면 된다.

 

추가로 Spring Boot는 DefaultFormattingConversionService를 상속받은 WebConversionService를 내부에서 사용한다.

 


Formatter 적용

 

Formatter를 웹 애플리케이션에 적용해 보자.

 

  • WebConfig - 수정
import hello.typeconverter.converter.IntegerToStringConverter;
import hello.typeconverter.converter.IpPortToStringConverter;
import hello.typeconverter.converter.StringToIntegerConverter;
import hello.typeconverter.converter.StringToIpPortConverter;
import hello.typeconverter.formatter.MyNumberFormatter;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

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

 

여기서 주의할 점

StringToIntegerConverter, IntegerToStringConverter를 꼭 주석처리 하자.

MyNumberFormatter도 숫자 -> 문자, 문자 -> 숫자로 변경하기 때문에 둘의 기능이 겹친다.

우선순위는 컨버터가 우선하므로 포맷터가 적용되지 않고, 컨버터가 적용된다.

 

■ 실행 - 객체 -> 문자

 ${number}: 10000
 ${{number}}: 10,000

 

컨버전 서비스를 적용한 결과 MyNumberFormatter가 적용되어서 10,000 문자가 출력된 것을 확인할 수 있다.

 

 

■ 실행 - 문자 -> 객체

 

// 실행 Log

MyNumberFormatter : text=10,000, locale=ko_KR
data = 10000

 

"10,000"이라는 포맷팅 된 문자가 Integer 타입의 숫자 10000으로 정상 변환 된 것을 확인할 수 있다.


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

 

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

 

IDE에서 Formatter Interface의 구현 클래스를 찾아보면 수 많은 날짜나 시간 관련 포맷터가 제공되는 것을 확인할 수 있다.

 

그런데 포맷터는 기본 형식이 지정되어 있기 때문에,

객체의 각 필드마다 다른 형식으로 포맷을 지정하기는 어렵다.

 

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

 

  • @NumberFormat : 숫자 관련 형식 지정 포맷터 사용
    • NumberFormatAnnotationFormatterFactory
  • @DateTimeFormat : 날짜 관련 형식 지정 포맷터 사용
    • Jsr310DateTimeFormatAnnotationFormatterFactory

 

예제를 통해서 알아보자.

 

  • FormatterController
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.NumberFormat;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import java.time.LocalDateTime;

@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 "formatterForm";
     }
     
     @PostMapping("/formatter/edit")
     public String formatterEdit(@ModelAttribute Form form) {
     
    	 return "formatterView";
     }
     
     @Data
     static class Form {
     
         @NumberFormat(pattern = "###,###")
         private Integer number;
         
         @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
         private LocalDateTime localDateTime;
         
     }
}

 

 

  • formatterForm.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
 <meta charset="UTF-8">
 <title>Title</title>
</head>
<body>
    <form th:object="${form}" th:method="post">
     number <input type="text" th:field="*{number}"><br/>
     localDateTime <input type="text" th:field="*{localDateTime}"><br/>
     <input type="submit"/>
    </form>
</body>
</html>

 

 

  • formatterView.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
 <meta charset="UTF-8">
 <title>Title</title>
</head>
<body>
    <ul>
         <li>${form.number}: <span th:text="${form.number}" ></span></li>
         <li>${{form.number}}: <span th:text="${{form.number}}" ></span></li>
         <li>${form.localDateTime}: <span th:text="${form.localDateTime}" ></span></li>
         <li>${{form.localDateTime}}: <span th:text="${{form.localDateTime}}" ></span></li>
    </ul>
</body>
</html>

 

실행해 보면 지정한 포맷으로 출력된 것을 확인할 수 있다.

 

 <!-- 결과 -- >
 ${form.number}: 10000
 ${{form.number}}: 10,000
 ${form.localDateTime}: 2021-01-01T00:00:00
 ${{form.localDateTime}}: 2021-01-01 00:00:00

 

정리해 보면

컨버터를 사용하든, 포맷터를 사용하든 등록 방법은 다르지만,

사용할 때는 컨버전 서비스를 통해서 일관성 있게 사용할 수 있다.

 

주의! )

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

특히 객체를 JSON으로 변환할 때 메시지 컨버터를 사용하면서 이 부분을 많이 오해하는데,
HttpMessageConverter의 역할은 HTTP 메시지 바디의 내용을 객체로 변환하거나
객체를 HTTP 메시지 바디에 입력하는 것이다.

예를 들어서 JSON을 객체로 변환하는 메시지 컨버터는 내부에서
Jackson 같은 라이브러리를 사용한다.

객체를 JSON으로 변환한다면 그 결과는 이 라이브러리에 달린 것이다.
따라서 JSON 결과로 만들어지는 숫자나 날짜 포맷을 변경하고 싶으면
해당 라이브러리가 제공하는 설정을 통해서 포맷을 지정해야 한다.

결과적으로 이것은 컨버전 서비스와 전혀 관계가 없다.

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

마치며

 

오늘은 Formatter에 대해 알아봤습니다.

다음 포스팅에서 뵙겠습니다.

 

위 포스팅은 김영한님의 Spring MVC 2편 - 백엔드 웹 개발 활용  강의를 참고했습니다.

 

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 강의 - 인프런

웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있

www.inflearn.com

 

728x90