WEB/Spring MVC 2

Spring MVC 2편 스프링 타입 컨버터

Tony Lim 2021. 10. 31. 12:09
728x90

스프링 MVC 요청 파라미터

HTTP 요청 파라미터들은 모두 String으로 처리 된다.

@RequestParam , @ModelAttribute , @PathVariable 기존 String으로 들어온것들을 알아서 integer나 알맞은 형식으로 변환해준다.

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

org.springframework.core.convert.converter; 를 사용해야한다.

@Slf4j
public class IpPortToStringConverter implements Converter<IpPort, String> {
    @Override
    public String convert(IpPort source) {
        log.info("convert source={}", source);
        //IpPort 객체 -> "127.0.0.1:8080"
        return source.getIp() + ":" + source.getPort();
    }
}
@Test
void stringToIpPort() {
    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");
}

IpPort 객체를 알맞은 스트링으로 convert 하는 테스트 코드이다.

아직까지는 그냥 개발자가 수동으로 converting하는거랑 별 차이가 없다.

 


@ConversionService

스프링은 이러한 개별 컨버터를 모아서 그것들을 편리하게 사용할 수 있게 해준다.

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

    }
}

ConversionService가 동작에 알맞은 converter를 찾아서 사용한다.

DefaultConversionService 는 위로 올라가보면 ConversionService ,ConveterRegistry 두가지 인터페이스를 구현하고 있다.

이렇게 컨버터를 사용하는 클라이언트와 컨버터를 등록하고 관리하는 클라이언트의 관심사를 명확하게 분리할 수 있다.

 


스프링에 Converter 적용하기

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

스프링에서 기본적으로 추가해논 Converter들이 존재한다. 하지만 새로운 컨버터를 추가하면 기본 컨버터보다 높은 우선순위를 가진다.

@RequestParam 는 ArgumenResolver 인 RequestParamMethodArgumentResolver 에서 ConversionService를 이용해 타입을 변환한다.

<!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 같이, converter를 적용하겠다는 뜻이다.

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

th:field도 자동으로 converter를 적용을 해준다. th:value의 경우는 자동으로 적용되지 않는다.

 


Formatter

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

1,000 을 1000으로 변경하고 싶은 상황 같을 때 쓴다.

이런식으로 객체를 특정한 포멧에 맞추어 출력하거나 또는 그 반대의 역할을 하는 것에 특화된 기능이 바로 포멧터 이다.

Formatter는 문자에 특화되었다 Converter의 특별한 버전이다. 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);
    }
}

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

 

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

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

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 는 Conversion 기능을 다 상속받은 FormattService이다.

 

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

@NumberFormat , @DateTimeFormat

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

패턴으로 주어진 방식으로 변환을 해준다.

저렇게 (10,000) 입력이들어와도 숫자로 인식해서 바꿔준다. 양방향인것이다.

 

HttpMessageConverter에는 ConversionService 적용되지 않는다.

특히 객체를 json으로 변환할 때 HttpMessageConverter 를 사용하면서 이 부분을 많이 오해한다. json 결과로 만들어지는 숫자나 날짜 포맷을 변경하고 싶으면 해당 라이브러리(Jackson)가 제공하는 설정을 통해서 포맷을 지정해야 한다. 

 

 

 

 

 

728x90