본문 바로가기
Spring/MVC 2편

스프링 타입 컨버터

by JHyun0302 2023. 8. 8.
728x90
  • 문자를 숫자로 변환 or 숫자를 문자로 변환 같은 타입 변환

 

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

 

http://localhost:8080/hello-v2?data=10

  • @RequestParam 사용하면 문자 10을 Integer 타입의 숫자 10으로 편리하게 변환해준다.
  • @ModelAttribute , @PathVariable도 마찬가지

스프링이 중간에서 타입을 변환해주었기 때문

 

 

 

 


Converter

 

 

개발자가 직접 새로운 타입을 만들어서 변환

 

public interface Converter<S, T> {
	T convert(S source);
}

 

ex) 문자로 "true" → Boolean 타입으로 받고 싶으면 String Boolean 타입으로 변환되도록 컨버터 인터페이스를 만들어서 등록

 

 

 

 

StringToIntegerConverter

@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

 

 

IntegerToStringConverter

@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 

 

 

 

 

사용자 정의 타입 컨버터

 

 

IpPort

@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

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

String → IpPort

 

 

 

 

IpPortToStringConverter

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

 

 

 

 

 

 

 


 ConversionService

 

 

  • 타입 컨버터를 하나하나 직접 찾아서 타입 변환에 사용해야하는 불편함을 해소 시켜줌

 

 

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

 

 

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

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

 

 

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

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

 

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

컨버터를 사용하는 클라이언트는 ConversionService 의존되며 컨버터를 어떻게 등록하고 관리하는지는 전혀 몰라도 된다.

 

 

 

 

 

 


스프링에 Converter 적용하기

 

WebConfig

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

 

 

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

 

실행 시 직접 등록한 StringToIntegerConverter 가 작동한다.

 

 

 

 

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

 

실행 시 직접 등록한 StringToIpPortConverter 가 작동한다.

 

 


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

 

 

  • 객체를 문자로 변환하는 작업을 확인

 

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

  

실행 시 직접 등록한 IntegerToStringConverter 가 작동한다.

실행 시 직접 등록한 IpPortToStringConverter 가 작동한다.

 

 

 

 

 

 

폼에 적용하기

 

@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 객체를 데이터를 전달하는 폼 객체로 사용

 

 

 

 


포맷터 - Formatter 소개

 

 

객체를 문자로, 문자를 객체로 변환하는 예

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

 

Locale

  • 여기에 추가로 날짜 숫자의 표현 방법은 Locale 현지화 정보가 사용될 수 있다.

 


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

 

 

Converter vs Formatter

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

 

 

 


포맷터 - 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> {
}

 

 

 

 

@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 객체 사용
  • print() 를 사용해서 객체  문자

 

 

 

 


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

 

 

  • DefaultFormattingConversionService는 FormattingConversionService 에 통화, 숫자 등 기본 포맷터를 추가 제공

 

 

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

 

 

 

WebConfig

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

 

우선순위는 컨버터가 우선하므로 포맷터가 적용되지 않고, 컨버터가 적용된다.

 

 

 

 

 

 

 

 


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

 

 

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

 

 

 

FormatterController

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

 

 

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

 

결과

  • ${form.number}: 10000
  • ${{form.number}}: 10,000
  • ${form.localDateTime}: 2021-01-01T00:00:00
  • ${{form.localDateTime}}: 2021-01-01 00:00:00

 

◎ 참고 : 공식 메뉴얼

https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#format-

CustomFormatAnnotations

 

 

 

★ 정리

컨버터를 사용하든, 포맷터를 사용하든 등록 방법은 다르지만, 사용할 때는 컨버전 서비스를 통해서 일관성 있게 사용 가능

 

 

 

주의!

  • HttpMessageConverter 의 역할은 HTTP 메시지 바디의 내용 → 객체 or 객체  HTTP 메시지 바디에 입력
  • "JSON 객체" 담당 메시지 컨버터는 내부에서 Jackson 같은 라이브러리를 사용
  • 결과적으로 컨버전 서비스와 전혀 관계가 없다.

 

 

 

 

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

 

반응형

'Spring > MVC 2편' 카테고리의 다른 글

파일 업로드  (0) 2023.08.08
API 예외 처리  (0) 2023.08.08
예외 처리 & 오류 페이지  (0) 2023.08.07
로그인 처리2 - 필터, 인터셉터  (0) 2023.08.07
로그인 처리1 - 쿠키, 세션  (0) 2023.08.07