본문 바로가기
Tech/Spring | Spring Boot

[Spring][스프링 MVC 2편 - 백엔드 웹 개발 활용 기술] 10. 스프링 타입 컨버터

by 싱브이 2024. 2. 9.
728x90
반응형

 

 

김영한님의 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 강의 내용을 정리한 것입니다.

 

 

 

스프링 타입 컨버터

[HelloController - 문자 타입을 숫자 타입으로 변경]

package hello.typeconverter.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletRequest;

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

분석)

String data = request.getParameter("data")`

HTTP 요청 파라미터는 모두 문자로 처리된다→  요청 파라미터를 자바에서 다른 타입으로 변환해서 사용하고 싶으면 숫자 타입으로 변환하는 과정을 거쳐야 한다.

Integer intValue = Integer.valueOf(data)

 

@RequestParam 사용

[HelloController - 추가]

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

@RequestParam 을 사용하면 스프링이 중간에서 타입을 변환해주기 때문에 문자 10Integer 타입의 숫자 10으로 받아들일 수 있다.

 

@ModelAttribute , @PathVariable 사용

[@ModelAttribute 타입 변환 예시]

@ModelAttribute UserData data
class UserData {
	Integer data;
}

[@PathVariable 타입 변환 예시]

 /users/{userId}
 @PathVariable("userId") Integer data

 

스프링의 타입 변환 적용 예

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

 

새로운 타입을 만들어서 변환하기

[컨버터 인터페이스]

package org.springframework.core.convert.converter;
public interface Converter<S, T> {
	T convert(S source);
}

 

스프링은 확장 가능한 컨버터 인터페이스를 제공한다. (추가적인 타입 변환이 필요하면 이 컨버터 인터페이스를 구현해서 등록하면 된다.)

이 컨버터 인터페이스는 모든 타입에 적용할 수 있다. 필요하면 X   →  Y 타입으로 변환하는 컨버터 인터페이스를 만들고, Y   →  X 타입으로 변환하는 컨버터 인터페이스를 만들어서 등록하면 된다.
예) 문자로 "true" 가  오면 Boolean 타입으로 받고 싶으면 String   →   Boolean 타입으로 변환 되도록 컨버터 인터페이스를 만들어서 등록하고, 반대로 적용하고 싶으면 Boolean   →   String 타입으로 변환 되도록 컨버터를 추가로 만들어서 등록하면 된다.

 

* 참고: 과거에 사용하던 PropertyEditor 는 동시성 문제가 있어서 타입을 변환할 때 마다 객체를 계속 생성해야 하는 단점이 있다.

Converter 사용이 편하다.

 


Converter

org.springframework.core.convert.converter.Converter 인터페이스 구현

[컨버터 인터페이스]

package org.springframework.core.convert.converter;
public interface Converter<S, T> {
    T convert(S source);
}

 

[StringToIntegerConverter - 문자를 숫자로 변환하는 타입 컨버터]

package hello.typeconverter.converter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.core.convert.converter.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 : Integer.valueOf(source)

[IntegerToStringConverter - 숫자를 문자로 변환하는 타입 컨버터]

package hello.typeconverter.converter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.core.convert.converter.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 : String.valueOf(source)

[ConverterTest - 타입 컨버터 테스트 코드]

package hello.typeconverter.converter;

import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;

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

 

 

 

사용자 정의 타입 컨버터

IP, PORT를 입력하면 IpPort 객체로 변환하는 컨버터

[IpPort]

package hello.typeconverter.type;
import lombok.EqualsAndHashCode;
import lombok.Getter;

@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 - 컨버터]

package hello.typeconverter.converter;

import hello.typeconverter.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);
    }
}

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

[IpPortToStringConverter]

package hello.typeconverter.converter;

import hello.typeconverter.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();
    }
}

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

 

[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 : 특정 조건이 참인 경우에만 실행

 


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.type.IpPort;
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 StringToIntegerConverter());
        conversionService.addConverter(new IntegerToStringConverter());
        conversionService.addConverter(new StringToIpPortConverter());
        conversionService.addConverter(new IpPortToStringConverter());
        
  		//사용
        assertThat(conversionService.convert("10", Integer.class)).isEqualTo(10);
        assertThat(conversionService.convert(10, String.class)).isEqualTo("10");
        IpPort ipPort = conversionService.convert("127.0.0.1:8080", IpPort.class);
        assertThat(ipPort).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");
     }
}

 

등록과 사용 분리

타입 컨버터들은 모두 컨버전 서비스 내부에 숨어서 제공하기 때문에 사용하는 입장에서는 타입 컨버터를 전혀 몰라도 된다.

타입을 변환을 원하는 사용자는 컨버전 서비스 인터페이스에만 의존하면 된다. 물론 컨버전 서비스를 등록하는 부분과 사용하는 부분을 분리하고 의존관계 주입을 사용해야 한다.

 

컨버전 서비스 사용

Integer value = conversionService.convert("10", Integer.class)

 

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

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

 

DefaultConversionService 는 다음 두 인터페이스를 구현했다.

  • ConversionService : 컨버터 사용에 초점
  • ConverterRegistry : 컨버터 등록에 초점

 

ISP  → 인터페이스를 분리하는 것

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


스프링에 Converter 적용

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

WebMvcConfigurer 가 제공하는 addFormatters()를 사용해서 추가하고 싶은 컨버터를 등록하면 된다. → 스프링 내부에서 ConversionService에 컨버터를 추가해준다.

 

등록한 컨버터가 잘 동작하는지 확인하기

[HelloController - 기존 코드]

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

 

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

 

IpPort 를 사용해보기

[HelloController - 추가]

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

 

http://localhost:8080/ip-port?ipPort=127.0.0.1:8080

처리 과정
@RequestParam@RequestParam을 처리하는 ArgumentResolverRequestParamMethodArgumentResolver에서 ConversionService를 사용해서 타입을 변환한다.


뷰 템플릿에 컨버터 적용

[ConverterController]

package hello.typeconverter.controller;

import hello.typeconverter.type.IpPort;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@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 객체를 담아서 뷰 템플릿에 전달

[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>${{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}} : 뷰 템플릿은 데이터를 문자로 출력한다. 따라서 컨버터를 적용하게 되면 Integer 타입인 10000을 String 타입으로 변환하는 컨버터인 IntegerToStringConverter를 실행하게 된다. 이 부분은 컨버터를 실행하지 않아도 타임리프가 숫자를 문자로 자동으로 변환히기 때문에 컨버터를 적용할 때와 하지 않을 때가 같다.
  • ${{ipPort}} : 뷰 템플릿은 데이터를 문자로 출력한다. 따라서 컨버터를 적용하게 되면 IpPort 타입을 String 타입으로 변환해야 하므로 IpPortToStringConverter 가 적용된다. 그 결과 127.0.0.1:8080 가 출력된다.

 

폼에 적용하기

[ConverterController - 코드 추가]

package hello.typeconverter.controller;

import hello.typeconverter.type.IpPort;
import lombok.Data;
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;

@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";
 	}
    
    @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";
	}
    
    @Data
    static class Form {
        private IpPort ipPort;
        public Form(IpPort ipPort) {
            this.ipPort = ipPort;
		}		 
    }
}

Form 객체를 데이터를 전달하는 폼 객체로 사용한다.

  • GET /converter/edit : IpPort 를 뷰 템플릿 폼에 출력한다.
  • POST /converter/edit : 뷰 템플릿 폼의 IpPort 정보를 받아서 출력한다.

[ 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을 출력하는 기능과 컨버전 서비스도 함께 적용된다.

 

  • GET /converter/edit
    th:field 가 자동으로 컨버전 서비스를 적용해주어서 ${{ipPort}}처럼 적용이 되었다. 따라서 IpPort  →  String로 변환된다.
  • POST /converter/edit
    @ModelAttribute를사용해서 String  →  IpPort로 변환된다.

Formatter

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

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

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

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

 

포맷터 : 컨버터의 특별한 버전으로 객체를 특정한 포멧에 맞추어 문자로 출력하거나 또는 그 반대의 역할을 하는 것에 특화된 기능

Converter vs Formatter

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

 

Formatter 만들기

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

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

[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> {
 }

 

[MyNumberFormatter] - 숫자 1000을 문자 '1,000'으로 (쉼표가 들어가는 포맷 적용하기, 반대로도)

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);
        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() 를 사용해서 객체를 문자로 변환한다.

[MyNumberFormatterTest]

package hello.typeconverter.formatter;

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 을 넣어주어야 한다.

 

[실행 결과 로그]

* 참고

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

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


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

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

그런데 생각해보면 포맷터는 객체 → 문자, 문자  객체로 변환하는 특별한 컨버터일 뿐이다.
포맷터를 지원하는 컨버전 서비스를 사용하면 컨버전 서비스에 포맷터를 추가할 수 있다.

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

 

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

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

 

[FormattingConversionServiceTest]

package hello.typeconverter.formatter;

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: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 를 사용하면 된다.

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


포맷터 적용

[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 StringToIntegerConverter()); 
        //registry.addConverter(new IntegerToStringConverter()); 
        registry.addConverter(new StringToIpPortConverter()); 
        registry.addConverter(new IpPortToStringConverter());
       //추가
        registry.addFormatter(new MyNumberFormatter());
    }
}

 

 


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

Formatter 인터페이스의 구현 클래스를 찾아보면 수 많은 날짜나 시간 관련 포맷터가 제공되지만, 포맷터는 기본 형식이 지정되어 있기 때문에 객체의 각 필드마다 다른 형식으로 포맷을 지정하기는 어렵다.

  • @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 formatterForm(Model model) {
        Form form = new Form();
        form.setNumber(10000);
        form.setLocalDateTime(LocalDateTime.now());
        model.addAttribute("form", form);
        return "formatter-form";
    }
     
    @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;
    }
}

[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>

 

[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>${{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>

 

[결과]

 

 

* 주의 !

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

 

HttpMessageConverter의 역할은 HTTP 메시지 바디의 내용을 객체로 변환하거나, 객체를 HTTP 메시지 바디에 입력하는 것이다.

예) JSON을 객체로 변환하는 메시지 컨버터는 내부에서 Jackson 같은 라이브러리를 사용한다. 객체를 JSON으로 변환한다면 그 결과는 이 라이브러리에 달린 것이다.

→  JSON 결과로 만들어지는 숫자나 날짜 포맷을 변경하고 싶으면 해당 라이브러리가 제공하는 설정을 통해서 포맷을 지정해야 한다. (컨버전 서비스와 관련 없음)

 

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

 

728x90
반응형

댓글