Spring

스프링 타입 컨버터

gogi masidda 2024. 1. 15. 16:12
@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") //@RequestParam을 사용하면 스프링이 중간에서 타입 변환을 해줌
    public String helloV2(@RequestParam Integer data) {
        System.out.println("data = " + data);
        return "ok";
    }
}

 Http 쿼리 스트링으로 전달하는 data=10은 숫자 10이 아니라 문자 10이다.

이렇게 @RequestParam으로 문자 10을 숫자 10으로 스프링이 중간에서 타입 변환을 해주어서 편리하게 받을 수 있다.

 

@PathVariable의 /users/{userid}에서도 userid부분은 문자다. 이것도 스프링이 타입 변환을 중간에서 해주는 것이다.

이렇게 문자 -> 숫자만이 아니라 숫자 -> 문자, Boolean -> 숫자도 가능하다.

 

스프링의 타입 변환 적용 예

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

 

개발자가 새로운 타입을 만들어서 변환하고 싶을 때도 스프링의 확장 가능한 컨버터 인터페이스를 사용하면 된다. 컨버터 인터페이스를 구현해서 등록하면 된다.

 

@Slf4j
public class StringToIntegerConvertor implements Converter<String, Integer> {

    @Override
    public Integer convert(String source) {
        log.info("convert source = {}", source);
        Integer integer = Integer.valueOf(source);
        return integer;
    }
}
@Slf4j
public class IntegerToStringConvertor implements Converter<Integer, String> {
    @Override
    public String convert(Integer source) {
        log.info("convertor source = {}", source);
        return String.valueOf(source);
    }
}
@Slf4j
public class StringToIpPortConvertor implements Converter<String, IpPort> {
    @Override
    public IpPort convert(String source) {
        log.info("convertor source = {}", source);
        //"127.0.0.1:8080"
        String[] split = source.split(":");
        String ip = split[0];
        int port = Integer.parseInt(split[1]);
        return new IpPort(ip, port);
    }
}
@Slf4j
public class IpPortToStringConvertor implements Converter<IpPort, String> {
    @Override
    public String convert(IpPort source) {
        log.info("convertor source = {}", source);
        //IpPort 객체 -> "127.0.0.1:8080"
        return source.getIp() + ":" + source.getPort();
    }
}

이렇게 간단하게 구현할 수 있다.

이렇게 만든 Convertor들 ConversionSevice에서 모아둘 수 있다.

ConversionSevice 인터페이스는 컨버팅이 가능한지 확인하는 기능과 컨버팅 기능을 제공한다.

 


 

스프링에 Convertor 적용

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToIntegerConvertor());
        registry.addConverter(new IntegerToStringConvertor());
        registry.addConverter(new StringToIpPortConvertor());
        registry.addConverter(new IpPortToStringConvertor());

    }
}

이렇게 추가한 컨버터가 스프링이 기본 제공하는 컨버터보다 우선순위를 가진다.

 

@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";
    }
    
결과
2024-01-15T13:24:33.664+09:00  INFO 33604 --- [nio-8080-exec-3] h.t.convertor.StringToIpPortConvertor    : convertor source = 127.0.0.1:8080
ipPort IP = 127.0.0.1
ipPort PORT = 8080

@RequestParam에서 ArgumentResolver인 RequestParamMethodArgumentResolve에서 ConvertService를 호출해서 타입을 변환한다.

 


 

뷰 템플릿에 Convertor 적용

타임리프는 렌더링 시에 컨버터를 적요해서 렌더링하는 방법을 지원한다.

 

<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}}, ${{ipPort}}처럼 중괄호가 두개 쳐져있는 것만 컨버터가 동작한다.

 

결과
${number}: 1000000
${{number}}: 1000000
${ipPort}: hello.typeconvertor.type.IpPort@59cb0946
${{ipPort}}: 127.0.0.1:8080

${number}는 스프링에서 알아서 convert해준 것이다.

 

Form에 적용하기'

<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와 th:value 모두 중괄호를 두개 사용했다.

 

결과

이렇게 th:field는 알아서 컨버트해주고, th:value는 해주지 않는다.

th:value에서 중괄호를 두번 사용하면 컨버트해준다.

 


Formatter

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

  • Converter는 범용 (객체 -> 객체)
  • Formatter는 문자에 특화(문자 -> 객체, 객체 -> 문자) + 현지화(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); //locale 정보를 사용해서 나라마다 다른 숫자 포맷 만들기
        Number parse = format.parse(text);
        return parse;
    }

    @Override
    public String print(Number object, Locale locale) {
        log.info("object = {}, locale = {}", object, locale);
        // 1000 -> "1,000"
        NumberFormat instance = NumberFormat.getInstance(locale);
        String format = instance.format(object);
        return format;
    }
}
@Test
    void formattingConversionService() {
        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
        //컨버터 등록
        conversionService.addConverter(new StringToIpPortConvertor());
        conversionService.addConverter(new IpPortToStringConvertor());

        //포맷터 등록
        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(1000);
    }

DefaultFormattingConversionService는 ConversionService관련 기능을 상속 받기 때문에 결과적으로 컨버터도 포맷터도 모두 사용 가능하다!

 

스프링에서 사용하기

컨버터와 똑같이 WebConfig에 등록

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        //우선순위때문에 주석처리
//        registry.addConverter(new StringToIntegerConvertor());
//        registry.addConverter(new IntegerToStringConvertor());
        registry.addConverter(new StringToIpPortConvertor());
        registry.addConverter(new IpPortToStringConvertor());

        //추가
        registry.addFormatter(new MyNumberFormatter());
    }
}
결과
${number}: 1000000
${{number}}: 1,000,000
${ipPort}: hello.typeconvertor.type.IpPort@59cb0946
${{ipPort}}: 127.0.0.1:8080

 

쿼리스트링에서 10,000으로 입력된 것도 10000으로 바뀌는 것을 확인할 수 있었다.

 

 

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

 

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

@NumberFormat과 @DateTimeFormat으로 형식을 구체적으로 지정 가능!

 

Form에서 적용하는 것은 앞에서 설명한 th:field, th:value 그리고 중괄호 두개와 동일하다.

728x90

'Spring' 카테고리의 다른 글

JDBC  (0) 2024.01.31
Spring Toy Project-1  (3) 2024.01.31
API 예외 처리  (0) 2024.01.13
예외 처리, 오류 페이지  (1) 2024.01.11
스프링 인터셉터  (1) 2024.01.09