본 내용은 인프런 김영한T 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 강의를 정리한 내용입니다:)
[Spring] 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 섹션10 스프링 타입 컨버터
◼️ 스프링 타입 컨버터 소개
HTTP 요청 파라미터는 모두 문자로 처리된다. 따라서 다른 타입으로 사용하고 싶다면 아래와 같이 변환을 거쳐야한다.
@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";
}
}
@ModelAttribute UserData data // @ModelAttribute
class UserData {
Integer data;
}
/users/{userId} // @PathVariable
@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);
}
◼️ 타입 컨버터 - Converter
숫자 → 문자 변환 타입 컨버터
@Slf4j
public class IntegerToStringConverter implements Converter<Integer,String> {
@Override
public String convert(Integer source) {
log.info("convert source={}",source);
return String.valueOf(source);
}
}
사용자 정의 타입 컨버터. IP,PORT 문자열 → IpPort객체 변환 타입 컨버터
@Getter
@EqualsAndHashCode // 모든 필드 값이 같다면 a.equals(b) 결과가 참이된다.
public class IpPort {
private String ip;
private int port;
public IpPort(String ip, int port) {
this.ip = ip;
this.port = port;
}
}
@Slf4j
public class StringToIpPortConverter implements Converter<String,IpPort> {
@Override
public IpPort convert(String source) {
log.info("convert 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);
}
}
🪄참고: 스프링은 용도에 따라 다양한 방식의 타입 컨버터를 제공한다.
Converter → 기본 타입 컨버터
ConverterFactory → 전체 클래스 계층 구조가 필요할 때
GenericConverter → 정교한 구현, 대상 필드의 애노테이션 정보 사용 가능
ConditionalGenericConverter → 특정 조건이 참인 경우에만 실행
스프링은 문자, 숫자,불린,Enum등 일반적인 타입에 대한 대부분의 컨버터를 기본으로 제공한다. IDE에서 Converter , ConverterFactory , GenericConverter 의 구현체를 찾아보면 수 많은 컨버터를 확인할 수 있다.
🪄 등록과 사용 분리 컨버터를 등록할 때는 StringToIntegerConverter 같은 타입 컨버터를 명확하게 알아야 한다. 반면에 컨버터를 사용하는 입장에서는 타입 컨버터를 전혀 몰라도 된다.
인터페이스 분리 원칙 - ISP(Interface Segregation Principle) 클라이언트는 자신이 이용하지 않는 메서드에 의존하지 않아야 한다. : DefaultConversionService는 ConversionService(컨버터 사용에 초점) ConverterRegistry(컨버터 등록에 초점) 인터페이스를 따로 구현했다. 결과적으로 컨버터를 사용하는 클라이언트는 꼭 필요한 메서드만 알게된다.
◼️ 스프링에 Converter 적용하기
스프링은 내부에서 ConversionService를 제공하며, addFormatters()를 사용해 추가하고싶은 컨버터를 등록하면 된다.
그러면 스프링은 해당 컨버터를 ConversionService에 등록해준다.
🔶 직접 만든 객체 컨버터 등록
@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("/ip-port")
public String ipPort(@RequestParam("ipPort") IpPort ipPort){
System.out.println("ipPort IP = " + ipPort.getIp()); // ipPort IP = 127.0.0.1
System.out.println("ipPort PORT= " + ipPort.getPort()); // ipPort PORT= 8080
return "ok";
}
🪄 @RequestParam 은 @RequestParam 을 처리하는 ArgumentResolver인 RequestParamMethodArgumentResolver 에서 ConversionService 를 사용해서 타입을 변환한다.
◼️ 뷰 템플릿에 컨버터 적용하기
타임리프는 렌더링 시에 컨버터를 적용해서 렌더링 하는 방법을 편리하게 지원한다. 객체 → 문자 변환 작업도 해보자.
convertView는 모델에 실 객체를 담아 뷰 템플릿에 전달한다.
@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";
}
}
{{ipPort}}: 타입을 String 타입으로 변환해야 하므로 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 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;
}
}
}
🪄 th:field input에 적용하는 필드이다. HTML 태그의 id, name, value 속성을 자동으로 만들어준다. id와 name은 th:field에서 지정한 변수 이름과 같게 만들어진다. value는 th:field에서 지정한 변수의 값(model에 담긴 값)을 사용한다.
웹 실행 결과 :)form.html을 렌더링할때 th:field는 IpPort를 String으로 컨버터 변환한다. @ModelAttribute는 String을 다시 객체 IpPort로 변환한다.
◼️ 포맷터 - Formatter
🟢 포맷터 (Formatter)
객체를 특정한 포멧에 맞추어 문자로 출력하거나 또는 그 반대의 역할
⬜ Converter vs Formatter
Converter 는 범용(객체 → 객체)
Formatter 는 문자에 특화(객체 → 문자, 문자 → 객체) + 날짜 현지화 정보(Locale)
Converter 의 특별한 버전
🔶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> {
}
숫자 1000 ↔ "1,000"1000 단위로 쉼표가 들어가는 포맷을 적용해보자. 그리고 그 반대도 처리해주는 포맷터를 만들어보자.
@Slf4j
public class MyNumberFormatter implements Formatter<Number> {
@Override
public Number parse(String text, Locale locale) throws ParseException { //문자를 숫자로 변환
log.info("text={}, locale={}",text,locale);
// NumberFormat; 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);
}
}
public class MyNumberFormatterTest {
MyNumberFormatter formatter = new MyNumberFormatter();
@Test
void parse() throws ParseException {
Number result = formatter.parse("1,000", Locale.KOREA);
assertThat(result).isEqualTo(1000L); // parse() 결과 Long
}
@Test
void print() {
String result = formatter.print(1000, Locale.KOREA);
assertThat(result).isEqualTo("1,000");
}
}
실습 실행 결과 :)
◼️ 포맷터를 지원하는 컨버전 서비스
포맷터를 지원하는 컨버전 서비스를 사용하면 컨버전 서비스에 포맷터를 추가할 수 있다. 내부에서 어댑터 패턴을 사용 해서 Formatter 가 Converter 처럼 동작하도록 지원한다.
🟢 FormattingConversionService
포맷터를 지원하는 컨버전 서비스
ConversionService관련 기능 상속받기 때문에, 컨버터 포맷터 모두 등록 가능
DefaultFormattingConversionService는 FormattingConversionService 에 기본적인 통화, 숫자 관 련 몇가지 기본 포맷터를 추가해서 제공
스프링 부트는 DefaultFormattingConversionService 를 상속받은 WebConversionService를 내부에서 사용
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);
}
}
◼️ 포맷터 적용하기
포맷터 컨버전 서비스를 적용해보자. 우선순위는 컨버터가 우선하므로 포맷터가 적용되지 않고, 컨버터가 적용된다. 따라서 앞서 등록한 컨버터를 주석 처리한다.
@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());
}
}
"10,000"이라는 포맷팅 된 문자가 Integer타입의 숫자 10000으로 정상 변환 된 것을 확인할 수 있다. :)
◼️ 스프링이 제공하는 기본 포맷터
스프링은 자바에서 기본으로 제공하는 타입들에 대해 수많은 포맷터를 기본으로 제공
☀️ 객체의 각 필드마다 다른 형식으로 포맷을 지정하려면?
@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) {
return "formatter-view";
}
@Data
static class Form {
@NumberFormat(pattern="###,###")
private Integer number;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime localDateTime;
}
}
🪄 주의! 요청의 경우 @RequestBody 를 처리하는 ArgumentResolver 가 있고, HttpEntity 를 처리하는 ArgumentResolver 가 있다. 이 ArgumentResolver 들이 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성하는 것이다. 응답의 경우 @ResponseBody 와 HttpEntity 를 처리하는 ReturnValueHandler 가 있다. 그리고 여기에서 HTTP 메시지 컨버터를 호출해서 응답 결과를 만든다.
메시지 컨버터( HttpMessageConverter )에는 컨버전 서비스가 적용되지 않는다. 예를 들어서 JSON을 객체로 변환하는 메시지 컨버터는 내부에서 Jackson 같은 라이브러리를 사용한다.
🔶 내가 본 핵심 정리
사용자 정의 타입 컨버터를 만들 수 있다.
컨버전 서비스는 컨퍼터들의 모음,관리 기능을 제공한다.
타임리프는 ${{..}} 으로 자동으로 등록되어있는 컨버전 서비스를 사용해서 객체를 String으로 변환한 결과를 출력한다.
타임리프 th:field는 컨버전 서비스를 자동 적용한다.
컨버전 서비스는 @RequestParam , @ModelAttribute , @PathVariable , 뷰 템플릿 등에서 사용할 수 있다.
Formatter는 Converter의 특별 버전으로, Locale을 반영하고 객체↔문자 변환에 사용된다.
컨버터가 포맷터보다 우선순위 적용된다.
각 필드에 Formatter를 적용할 수 있다.
스프링 부트는 DefaultFormattingConversionService 를 상속 받은 WebConversionService 를 내부에서 사용한다.
@RequestBody, HttpEntity 는 컨버전 서비스 적용X. 메시지 컨버터를 사용한다.