[Spring Web MVC] 스프링 타입 컨버터
HTTP 요청 파라미터는 모두 String 으로 처리된다.
따라서 요청 파라미터를 정수나 실수와 같은 다른 타입으로 사용하고 싶은 경우 타입 변환 과정을 거쳐야 한다.
@GetMapping("/hello-v2")
public String helloV2(@RequestParam Integer data) {
System.out.println("data = " + data);
return "ok";
}
@ModelAttribute UserData data
class UserData {
Integer data;
}
/users/{userId}
@PathVariable("userId") Integer data
@RequestParam @ModelAttribute @PathVariable 애너테이션을 사용할 때는 스프링이 중간에 타입을 변환해준다.
public interface Converter<S, T> {
T convert(S source);
}
스프링은 확장 가능한 컨버터 인터페이스를 제공한다.
스프링에 추가적인 타입 변환이 필요하면 이 컨버터 인터페이스를 구현해서 등록 후 사용하면 된다.
public class StringToIntegerConverter implements Converter<String, Integer> {
@Override
public Integer convert(String source) {
return Integer.valueOf(source);
}
}
public class IntegerToStringConverter implements Converter<Integer, String> {
@Override
public String convert(Integer source) {
return String.valueOf(source);
}
}
정수 -> 문자열
문자열 -> 정수 로 변환하는 Converter이다.
@Slf4j
public class StringToIpPortConverter implements Converter<String, IpPort> {
@Override
public IpPort convert(String source) {
//"127.0.0.1:8080" -> IpPort 객체
log.info("convert source={}", source);
String[] split = source.split(":");
String ip = split[0];
int port = Integer.parseInt(split[1]);
return new IpPort(ip, port);
}
}
@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 객체를 <-> 문자열 상호 변환하는 예제이다. (ip는 String 타입이라서 concatnation이 적용된다)
타입 컨버터는 편하려고 만들어 쓰는건데 이렇게 일일이 사용하면 전혀 편하지 않다.
타입 컨버터를 등록하고 관리하면서 좀 편하게 변환할 수 있는 그런게 없을까?
컨버전 서비스를 사용해보자.
컨버전 서비스는 개별 컨버터를 모아두고 묶어서 사용할 수 있는 기능을 제공한다.
@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");
}
컨버전 서비스 인터페이스는 컨버팅이 가능한지 여부를 확인하고 컨버팅 기능을 제공한다.
DefaultConversionService 는 ConversionService 인터페이스의 구현체이고, 컨버터를 등록하는 기능을 제공한다.
등록되는 타입 컨버터들은 모두 컨버전 서비스 내부에서 제공된다.
타입 변환 기능을 사용하는 쪽에서는 서비스 인터페이스에만 의존하고, 타입 컨버터에 대해서는 몰라도 상관 없게 설계해야한다.
인터페이스 분리 원칙에 따르면 클라이언트는 자신이 이용하지 않는 메서드에 의존하지 않아야 한다.
DefaultConversionService를 살펴보면 컨버터의 사용에 초점을 맞춘 ConversionService 인터페이스와 컨버터의 등록에 초점을 맞춘 ConverterRegistry 인터페이스를 상속받아 관심사를 명확하게 분리하고 있다.
컨버터를 사용하는 클라이언트는 ConversionService만 의존하면 되기에, 컨버터를 어떻게 등록하고 관리하는지는 몰라도 되고 꼭 필요한 메서드만 알게 된다.
이렇게 인터페이스를 분리하는 작업을 ISP라고 부른다.
@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());
}
}
컨버터를 등록해보자.
WebMvcConfiguer가 제공하는 addFormatters() 메서드를 사용해 컨버터를 등록하면 스프링 내부에서 ConversionService에 컨버터를 등록한다.
사실 스프링은 수 많은 기본 컨버터들을 제공하기 때문에.. 문자열을 정수로 바꾸거나 정수를 문자열로 바꾸는 간단한 작업은 기본 컨버터가 알아서 처리해준다. (별도로 추가한 컨버터는 기본 컨버터보다 우선순위가 높다)
@RequestParam을 사용하면 RequestParamMethodArgumentResolver에서 ConversionService를 사용해 타입을 변환해준다.
HTTP 요청을 컨트롤러에서 받을 때 컨버터를 사용해 문자를 객체로 만들었다.
뷰 템플릿에서 뷰를 렌더링 할 때는 반대로 객체를 문자로 변환하는 작업을 수행한다.
<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>
타임리프에서 숫자는 문자로 자동 변환돼서 출력되니, 컨버터를 적용하든 말든 똑같다.
${{}} 로 변수를 괄호로 두 번 감싸면 컨버터가 적용된다.
${}로 실행하면 객체 주소로 출력되지만, ${{}}로 실행 시 컨버터를 사용한 결과를 출력한다. (객체는 toString 결과를 출력한다)
<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 속성을 사용하면 괄호를 하나만 사용해도 컨버터가 적용된다.
숫자 1000을 1,000 으로 / 날짜 객체를 특정 형식으로 출력하거나..
이렇게 객체를 특정한 포멧에 맞춰 문자로 출력하거나, 그 반대를 수행할 때 특화된 기능이 Formatter 이다.
컨버터는 객체를 특정 객체로 바꿀 때 사용하고, 포매터는 객체를 문자로 바꾸거나 문자를 객체로 바꿀 때 사용한다.
즉, 포매터는 컨버터의 특별한 버전이다.
@Slf4j
public class MyNumberFormatter implements Formatter<Number> {
@Override
public Number parse(String text, Locale locale) throws ParseException {
log.info("{} {}", text, locale);
NumberFormat format = NumberFormat.getInstance(locale);
return format.parse(text);
}
@Override
public String print(Number object, Locale locale) {
return NumberFormat.getInstance(locale).format(object);
}
}
날짜와 숫자의 표현 방법은 locale을 사용해 현지화 정보를 가져온다.
1,000처럼 숫자 중간에 쉼표를 넣어서 숫자를 표현하는 기능은 자바에서 제공하는 NumberFormat 객체를 사용해서 구현하자.
locale 정보를 사용해 나라별로 다른 숫자 포맷을 만들어준다.
parse()로 문자를 숫자로 변환하고, print()로 객체를 문자로 변환한다.
ConversionService에는 포매터를 등록할 수 없지만, 포매터를 지원하는 ConversionService를 사용하면 포매터를 추가할 수 있다. (내부에서 어댑터 패턴을 사용해 Formatter가 Converter처럼 동작하도록 지원한다)
즉, FormattingConversionService는 ConversionService관련 기능을 상속받기 때문에 컨버터와 포맷터 모두 등록할 수 있다.
스프링 부트는 DefaultFormattingConversionService를 상속받은 WebConversionService를 내부적으로 사용하니, 포매터 등록 관련 문제는 깊게 생각하지 말고 그냥 쓰자!
스프링은 자바에서 제공하는 타입들에 대해 수많은 포매터들을 기본으로 제공한다.
포맷터들은 기본 형식이 정해져 있기 때문에 객체의 각 필드마다 다른 형식으로 포맷을 지정하기는 쉽지 않다.
이런 문제를 해결하기 위해 애너테이션을 기반으로 하는 포매터가 등장했다.
@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 애너테이션은 날짜 관련 형식 지정 포맷터가 사용된다.
애너테이션의 pattern 요소로 원하는 포맷을 지정해 줄 수 있다.
컨버터와 포매터 모두 컨버전 서비스를 통해 등록하고 사용할 수 있다.
* 메세지 컨버터에는 컨버전 서비스가 적용되지 않음에 주의하자.
메세지 컨버터는 HTTP 메세지 바디의 내용을 객체로 변환하거나 객체를 HTTP 메세지 바디에 입력하는 역할을 한다.
즉, 메세지 컨버터는 컨버전 서비스와 전혀 관계없다.
'Spring > Spring Web MVC' 카테고리의 다른 글
[Spring Web MVC] 로그인 처리 - Cookie / Session (0) | 2023.07.10 |
---|---|
[Spring Web MVC] Multipart (1) | 2022.08.29 |
[Spring Web MVC] API 예외 처리 (0) | 2022.08.26 |
[Spring Web MVC] 예외 처리와 오류 페이지 (0) | 2022.08.26 |
[Spring Web MVC] Bean Validation (0) | 2022.08.23 |
댓글
이 글 공유하기
다른 글
-
[Spring Web MVC] 로그인 처리 - Cookie / Session
[Spring Web MVC] 로그인 처리 - Cookie / Session
2023.07.10 -
[Spring Web MVC] Multipart
[Spring Web MVC] Multipart
2022.08.29 -
[Spring Web MVC] API 예외 처리
[Spring Web MVC] API 예외 처리
2022.08.26 -
[Spring Web MVC] 예외 처리와 오류 페이지
[Spring Web MVC] 예외 처리와 오류 페이지
2022.08.26