관리 메뉴

나구리의 개발공부기록

스프링 타입 컨버터 - 프로젝트 생성, 소개, Converter, ConversionSevice, 스프링에 Converter적용, 뷰 템플릿에 컨버터 적용, Formatter, 포맷터 적용, 스프링이 제공하는 기본 포맷터 본문

인프런 - 스프링 완전정복 코스 로드맵/스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술

스프링 타입 컨버터 - 프로젝트 생성, 소개, Converter, ConversionSevice, 스프링에 Converter적용, 뷰 템플릿에 컨버터 적용, Formatter, 포맷터 적용, 스프링이 제공하는 기본 포맷터

소소한나구리 2024. 9. 9. 18:51

  출처 : 인프런 - 스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술 (유료) / 김영한님  
  유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용  

https://inf.run/GMo43


1. 프로젝트 생성


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

1) 예제 코드

  • 예제 코드를 작성 후 /hello-v1?data=10, /hello-v2?data=10 이렇게 웹 브라우저에서 실행을 하면 쿼리스트링으로 전달되는 값은 숫자처럼 보여도 문자임(URL 경로는 문자)
  • HTTP 요청 파라미터는 모두 문자로 처리 되기 때문에 helloV1 메서드 처럼 요청 파라미터를 자바에서 다른 타입으로 변환해서 사용하려면 타입을 변환해야함
  • 스프링 MVC가 제공하는 @RequestParam을 사용하면 helloV2의 메서드처럼 바로 타입 변환을 할 수 있는데, 이것은 스프링이 중간에서 타입을 변환해주었기 때문임
  • @ModelAttribute나 @PathVariable을 사용할 때도 Integer로 타입을 변환할 수 있는 이유도 위와 동일함.
package hello.typeconverter.controller;

@RestController
public class HelloController {

    @GetMapping("/hello-v1")
    public String helloV1(HttpServletRequest request) {
        String data = request.getParameter("data");     // 문자 타입 조회
        Integer intValue = Integer.valueOf(data);          // 숫자 타입으로 변경
        System.out.println("intValue = " + intValue);
        return "ok";
    }

    @GetMapping("/hello-v2")
    public String helloV2(@RequestParam Integer data) {
        System.out.println("data = " + data);
        return "ok";
    }
}

 

2) 스프링의 타입 변환 적용 예시

  • 스프링 MVC 요청 파라미터: @RequestParam, @ModelAttribute, @PathVariable
  • @Value 등으로 YML 정보 읽기
  • XML에 넣은 스프링 빈 정보를 변환
  • 뷰를 렌더링 할 때

3) 스프링과 타입 변환

  • 이렇게 타입 변환해야 하는 경우가 상당히 많은데, 개발자가 직접 하나하나 타입 변환을 해야하는 번거로운 작업을 스프링이 중간에 타입 변환기를 사용해서 변환해 주기 때문에 편리하게 해당 타입을 받을 수 있음
  • 문자를 숫자로, 숫자를 문자로, Boolean 타입을 숫자로 변경하는 것도 가능함
  • 개발자가 새로운 타입을 만들어서 변환하는 것도 가능함(새로운 클래스를 만들고 해당 클래스 내에서 다양한 변환을 정의하는 것)

(1) 컨버터 인터페이스

  • 스프링은 확장 가능한 컨버터 인터페이스를 제공하며 개발자가 추가적인 타입 변환이 필요하면 이 컨버터 인터페이스를 구현해서 등록하면 됨
  • 과거에는 PropertyEditor라는 것으로 타입을 변환했으나 동시성 문제가 있어 타입을 변환할 때 마다 객체를 계속 생성해야하는 단점이 있어 기능확장 시에는 Converter를 사용
@FunctionalInterface
public interface Converter<S, T> {
    @Nullable
    T convert(S source);
// ... 기타 코드
}

3. 타입 컨버터 - Converter

  • Converter라는 이름의 인터페이스가 많으니 패키지 경로를 꼭 확인해서 사용할 것
  • org.springframework.core.convert.converter.Converter

1) 원초적인 컨버터

(1) Integer -> String 변환 컨버터

package hello.typeconverter.converter;

// 숫자를 문자로 변환하는 컨버터
@Slf4j
public class IntegerToStringConverter implements Converter<Integer, String> {

    @Override
    public String convert(Integer source) {
        log.info("convert source: {}", source);
        return String.valueOf(source);  // Integer -> String 변환
    }
}

 

(2) String -> Integer 변환 컨버터

package hello.typeconverter.converter;

// 문자를 숫자로 변환하는 타입 컨버터
@Slf4j
public class StringToIntegerConverter implements Converter<String, Integer> {

    @Override
    public Integer convert(String source) {
        log.info("convert source: {}", source);
        return Integer.valueOf(source);     // String -> Integer 변환
    }
}

 

(3) Test

  • 단순하고 원초적인 로직으로 컨버터를 구현하고 테스트를 해보면 당연히 컨버팅이 잘 되어 테스트가 통과됨
public class ConverterTest {

    @Test
    void stringToInteger() {
        StringToIntegerConverter converter = new StringToIntegerConverter();
        Integer result = converter.convert("10");
        assertThat(result).isEqualTo(10);
    }

    @Test
    void integerToString() {
        IntegerToStringConverter converter = new IntegerToStringConverter();
        String result = converter.convert(10);
        assertThat(result).isEqualTo("10");
    }

}

2) 사용자 정의 타입 컨버터

  • 조금더 실무에서 자주쓰는 실용적인 예제로 IP, PORT를 입력하면 IpPort 객체로 변환하는 컨버터를 구현해보기
  • "127.0.0.1:8080" 이렇게 들어오면 ip는 String, port는 Integer로 변환되는 컨버터

(1) IpPort 클래스 정의

  • @EqualsAndHashCode : 모든 필드를 사용해서 equals(), hashcode()를 생성
  • 실제 값이 같다면(논리적 값) a.equals(b)의 결과가 true가 됨
package hello.typeconverter.type;

@Getter
@EqualsAndHashCode  // 롬복 - [equals(): 값 비교, hashCode(): 객체의 해시코드 반환]을 자동 생성
public class IpPort {

    private String ip;
    private int port;

    public IpPort(String ip, int port) {
        this.ip = ip;
        this.port = port;
    }

}

 

(2) String -> IpPort, IpPort -> String 으로 변환하는 컨버터 생성

  • Converter<String, IpPort> 구현 -> String 을 IpPort로 변환
  • Converter<IpPort, String> 구현 -> IpPosrt를 String으로 변환
package hello.typeconverter.converter;

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

    @Override
    public IpPort convert(String source) {
        log.info("convert source: {}", source);
        // "127.0.0.1:8080" 변환
        String[] split = source.split(":"); // :을 기준으로 자르기
        String ip = split[0];                     // 앞쪽은 String = ip
        int port = Integer.parseInt(split[1]);    // 뒤쪽은 int = 포트
        return new IpPort(ip, port);              // IpPort 객체에 값을 넣어서 생성
    }
}

 

package hello.typeconverter.converter;

// Ip(String), Port(Integer) ->  "127.0.0.1:8080" 변환
@Slf4j
public class IpPortToStringConverter implements Converter<IpPort, String> {

    @Override
    public String convert(IpPort source) {
        log.info("covert source: {}", source);
        return source.getIp() + ":" + source.getPort(); // ip와 포트를 + 연산자로 문자열 결합
    }
}

 

(3) Test  코드 추가

  • 테스트 코드를 작성 후 실행해보면 테스트가 통과되어 컨버팅이 잘 된 것을 확인할 수 있음
public class ConverterTest {

    @Test
    void ipPortToString() {
        IpPortToStringConverter converter = new IpPortToStringConverter();
        IpPort ipPort = new IpPort("127.0.0.1", 8080);      // IpPort 객체 생성
        String result = converter.convert(ipPort);          // String 변환
        assertThat(result).isEqualTo("127.0.0.1:8080");     // 변환 비교
    }

    @Test
    void stringToIpPort() {
        StringToIpPortConverter converter = new StringToIpPortConverter();
        String source = "127.0.0.1:8080";                               // 문자열 생성
        IpPort result = converter.convert(source);                      // 문자열을 IpPort 타입으로 변환
        assertThat(result).isEqualTo(new IpPort("127.0.0.1", 8080));    // @EqualsAndHashCode 때문에, 값비교가 가능
    }

}

3) Converter 정리

  • 타입 컨버터 인터페이스는 단순해서 이해하기는 쉽다
  • 하지만 이렇게 타입 컨버터가 필요할 때마다 하나하나를 직접 사용하면 개발자가 직접 컨버팅 하는 것과 차이가 없게 되는데 스프링이 개별 컨버터를 모아두고 묶어서 편리하게 사용할 수 있는 컨버전 서비스라는 기능을 제공함
  • 스프링은 용도에 따라 다양한 방식의 타입 컨버터를 제공하고, 문자, 숫자, 불린, Enum 등 일반적인 타입에 대한 대부분의 컨버터를 기본으로 제공하므로 자세한 내용은 IDE의 구현체나 공식문서를 참고해서 확인
  • https://docs.spring.io/spring-framework/reference/core/validation/convert.html
  • Converter -> 기본 타입 컨버터
  • ConverterFactory -> 전체 클래스 계층 구조가 필요할 때
  • GenericConverter -> 정교한 구현, 대상 필드의 애노테이션 정보를 사용 가능
  • ConditionalGenericConverter -> 특정 조건이 참인 경우에만 실행

4. 컨버전 서비스 - ConversionService

  • 개별 컨버터를 묶어서 사용할 수 있는 기능을 제공

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

  • DefaultConversionService : ConversionService 인터페이스를 구현한 구현체
  • .addConverter() 로 컨버터를 등록할 수 있음
  • .convert() 메서드 하나로 작성된 컨버터의 룰에 따라 매개변수만 넣어주면 알아서 컨버터가 동작됨
public class ConversionServiceTest {

    @Test
    void conversionService() {
        // DefaultConversionService : ConversionService 구현체중 하나, 컨버전을 등록할 수 있음
        DefaultConversionService conversionService = new DefaultConversionService();

        // 만들었던 컨버터들 모두 등록
        conversionService.addConverter(new StringToIntegerConverter());
        conversionService.addConverter(new IntegerToStringConverter());
        conversionService.addConverter(new StringToIpPortConverter());
        conversionService.addConverter(new IpPortToStringConverter());

        // .convert()로 사용

        // StringToIntegerConvert 동작
        assertThat(conversionService.convert("10", Integer.class)).isEqualTo(10);

        // IntegerToStringConvert 동작
        assertThat(conversionService.convert(10, String.class)).isEqualTo("10");

        // StringToIpPortConvert 동작
        assertThat(conversionService.convert("127.0.0.1:8080", IpPort.class))
                                        .isEqualTo(new IpPort("127.0.0.1", 8080));

        // IpPortToStringConvert 동작
        assertThat(conversionService.convert(new IpPort("127.0.0.1", 8080), String.class))
                                        .isEqualTo("127.0.0.1:8080");
    }
}

 

(1) 등록과 사용 분리

  • 컨버터를 등록할 때는 StringToIntegerConverter와 같이 작성된 타입 컨버터를 명확하게 알아야 하지만 컨버터를 사용하는 입장에서는 타입 컨버터를 전혀 몰라도 컨버전 서비스 내부에 숨어서 동작하며 .convert() 메서드로 사용 가능함
  • 컨버전 서비스를 등록하는 부분과 사용하는 부분을 분리하고 의존관계 주입을 사용하면 타입을 변환을 원하는 사용자는 구현체는 모르고 컨버전 서비스 인터페이스에만 의존하여 사용만 하면됨

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

  • DefaultConversionService는 컨버터 사용에 초점을 둔 ConversionService와 컨버터 등록에 초점을 둔 ConverterRegistry 두가지 인터페이스를 구현함
  • 이렇게 각 인터페이스를 분리하면 컨버터를 사용하는 클라이언트와 컨버터를 등록하고 관리하는 클라이언트의 관심사를 명확하게 분리할 수 있고, 컨버터를 사용하는 클라이언트는 ConversionService만 의존하게 되면서 클라이언트가 꼭 필요한 메서드만 알게되도록 인터페이스를 분리하는 것을 ISP라고 함
  • 스프링에서 코드를 구현할때 대부분 이런식으로 작성이 되어있음

5. 스프링에 Converter 적용하기

1) WebConfig - 컨버터 등록

  • 스프링은 내부에서 ConversionService를 제공함
  • WebMvcConfigurer가 제공하는 addFormatters()를 사용해서 추가하고 싶은 컨버터를 등록하면 스프링이 ConversionService에 컨버터를 추가해줌
package hello.typeconverter;

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

 

2) 동작 해보기

(1) 기존에 만들었던 helloV2() 컨버터 실행

  • /hello-v2?data=19 로 접속
  • 로그를 확인해보면 converter.StringToIntegerConverter   : convert source: 19, data = 19 로 동작하는 것을 확인할 수 있음
  • 그러나, 이렇게 등록 전에도 해당 코드는 동작이 잘 되었는데 이미 스프링이 내부에서 수많은 기본 컨버터를 제공하기 때문
  • 컨버터를 추가하면 추가한 컨버터가 기본 컨버터보다 높은 우선 순위를 가짐

(2) HelloController에 직접 정의한 타입인 IpPort를 추가

  • http://localhost:8080/ip-port?ipPort=127.0.0.1:8080로 접속
  • 로그를 확인해보면 아래처럼 출력되어 변환이 잘 되었음
    converter.StringToIpPortConverter    : convert source: 127.0.0.1:8080
    ipPort = 127.0.0.1
    ipPort.getPort() = 8080
  • @RequestParam, @ModelAttribute, @PathVariable이 모두 동일하게 적용됨
@GetMapping("ip-port")
public String ipPort(@RequestParam IpPort ipPort) {
    System.out.println("ipPort = " + ipPort.getIp());
    System.out.println("ipPort.getPort() = " + ipPort.getPort());
    return "ok";
}

 

(3) 처리 과정

  • RequestParam은 @RequestParam을 처리하는 ArgumentResolver인 RequestParamMethodArgumentResolver에서 ConversionService를 사용해서 타입을 변환함
  • 부모 클래스와 다양한 외부 클래스를 호출하는 등 복잡한 내부 과정을 거치기 때문에 대략적으로 이렇게 처리되는 것으로 이해하고, 깊이 있게 확인하고자 하면 IpPortConverter에 디버그 브레이크 포인트를 걸어서 확인해보면 처리 과정들을 쭉 볼 수 있음

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

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

1) 컨버터 적용

(1) ConverterController

  • model.addAttribute로 각 데이터 입력
package hello.typeconverter.controller;

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

 

(2) 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>${{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>

 

(3) 실행 결과 및 로그

  • number의 경우 컨버터를 적용하든, 적용하지 않든 스프링이 기본으로 제공하는 컨버터가 있어서 10000이라는 String타입이 Integer로 자동으로 변환되어 출력이 됨
    (컨버터를 적용하면 만들어준 컨버터가 적용되므로 IntegerToStringconverter가 적용된 것을 로그에서 볼 수 있음
  • 하지만 ipPort의 경우 직접 정의한 객체이므로, 만든 컨버터를 적용하지 않으면 스프링이 이러한 상황은 컨버팅을 제공하지 않으므로 객체.toString으로 객체의 주소값이 출력이 되어버리게 됨
  • 그래서 이런 경우에는 컨버전 서비스를 적용해주어야 정상적으로 출력이 됨

2) 폼에 적용하기 

  • HTTP Form에는 자동으로 컨버터가 적용이 됨

(1) ConverterController - 코드 추가

package hello.typeconverter.controller;

@Controller
public class ConverterController {
    // ...

    @GetMapping("/converter/edit")
    public String converterForm(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;
        }
    }
}

 

(2) converter-form.html 작성

  • th:field : 컨버터 적용 -> 변환되어서 값이 출력됨
  • th:value : 컨버터 적용 안함 -> 변환되지 않아서 객체의 주소값이 그대로 출력됨
<!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>

 

(3) 실행

  • GET / converter/edit : th:filed가 자동으로 컨버전 서비스를 적용해서 IpPort -> String으로 변환됨
  • POST /converter/edit : 제출을 누르면 POST 전송이 되는데, 넘어온 String을 @ModelAttribute를 사용해서 String -> IpPort로 변환 출력

GET / POST 실행 결과와 컨버터가 적용된 로그


7. 포맷터 - Formatter

  • Converter는 입력과 출력 타입에 제한이 없는 범용 타입 변환기능을 제공
  • 그러나 일반적인 웹 애플리케이션 환경을 보면 불린 타입을 숫자로 바꾸는 것 같은 범용 기능 보다는 문자를 다른 객체로 변환하거나, 다른 객체를 문자를 변환하는 상황이 대부분임
  • ex) 숫자 출력 시 Integer -> String 출력 시점에 1000 -> "1,000" 이렇게 단위에 쉼표를 넣거나,
          "1,000" 이라는 문자열을 1000이라는 숫자로 변경할때나,
          날짜 객체를 문자인 "2021-01-01 10:50:11"과 같이 출력하거나 그 반대의 상황 등
  • 날짜 / 숫자의 표현방법은 나라마다 표시 방식이 다를 수 있는데 Locale 현지화 정보화 정보가 사용될 수 있음
  • 이런식으로 특정한 포맷에 맞추어 문자로 출력하거나 또는 그 반대의 역할을 하는 것에 특화된 기능이 바로 포맷터이며 컨버터의 특별한 버전이라고 이해하면 됨

Cpmverter vs Formatter

  • Converter는 범용 : 객체 -> 객체
  • Formatter는 문자에 특화 : 객체 -> 문자, 문자 -> 객체 + 현지화(Locale)

1) 포맷터 만들기

(1) Formatter 인터페이스 구조

  • String print(T object, Locale locale) : 객체를 문자로 변경
  • T Parse(String text, Locale locale) : 문자를 객체로 변경
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> {
}

 

(2) MyNumberFormatter

package hello.typeconverter.formatter;

@Slf4j
// String 변환은 기본으로 되기 때문에 스트링을 제외할 변환 타입을 제네릭 타입에 넣어주면 됨
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);  // parse()로 String 을 포맷팅 변환
    }

    @Override
    // Number : Integer, Long 과같은 숫자 타입의 부모 클래스임
    public String print(Number object, Locale locale) {
        log.info("object = {}, locale ={}", object, locale);
        return NumberFormat.getInstance(locale).format(object); // .format()으로 Number 를 포맷팅 변환
    }
}

 

(3) Test

  • 테스트 하고자하는 클래스에 커맨드 + 쉬프트 + t를 하면 자동으로 test 디렉토리에 동일한 경로의 패키지를 생성하여 테스트 클래스를 생성해줌
  • 만든 MyNumberFormatter()를 생성하여 Locale을 KOREA로 적용하여 test 진행
  • .parse() : 반환타입이 Long 이라서 1000L 으로 반환됨 -> 필요시 Integer 변환
  • .print() : Locale 정보가 반영된 포맷팅 문자 "1,000"이 반환됨
class MyNumberFormatterTest {

    MyNumberFormatter formatter = new MyNumberFormatter();

    @Test
    void parse() throws ParseException {
        Number result = formatter.parse("1,000", Locale.KOREA);
        assertThat(result).isEqualTo(1000L); // Number 타입이지만 Long 으로 반환됨
    }

    @Test
    void print() {
        String result = formatter.print(1000, Locale.KOREA);
        assertThat(result).isEqualTo("1,000");
    }
}

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

  • 기본 컨버전 서비스는 컨버터만 등록할 수 있고 포맷터를 등록할 수 없는데, FormattingConversionService는 내부에서 어댑터 패턴을 사용해서 Formatter가 Converter 처럼 동작하도록 하여 컨버전 서비스에 포맷터를 추가할 수 있음
  • DefualtFormattingConversionService는 FormattingConversionService에 기본적인 통화, 숫자 관련 몇가지 기본 포맷터를 추가해서 제공함
  • FormattingconversionService는 ConversionService 관련 기능을 상속 받기 때문에 결과적으로 컨버터, 포맷터 모두 등록할 수 있으며 사용할 때는 구분없이 .convert로 사용하면 됨

1) Test

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 result = conversionService.convert("127.0.0.1:8080", IpPort.class);
        assertThat(result).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);
    }
}

9. 포맷터 적용하기

  • 웹 애플리케이션에 포맷터 적용

1) Webconfig - 수정

  • 둘의 기능이 겹칠 경우 컨버터가 포맷팅보다 우선순위가 먼저 적용되므로 컨버터를 주석처리 진행
package hello.typeconverter;

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

2) 실행

(1) 객체 -> 문자

  • localhost:8080/converter-view로 접속해보면 10,000으로 포맷팅이 된것을 확인할 수 있음

(2) 문자 -> 객체

  • localhost:8080/hello-v2?data=10,000로 data에 파라미터 값으로 10,000을 넣어서 접속해보면 로그에 10000으로 변환된 것을 확인할 수 있음

좌) 10000 -> 10,000으로 포맷팅 / 우) 10,000 -> 10000으로 변환


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

  • Formatter 인터페이스의 구현 클래스를 찾아보면 수 많은 날짜나 시간 관련 포맷터가 제공되는 것을 확인할 수 있음
  • 그러나 포맷터는 기본 형식이 지정되어 있기 때문에 객체의 각 필드마다 다른 형시으로 포맷을 지정하기 어려운데 애노테이션 기반으로 원하는 형식을 지정해서 사용할 수 있는 매우 유용한 포맷터 2가지를 기본으로 제공함

1) 사용해보기

(1) FormatterController

  • @NumberFormat : 숫자 관련 형식 지정 포맷터 사용(NumberFormatAnnotationFormatterFactory)
  • @DateTimeFormat : 날짜 관련 형식 지정 포맷터 사용, (Jsr310DateTimeFormatAnnotationFormatterFactory)
  • 패턴으로 지정되어 있기 때문에 해당 패턴에 맞으면 String -> 객체, 객체 -> String으로 변환이 가능함
package hello.typeconverter.controller;

@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 "formatter-form";
    }

    // @ModelAttribute 는 자동으로 model 에 값을 넣어줌
    @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;
    }
}

 

(2) 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>

 

(3) 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>${{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>

 

** 주의

  • 메시지 컨버터(HttpMessageConverter)에는 컨버젼 서비스가 적용되지 않음
  • 특히 객체를 JSON으로 변환할 때 메시지 컨버터를 사용하면서 오해가 생기는데, 컨버전 서비스와 전혀 관계가 없음
  • HttpMessageConverter의 역할은 HTTP 메시지 바디의 내용을 객체로 변환하거나 객체를 HTTP 메시지 바디에 입력하는 것인데, JSON을 객체로 변환하는 메시지 컨버터는 내부에서 Jackson 같은 라이브러리를 사용함
  • 객체를 JSON으로 변환한다면 그결과는 라이브러리에 달린 것이므로 JSON 결과로 만들어지는 숫자나 날짜 포맷을 변경하고 싶으면 해당 라이브러리가 제공하는 설정을 통해서 포맷을 지정해야함
  • 컨버전 서비스는 @RequestParam, @ModelAttribute, @PathVariable, 뷰 템플릿 등에서 사용 가능함