10. 스프링 타입 컨버터

ys·2024년 1월 16일

Spring-mvc2

목록 보기
10/10

김영한 강사님의 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술을 듣고 정리한 내용입니다. 자세한 내용은 강의를 참고해주세요

  • 생각을 해보면, 우리가 파라미터를 보내,받거나, @PathVariable을 이용해, url을 받을 때 -> 모두 자료형이 String이다
  • 그런데 우리가 타입을 변환하지 않아도, 다 타입이 바꿔서 사용할 수 있다.
  • 모두 스프링이 자동으로 바꿔주는데, 바로 Converter로 바꿔 주는 것이다
  • 이렇게 url경로의 문자열을 Integer타입으로 자동으로 바꿔준다.

스프링 타입 변환 적용의 예

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

Converter

  • 스프링은 확장 가능한 컨버터 인터페이스를 제공한다
  • 개발자가 스프링에 추가적인 타입 변환이 필요하다면, 이 컨퍼에 인터페이스를 구현후, @Configuration의 클래스에 등록하면 된다(WebMvcConfigurer 구현)
  • 컨버터 인터페이스는 보다시피 제네릭형태로 되어있는데, Converter<S,T>로, 메서드 convert를 사용하면 S타입을 T타입으로 변경한다.
  • 예제의 다양성을 위해 127.0.0.1:8080같은 스트링을 객체로 포트와 아이피로 나누는 자료형을 하나 만든다
  • @EqualsAndHashCode를 사용해, 참조 자료형이, 참조값이 달라도, 값이 같으면 equals결과가 침이 되게 한다

parseInt(): 원시데이터인 int 타입을 반환
Integer.valueOf(): Integer 래퍼(wrapper)객체를 반환

@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("data") Integer data){
        System.out.println("data = " + data);
        return "ok";
    }

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

}
  • 이렇게 컨트롤러를 이용해, 우리가 값을 원하는 자료형으로 변경하도록 코드를 짰다
  • 그런데, 이렇게 타입 컨버터를 하나하나 직접 사용하면, 개발자가 직접 컨버팅을 하는것과 뭔 차이가 있을까?
  • 타입 컨버터를 등록하고 관리하면서 편리하게 변환기능을 제공하는 역할을 하는 무언가가 필요하다
  • 그것이 바로 컨버전 서비스 ConversionService이다

참고
스프링은 용도에 따라 다양한 방식의 타입 컨버터를 제공한다.
Converter: 기본 타입 컨버터
ConverterFactory: 전체 클래스 계층 구조가 필요할 때
GenericConverter: 정교한 구현, 대상 필드의 애노테이션 정보 사용 가능
ConditionalGenericConverter: 특정 조건이 참인 경우에만 실행

ConversionService

  • 아까 말했듯, 타입 컨버터를 하나하나 직접 찾아서 사용하면 불편하다
  • 스프링은 개별 컨버터를 모아두고 그것들을 묶어서 편리하게 사용할 수 있는 기능을 제공한다
  • 이것이 바로 컨버전 서비스
  • canConvert : 컨버팅이 가능한가?
  • convert : 컨버팅이 가능하다면, 컨버팅 기능을 제공
  • 우리는 사용할 때, DefaultConversionService를 이용하는데
  • 여기에는 등록기능과, 컨버터 사용기능이 모두 구현되어 있다.
  • 위 그림처럼 인터페이스를 구현을 하고 있는데
  • ConversionService : 컨버터 사용에 초점
  • ConverterRegistry : 컨버터 등록에 초점
  • 스프링이 다음과 같이 다른 인터페이스로 나누어서 상속 받는 이유는...

등록과 사용의 분리! -> 즉 역할에 따라 분리를 했기 때문이다(ISP)

  • 스프링 등록시에는, 우리가 만든 StringToIntegerConverter같은 타입 컨버터를 명확히 알아야 한다
  • 그런데 사용자인 클라이언트 입장에서는 굳이 저 타입 컨버터를 알아야 될까???
  • 아니다 몰라도 된다. 모두 컨버전 서비스 내부에 숨어있기 때문에
  • 사용자는 컨버전 서비스 인터페이스에만 의존하면 된다
  • 그렇기에 인터페이스를 다음과 같이 분리하여, 의존관계를 내가 사용하는 거에만 의존하게 바꾸었다
  • 좋은 객체 지향 설계 원칙인 SOLIDISP(Interface Segregation Principle)을 잘 지켰고, 좋은 설계라고 할 수 있다.
@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());
	}
}
  • @Configuration이 있는 WebConfig에 WebConfigurer을 구현해,
    addFormatters라는 메서드를 오버라이드해서, 추가하고 싶은 컨버터를 등록한다
  • 이렇게 등록하면, 스프링 내부에서 사용하는 ConversionService에 컨버터를 추가할 수 있다!!!
  • 스프링에서는 수 많은 기본 컨버터를 제공하는데
  • 이렇게 WebConfig에 추가한 컨버터가, 스프링에서 제공하는 기본 컨버터보다 높은 우선순위를 갖는다.
  • 만약 우리가 @RequestParam을 이용한다고 하면, ArgumentResolver의 구현체인 RequestParamMethodArgumentResolver에서 ConversionService를 사용해 원하는 타입으로 바꿀 수 있다!

뷰 템플릿에 컨버터 적용

  • 뷰 템플릿에 컨버터를 적용할 수 있다
  • 타입리프는 렌더링 시에 컨버터를 적용해, 편리하게 원하는 타입으로 사용할 수 있다
  • 타임리프는 ${{...}}를 사용하면 자동으로 컨버전 서비스를 사용해서 변환된 결과를 출력해준다

{} 1개 : converter적용 X
{{}} 2개 : converter적용O

  • th:field는 converter까지 자동으로 적용해버린다, {{}}를 2개 쓸 필요가 없다

  • 만약 컨버터 적용하기 싫다?? -> th:value를 사용하면 된다

  • ${{number}} : 뷰 템플릿은 데이터를 문자로 출력한다. 따라서 컨버터를 적용하게 되면 Integer 타입인 10000 을 String 타입으로 변환하는 컨버터인 IntegerToStringConverter 를 실행하게 된다.

  • ${{ipPort}} : 뷰 템플릿은 데이터를 문자로 출력한다. 따라서 컨버터를 적용하게 되면 IpPort 타입을 String 타입으로 변환해야 하므로 IpPortToStringConverter 가 적용된다. 그 결과 127.0.0.1:8080 가 출력된다.

컨버터를 Form에 적용하기

@Slf4j
@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 form, Model model){
        IpPort ipPort = form.getIpPort();
        model.addAttribute("ipPort",ipPort);
        return "converter-view";
    }

    @Getter
    @Setter
    @RequiredArgsConstructor
    static class Form{
        private IpPort ipPort;

        public Form(IpPort ipPort) {
            this.ipPort = ipPort;
        }
    }
}
  • Form 객체를 이용해, IpPort를 이용하기위해서 추가해준다
  • th:objectth:field="*{}"문법을 이용할 수 있다.
  • Model이니까 addAttribute로 잘 담아서 view에서 이용하자!
  • GET / converter/ edit
    • th:field자동으로 컨버전 서비스를 적용해주어서 %{{isPort}}처럼 적용되었다
    • IsPort객체가 String으로 변환되었다
  • POST / converter/ edit
    • @ModelAttribute를 사용해서 String자료를 IpPort로 변환하고
    • convert-view에서 IpPort 객체를 이용한다

Formatter

  • 포멧터는 컨버터의 특수한 예인데
  • 예제와 실제 사례를 보면 객체를 문자로 변환하거나, 문자를 객체로 변환하기 위한 경우가 많다
  • 이런 경우를 위해 Formatter가 있다
  • 추가로 Locale 정보가 있는데, 이를 통해 현지화 정보를 사용한다
  • 이렇게 객체를 특정한 포멧에 맞추어 문자로 출력 혹은 반대 역할을 하는데에 특화된 기능이 바로 포맷터이다
  • 다음과 같이 컨버터가 더 범용적이라는 것을 알 수 있다.

  • 이렇게 인터페이스를 구현해서 사용한다
  • Fomatter는 문자열이 고정되어 있기 때문에, 제네릭에 바꿀 객체 타입만 넣어주면된다

숫자 1000을 1,000으로 바꾸는 포맷, 반대도 해주는 포맷터를 만들어 보자

@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);
    }
}
  • Local정보로 포맷팅을 해주는 자바 문법인 NumberFormat기능을 이용해준다
  • parse() : 문자를 -> 객체
  • print() : 객체를 -> 문자
  • 파라미터 : 바꿀 객체, Locale.KOREA(로케일 정보)

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

  • 컨버전 서비스에는 컨버터만 등록할 수 있고, 포맷터를 등록할 수 는 없다.
  • 그런데 생각해보면 포맷터는 객체 문자, 문자 객체로 변환하는 특별한 컨버터일 뿐이다.
  • 포맷터를 지원하는 컨버전 서비스를 사용하면 컨버전 서비스에 포맷터를 추가할 수 있다.
  • 내부에서 어댑터 패턴을 사용해서 Formatter 가 Converter 처럼 동작하도록 지원한다.
  • FormattingConversionService 는 포맷터를 지원하는 컨버전 서비스이다
  • DefaultFormattingConversionServiceFormattingConversionService 에 기본적인 통화, 숫자 관
    련 몇가지 기본 포맷터를 추가해서 제공한다.
  • FormattingConversionServiceConversionService 관련 기능을 상속받기 때문에 결과적으로 컨버터도 포맷터도 모두 등록할 수 있다.
  • 그리고 사용할 때는 ConversionService 가 제공하는 convert 를 사용하면 된다.
  • 추가로 스프링 부트는 DefaultFormattingConversionService 를 상속 받은 WebConversionService내부에서 사용한다

포맷터 적용하기

  • 우선순위는 컨버터가 우선하므로 포맷터가 적용되지 않고, 컨버터가 적용된다.
  • 다시 @Configuration 파일에 등록해주고
  • 우리가 원하는데로 포맷팅이 된 것을 알 수 있다

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

  • 스프링은 자바에서 기본으로 제공하는 타입들에 대해 수 많은 포맷터를 기본으로 제공한다.
  • 포맷터는 기본 형식이 지정되어 있기 때문에, 객체의 각 필드마다 다른 형식으로 포맷을 지정하기는 어렵다
  • 같은 타입이면 같은 포맷터가 적용됬었는데....
  • 이제는 필드마다 어노테이션 처리를 해줘서 -> 같은 타입이어도 다른 포맷터를 적용할 수 있다는 말이다!!
  • 정말 어노테이션은 대박인거 같다

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

@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 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 @LocalDateTime 각각 에노테이션을 붙여서 적용을 한다!!!
  • pattern 파라티머를 이용해, 원하는 모양으로 포맷팅을 하는 것이다
  • 결과가 원하는대로 나오는 것을 알 수 있다.
  • 컨버터를 사용하든, 포맷터를 사용하든 등록 방법은 다르지만, 사용할 때는 컨버전 서비스를 통해서 일관성 있게 사용할수 있다.

주의!
메시지 컨버터( HttpMessageConverter )에는 컨버전 서비스가 적용되지 않는다.
특히 객체를 JSON으로 변환할 때 메시지 컨버터를 사용하면서 이 부분을 많이 오해하는데,
HttpMessageConverter 의 역할은 HTTP 메시지 바디의 내용을 객체로 변환하거나 객체를 HTTP 메시지 바디에 입력하는 것이다. 예를 들어서 JSON을 객체로 변환하는 메시지 컨버터는 내부에서 Jackson 같은 라이브러리를 사용한다.
객체를 JSON으로 변환한다면 그 결과는 이 라이브러리에 달린 것이다.
따라서 JSON 결과로 만들어지는 숫자나 날짜 포맷을 변경하고 싶으면 해당 라이브러리가 제공하는 설정을 통해서 포맷을 지정해야 한다.
결과적으로 이것은 컨버전 서비스와 전혀 관계가 없다.

jackson 데이터 포멧터
ObectMapper 데이터 포멧터
이렇게 검색해야 함!!!(해당 라이브러리의)

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

profile
개발 공부,정리

0개의 댓글