1. 스프링 타입 컨버터 소개

  • 문자를 숫자로 변환하거나, 반대로 숫자를 문자로 변환해야 하는 것 처럼 애플리케이션을 개발하다 보면 타입을 변환해야 하는 경우가 상당히 많다.
  • 하지만 스프링은 자동으로 변환해준다.

HelloController

  • @GetMapping("/hello-v2")
    public String helloV2(@RequestParam Integer data) {
        System.out.println("data = " + data);
        return "ok";
    }
    
    • 실행 : http://localhost:8080/hello-v2?data=10
    • 결과 : “ok”
  • 이 HTTP 쿼리 스트링으로 전달하는 data=10 부분에서 10은 숫자 10이 아니라 문자 “10”이다.

  • 스프링이 제공하는 @RequestParam 을 사용하면 이 문자 10을 Integer 타입의 숫자 10으로 편리하게 받을 수 있다.

  • 이것은 스프링이 중간에서 타입을 변환해주었기 때문이다.

스프링과 타입 변환

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

  • package org.springframework.core.convert.converter;
    public interface Converter<S, T> {
    	T convert(S source);
    }
    
  • 개발자는 스프링에 추가적인 타입 변환이 필요하면 이 컨버터 인터페이스를 구현해서 등록하면 된다.

2. 타입 컨버터 - Converter

  • 타입 컨버터를 사용하려면 org.springframework.core.convert.converter.Converter 인터페이스를 구현하면 된다.

Converter 라는 이름의 인터페이스가 많으니 조심해야 한다.

사용자 정의 타입 컨버터

  • 127.0.0.1:8080 과 같은 IP, PORT를 입력하면 IpPort 객체로 변환하는 컨버터를 만들어보자

IpPort

  • “127.0.0.1:8080”
  • 롬복의 @EqualsAndHashCode 를 넣으면 모든 필드를 사용해서 equals() , hashcode() 를 생성한다. 따라서 모든 필드의 값이 같다면 a.equals(b) 의 결과가 참이 된다.

  • package hello.typeconverter.converter.type;
      
    import lombok.EqualsAndHashCode;
    import lombok.Getter;
    import org.springframework.web.bind.annotation.GetMapping;
      
    @Getter
    @EqualsAndHashCode
    public class IpPort {
        private String ip;
        private int port;
      
        public IpPort(String ip, int port) {
            this.ip = ip;
            this.port = port;
        }
    }
    

StringToIpPortConverter - 컨버터

  • 127.0.0.1:8080 같은 문자를 입력하면 IpPort 객체를 만들어 반환한다.

  • package hello.typeconverter.converter;
      
    import hello.typeconverter.converter.type.IpPort;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.core.convert.converter.Converter;
      
    @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);
        }
    }
    

ConverterTest - IpPort 컨버터 테스트 추가

  • IpPort 객체를 입력하면 127.0.0.1:8080 같은 문자를 반환한다.

  • package hello.typeconverter.converter;
      
    import hello.typeconverter.converter.type.IpPort;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.core.convert.converter.Converter;
      
    @Slf4j
    public class IpPortToStringConverter implements Converter<IpPort, String> {
      
        @Override
        public String convert(IpPort source) {
      
            log.info("convert source={}", source);
      
            return source.getIp() + ":" + source.getPort();
        }
    }
    

ConverterTest - IpPort 컨버터 테스트

  • @Test
    void stringToIpPort() {
        StringToIpPortConverter converter = new StringToIpPortConverter();
        String source = "127.0.0.1:8080";
        IpPort result = converter.convert(source);
        assertThat(result).isEqualTo(new IpPort("127.0.0.1", 8080));
    }
      
    @Test
    void ipPortToString() {
        IpPortToStringConverter converter = new IpPortToStringConverter();
        IpPort source = new IpPort("127.0.0.1", 8080);
        String result = converter.convert(source);
        assertThat(result).isEqualTo("127.0.0.1:8080");
    }
    
  • 그런데 이렇게 타입 컨버터를 하나하나 직접 사용하면, 개발자가 직접 컨버팅 하는 것과 큰 차이가 없다.
  • 타입 컨버터를 등록하고 관리하면서 편리하게 변환 기능을 제공하는 역할을 하는 무언가가 필요하다.

스프링이 제공하는 다양한 방식의 타입 컨버터

  • Converter : 기본 타입 컨버터
  • ConverterFactory : 전체 클래스 계층 구조가 필요할 때
  • GenericConverter : 정교한 구현, 대상 필드의 애노테이션 정보 사용 가능
  • ConditionalGenericConverter : 특정 조건이 참인 경우에만 실행

스프링은 문자, 숫자, 불린, Enum등 일반적인 타입에 대한 대부분의 컨버터를 기본으로 제공한다. IDE에서 Converter , ConverterFactory , GenericConverter 의 구현체를 찾아보면 수 많은 컨버터를 확인할 수 있다.

3. 컨버전 서비스 - ConversionService

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

ConversionService 인터페이스

  • package org.springframework.core.convert;
    import org.springframework.lang.Nullable;
      
    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);
    }
    
    • 컨버전 서비스 인터페이스는 단순히 컨버팅이 가능한지 확인하는 기능과 컨버팅 기능을 제공한다.

ConversionServiceTest - 컨버전 서비스 테스트 코드

  • package hello.typeconverter.converter;
      
    import hello.typeconverter.converter.type.IpPort;
    import org.assertj.core.api.Assertions;
    import org.junit.jupiter.api.Test;
    import org.springframework.core.convert.support.DefaultConversionService;
      
    import static org.assertj.core.api.Assertions.*;
      
    public class ConversionServiceTest {
      
        @Test
        void conversionService(){
            //등록
            DefaultConversionService conversionService = new DefaultConversionService();
            conversionService.addConverter(new StringToIpPortConverter());
            conversionService.addConverter(new IpPortToStringConverter());
            conversionService.addConverter(new StringToIntegerConverter());
            conversionService.addConverter(new IntegerToStringConverter());
      
            //사용
            assertThat(conversionService.convert("10", Integer.class)).isEqualTo(10);
            assertThat(conversionService.convert(10, String.class)).isEqualTo("10");
      
            IpPort result = conversionService.convert("127.0.0.1:8080", IpPort.class);
            assertThat(result).isEqualTo(new IpPort("127.0.0.1", 8080));
      
            String ipPortString = conversionService.convert(new IpPort("127.0.0.1", 8080), String.class);
            assertThat(ipPortString).isEqualTo("127.0.0.1:8080");
        }
    }
    
    • DefaultConversionService 는 ConversionService 인터페이스를 구현했는데, 추가로 컨버터를 등록하는 기능도 제공한다.

인터페이스 분리 원칙 - ISP(Interface Segregation Principle)

등록과 사용 분리

  • 컨버터를 등록할 때는 StringToIntegerConverter 같은 타입 컨버터를 명확하게 알아야 한다.
  • 반면에 컨버터를 사용하는 입장에서는 타입 컨버터를 전혀 몰라도 된다. 타입 컨버터들은 모두 컨버전 서비스 내부에 숨어서 제공된다.
  • 따라서 타입을 변환을 원하는 사용자는 컨버전 서비스 인터페이스에만 의존하면 된다.
  • 물론 컨버전 서비스를 등록하는 부분과 사용하는 부분을 분리하고 의존관계 주입을 사용해야 한다.

인터페이스 분리 원칙은 클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다.

  • DefaultConversionService 는 다음 두 인터페이스를 구현했다.
    • ConversionService : 컨버터 사용에 초점
    • ConverterRegistry : 컨버터 등록에 초점
  • 이렇게 인터페이스를 분리하면 컨버터를 사용하는 클라이언트와 컨버터를 등록하고 관리하는 클라이언트의 관심사를 명확하게 분리할 수 있다.
  • 특히 컨버터를 사용하는 클라이언트는 ConversionService 만 의존하면 되므로, 컨버터를 어떻게 등록하고 관리하는지는 전혀 몰라도 된다.
  • 결과적으로 컨버터를 사용하는 클라이언트는 꼭 필요한 메서드만 알게된다.

즉, 사용하는 사람은 어떻게 등록하는지 몰라도 된다는 말. 이렇게 인터페이스를 분리하는 것을 ISP 라 한다.

4. 스프링에 Converter 적용하기

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

WebConfig - 컨버터 등록

  • package hello.typeconverter;
      
    import ..;
      
    @Configuration
    public class WebConfig implements WebMvcConfigurer {
      
        @Override
        public void addFormatters(FormatterRegistry registry) {
            registry.addConverter(new StringToIpPortConverter());
            registry.addConverter(new IpPortToStringConverter());
            registry.addConverter(new StringToIntegerConverter());
            registry.addConverter(new IntegerToStringConverter());
        }
    }
    
  • 스프링은 내부에서 ConversionService 를 제공한다.

  • 우리는 WebMvcConfigurer 가 제공하는 addFormatters() 를 사용해서 추가하고 싶은 컨버터를 등록하면 된다.

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

    컨버터를 추가하면 추가한 컨버터가 기본 컨버터 보다 높은 우선순위를 가진다.

HelloController

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

실행

  • http://localhost:8080/ip-port?ipPort=127.0.0.1:8080
  • ipPort IP = 127.0.0.1 / ipPort PORT = 8080
  • ?ipPort=127.0.0.1:8080 쿼리 스트링이 @RequestParam IpPort ipPort 에서 객체 타입으로 잘 변환 된 것을 확인할 수 있다.

처리과정

  • @RequestParam 은 @RequestParam 을 처리하는 ArgumentResolver 인 RequestParamMethodArgumentResolver 에서 ConversionService 를 사용해서 타입을 변환한다.
  • 만약 더 깊이있게 확인하고 싶으면 IpPortConverter 에 디버그 브레이크 포인트를 걸어서 확인해보자.
    • image-20230316214847885

5. 뷰 템플릿에 컨버터 적용하기

  • 타임리프는 렌더링 시에 컨버터를 적용해서 렌더링 하는 방법을 편리하게 지원한다.
  • 이전까지는 문자를 객체로 변환했다면, 이번에는 그 반대로 객체를 문자로 변환하는 작업을 확인할 수 있다.

ConverterController

  • @Controller
    public class ConverterController {
      
        @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";
        }
    }
    
    • Model 에 숫자 10000 와 ipPort 객체를 담아서 뷰 템플릿에 전달한다.

converter-view.html

  • resources/templates/converter-view.html

  • <!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>$: <span th:text="$" ></span></li>
      <li>${ipPort}: <span th:text="${ipPort}" ></span></li>
      <li>$: <span th:text="$" ></span></li>
    </ul>
    </body>
    </html>
    

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

  • 변수 표현식 : ${…}
  • 컨버전 서비스 적용 : $

실행결과

  • image-20230316220454022

  • $ -> ipPortToStringConverter 적용
  • $ -> IntegerToStringConverter 적

폼에 적용하기

ConverterController - 코드 추가

  • package hello.typeconverter.controller;
      
    import ...;
      
    @Controller
    public class ConverterController {
      
        @GetMapping("/converter/edit")
        public String convertForm(Model model) {
            IpPort ipPort = new IpPort("127.0.0.1", 8080);
      
            Form form = new Form(ipPort);
            model.addAttribute("form", form);
      
            return "converter-form";
        }
      
        @PostMapping("/converter/edit")
        public String converterEdit(@ModelAttribute Form form, Model model) {
            IpPort ipPort = form.getIpPort();
            model.addAttribute("ipPort", ipPort);
            return "converter-view";
        }
      
        //Form 객체를 데이터를 전달하는 폼 객체로 사용한다.
        @Data
        static class Form {
            private IpPort ipPort;
      
            public Form(IpPort ipPort) {
                this.ipPort = ipPort;
            }
        }
    }
    

/converter-form.html

  • resources/templates/converter-form.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">
      th:field <input type="text" th:field="*{ipPort}"><br/>
      th:value <input type="text" th:value="*{ipPort}">(보여주기 용도)<br/>
      <input type="submit"/>
    </form>
    </body>
    </html>
    
    • 타임리프의 th:field 는 앞서 설명했듯이 id , name 를 출력하는 등 다양한 기능이 있는데, 여기에 컨버전 서비스도 함께 적용된다.
      • 템플릿 : th:field <input type="text" th:field="*{ipPort}"><br/>
      • HTML 에서 : th:field <input type="text" id="ipPort" name="ipPort" value="127.0.0.1:8080"><br/>
    • ipPort 를 그대로 쓰고 싶으면 th:value="*{ipPort}" 라고 value 값으로 넣어야 한다.

converter/edit 화면

  • image-20230316221018018

converter/edit 에서 form 전송 시

  • String 값인 “127.0.0.1:8080” 가 IpPort 로 변경된다 (IpPortToStringConverter)
  • @ModelAttribute Form form 에 IpPort 값이 담긴다.
  • Post 로 받는다.

6. 포맷터 - Formatter

  • Converter 는 입력과 출력 타입에 제한이 없는, 범용 타입 변환 기능을 제공한다.
  • 하지만 개발자 입장에서는 문자를 다른 타입으로 변환하거나, 다른 타입을 문자로 변환하는 상황이 대부분이다.

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

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

Converter vs Formatter

  • Converter 는 범용(객체 -> 객체)
  • Formatter 는 문자에 특화(객체 문자, 문자 객체) + 현지화(Locale) Converter 의 특별한 버전
    • Locale : 날짜 숫자의 표현 방법은 Locale 현지화 정보가 사용될 수 있다.

포맷터 - Formatter 만들기

Formatter 인터페이스

  • 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;
    }
    
    • Formatter 는 Printer 와 Parser 인터페이스를 상속받는다.
    • 각각 print, parse 기능이 있다. (ISP 원칙)
      • String print(T object, Locale locale) : 객체를 문자로 변경한다.
      • T parse(String text, Locale locale) : 문자를 객체로 변경한다.

MyNumberFormatter

  • 숫자 1000 을 문자 “1,000” 으로 그러니까, 1000 단위로 쉼표가 들어가는 포맷을 적용해보자. 그리고 그 반대도 처리해주는 포맷터를 만들어보자.

  • package hello.typeconverter.formatter;
      
    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);
            //"1,000" -> 1000
            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);
        }
    }
    
    • implements Formatter<Number> : String 은 기본이니까, 나머지 하나의 타입을 넣어준다.
    • “1,000” 처럼 숫자 중간의 쉼표를 적용하려면 자바가 기본으로 제공하는 NumberFormat 객체를 사용하면 된다.
    • NumberFormat 에 public Number parse(String source) 메서드가 있다.
    • 또한 public final String format(double number) 메서드가 있다.
    • 두개를 활용해서 변경하면 된다.

MyNumberFormatterTest

  • package hello.typeconverter.formatter;
      
    import org.assertj.core.api.Assertions;
    import org.junit.jupiter.api.Test;
      
    import java.text.ParseException;
    import java.util.Locale;
      
    import static org.assertj.core.api.Assertions.*;
    import static org.junit.jupiter.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 을 넣어주어야 한다.

실행 결과 로그

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

참고

  • 스프링은 용도에 따라 다양한 방식의 포맷터를 제공한다.

  • Formatter 포맷터

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

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

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

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

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

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

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

DefaultFormattingConversionService 테스트

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

DefaultFormattingConversionService 상속 관계

  • FormattingConversionService 는 ConversionService 관련 기능을 상속받기 때문에 결과적으로 컨버터도 포맷터도 모두 등록할 수 있다.
  • 그리고 사용할 때는 ConversionService 가 제공하는 convert 를 사용하면 된다. (converter 든, format 이든!)

추가로 스프링 부트는 DefaultFormattingConversionService 를 상속 받은 WebConversionService 를 내부에서 사용한다.

8. 포맷터 적용하기

WebConfig - 수정

  • package hello.typeconverter;
      
    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 StringToIpPortConverter());
            registry.addConverter(new IpPortToStringConverter());
            //MyNumberFormatter() 보다 우선순위가 높기 때문에 주석처리
            //우선순위는 converter -> formatter
    //        registry.addConverter(new StringToIntegerConverter());
    //        registry.addConverter(new IntegerToStringConverter());
      
            //추가
            registry.addFormatter(new MyNumberFormatter());
      
        }
    }
    

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

    • MyNumberFormatter 도 숫자 <-> 문자로 변경하기 때문에 둘의 기능이 겹친다.
    • 우선순위는 컨버터가 우선하므로 포맷터가 적용되지 않고, 컨버터가 적용된다.

실행

객체 -> 문자

  • http://localhost:8080/converter-view
    • ${number}: 10000
    • $: 10,000
  • 컨버전 서비스를 적용한 결과 MyNumberFormatter 가 적용되어서 10,000 문자가 출력된 것을 확인할 수 있다.

문자 -> 객체

  • http://localhost:8080/hello-v2?data=10,000
    • MyNumberFormatter : text=10,000, locale=ko_KR
    • data = 10000
  • “10,000” 이라는 포맷팅 된 문자가 Integer 타입의 숫자 10000으로 정상 변환 된 것을 확인할 수 있다.

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

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

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

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

    • @Data
      static class Form{
          private Integer number;
          private LocalDateTime localDateTime;
      }
      
    • 이렇게 있을 때, number, localDateTime 각각 필드마다 다른 형식 포맷 지정이 힘들다는 말임.
  • 스프링은 이런 문제를 해결하기 위해 애노테이션 기반으로 원하는 형식을 지정해서 사용할 수 있는 매우 유용한 포맷터 두 가지를 기본으로 제공한다.

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

FormatterController

  • package hello.typeconverter.controller;
      
    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 formmaterForm(Model model){
            Form form = new Form();
            form.setNumber(10000);
            form.setLocalDateTime(LocalDateTime.now());
            model.addAttribute("form", form);
      
            return "formatter-form";
        }
      
        //문자가 들어오지만 Integer, LocalDateTime 으로 바꾼다. 어노테이션으로 패턴을 알기때문
        @PostMapping("/formatter/edit")
        public String formatterEdit(@ModelAttribute Form form){
            return "formatter-view";
        }
      
        @Data
        static class Form{
            @NumberFormat(pattern = "###,###")
            private Integer number;
            @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
            private LocalDateTime localDateTime;
        }
    }
    
    • Form 클래스를 보면 각각의 필드에 포맷이 적용되어 있다. (@NumberFormat)

@GetMapping(“/formatter/edit”)

  1. GET /formatter/edit 요청
  2. model 에 Form form 을 담아서 반환 (Integer, LocalDateTime 을 가지고 있음)
  3. @NumberFormat, @DateTimeFormat 를 통해 formatter 가 동작해서 String 으로 변경해서 렌더링!
  4. image-20230316230358170

@PostMapping(“/formatter/edit”)

  1. POST /formatter/edit 로 String 타입의 데이터를 보낸다. “10,000”, “2023-03-16 22:57:55”
  2. @ModelAttribute Form form 으로 받을 때 formatter 가 동작
  3. 각각 Integer, LocalDateTime 으로 변경된다.
  4. image-20230316230624256

formatter-form.html

  • templates/formatter-form.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>
    

formatter-view.html

  • templates/formatter-view.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>$: <span th:text="$" ></span></li>
     <li>${form.localDateTime}: <span th:text="${form.localDateTime}" ></span></
    li>
     <li>$: <span th:text="$" ></
    span></li>
    </ul>
    </body>
    </html>
    

참고 > @NumberFormat , @DateTimeFormat 의 자세한 사용법이 궁금한 분들은 다음 링크를 참고하거나 관련 애노테이션을 검색해보자.

댓글남기기