내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.
1. 스프링 타입 컨버터 소개
다음과 같은 컨트롤러가 하나 있다고 하자!
@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";
}
HTTP 요청 파라미터는 모두 문자로 처리된다.
따라서 요청 파라미터를 자바에서 다른 타입으로 변환해서 사용하고 싶으면 다음과 같이 숫자 타입으로 변환하는 과정을 거쳐야 한다.
위 코드에서도 다음과 같이 String -> Integer로 타입을 변환하여 사용하고 있다.
Integer intValue = Integer.valueOf(data);
● 스프링의 타입 변환
스프링을 사용하면 @RequstParam, @ModelAttribute , @PathVariable 에서도 타입 컨버팅을 확인할 수 있다.
예를 들어 쿼리 스트링으로 ?data=10 으로 전달하면, 여기서 10은 String의 10으로 전달된다.
이를 @RequstParam 같은것을 사용하면 Interger 타입의 10으로 변환되어 받을수가 있다.
이것은 스프링이 중간에서 타입을 변환해주었기 때문이다.
이러한 예는 @ModelAttribute , @PathVariable 에서도 확인할 수 있다.
이러한 중간 타입 변환 과정을 스프링에서 전부 도와주고 있다.
더 나아가 만약 개발자가 새로운 타입을 만들어 컨버팅 하고싶다면? 어떻게 해야할까?
=> 컨버터 인터페이스 를 사용하면 된다.
package org.springframework.core.convert.converter;
public interface Converter<S, T> {
T convert(S source);
}
스프링은 이러한 확장 가능한 컨버터 인터페이스를 제공한다.
개발자는 스프링에 추가적인 타입 변환이 필요하면 언제든 이 컨버터 인터페이스를 구현해서 등록하면 된다!
2. 타입 컨버터 - Converter
타입 컨버터를 사용하려면 org.springframework.core.convert.converter.Converter 인터페이스를 구현하면 된다.
예시로 127.0.0.1:8080 과 같은 IP, PORT를 입력하면 IpPort 객체로 변환하는 컨버터를 만들어보자.
우선 객체로 사용될 IpPort Class 는 다음과 같다.
@Getter
@EqualsAndHashCode
public class IpPort {
private String ip;
private int port;
public IpPort(String ip, int port) {
this.ip = ip;
this.port = port;
}
}
이제 이 class를 활용하는 컨버터를 구현해 보자.
● String => IpPort
@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);
}
}
● IpPort => String
@Slf4j
public class IpPortToStringConverter implements Converter<IpPort, String> {
@Override
public String convert(IpPort source) {
log.info("Convert Source = {}", source);
return source.getIp() + ":" + source.getPort();
}
}
양방향으로 컨버팅이 가능하도록 2가지 모두 구현해 주었다.
이제 이를 사용하는 Test 코드를 작성해 보자!
● converterTest
public class converterTest {
@Test
void stringToIpPort() {
StringToIpPortConverter converter = new StringToIpPortConverter();
String source = "127.0.0.1:8080";
IpPort result = converter.convert(source);
assertThat(result).isEqualTo(new IpPort("127.0.0.1", 8080));
}
@Test
void ipPortToString() {
IpPortToStringConverter converter = new IpPortToStringConverter();
IpPort source = new IpPort("127.0.0.1", 8080);
String result = converter.convert(source);
assertThat(result).isEqualTo("127.0.0.1:8080");
}
}
실행결과 원하는 결과를 얻을 수 있다.
하지만 한가지 의문이 든다. 이렇게 컨버터를 직접 구현한후, 하나하나 객체를 만들고, Source를 넘기고 사용하는 방식은 매우 불편한다.
이렇게 타입 컨버터를 하나하나 직접 사용하면, 개발자가 직접 컨버팅 하는 것과 큰 차이가 없다.
Spring은 이를 해결하기 위해 ConversionService 를 제공한다. 이는 다음 단락에서 알아보자!
3. 컨버전 서비스 - ConversionService
타입 컨버터를 하나하나 직접 찾아서 타입 변환에 사용하는 것은 매우 불편하다.
그래서 스프링은 개별 컨버터를 모아두고 그것들을 묶어서 편리하게 사용할 수 있는 기능을 제공하는데, 이것이 바로 컨버전 서비스( ConversionService )이다.
테스트 코드를 통해서 사용예를 살펴봅시다.
public class ConversionServiceTest {
@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 를 사용했는데, DefaultConversionService 는 GenericConversionService를 상속 했으며,
GenericConversionService 는 => ConfigurableConversionService를 구현하였으며,
ConfigurableConversionService가 ConversionService를 확장하였다.
결론적으로 DefaultConversionService 는 ConversionService 인터페이스를 구현했는데, 추가로 컨버터를 등록하는 ConverterRegistry도 구현했기 때문에 등록 기능도 제공한다.
● 등록과 사용의 분리
컨버터를 등록할 때는 StringToIntegerConverter 같은 타입 컨버터를 명확하게 알아야 하지만,
반면 컨버터를 사용하는 입장에서는 타입 컨버터를 전혀 몰라도 된다.
사용자는 컨버전 서비스의 인터페이스에만 의존하면서 사용만 하면 된다.
● 인터페이스 분리 원칙 - ISP(Interface Segregation Principal)
인터페이스 분리 원칙은 클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다.
위에서 DefaultConversionService 는 다음 두 인터페이스를 구현했었다.
- ConversionService : 컨버터 사용에 초점
- ConverterRegistry : 컨버터 등록에 초점
이렇게 인터페이스를 분리하면 컨버터를 사용하는 클라이언트와 컨버터를 등록하고 관리하는 클라이언트의 관심사를 명확하게 분리할 수 있다.
특히 컨버터를 사용하는 클라이언트는 ConversionService만 알면 되기 때문에 관리하는 부분은 전혀 몰라도 된다.
이러한 인터페이스 분리를 ISP 라고 부른다.
4. 스프링에 Converter 적용하기
지금까지 만든 컨버터를 등록하여 Spring에서 사용해 봅시다!
@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());
}
}
스프링은 내부에서 ConversionService 를 제공해주고 있다.
우리는 WebMvcConfigurer 가 제공하는 addFormatters() 를 사용해서 추가하고 싶은 컨버터를 등록하면 된다.
이렇게 하면 스프링은 내부에서 사용하는 ConversionService 에 컨버터를 추가해준다.
컨버터를 추가하면 추가한 컨버터가 기본 컨버터 보다 높은 우선순위를 가진다.
● IpPort 컨트롤러 사용해 보기
기존에 우리가 만든 IpPort class를 사용하여 컨버팅을 진행해 보자.
@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";
}
http://localhost:8080/ip-port?ipPort=127.0.0.1:8080 로 요청을 보내보자!
결과는 다음과 같다.
로그가 정상적으로 출력 되었다.
● 처리 과정
@RequestParam은 @RequestParam 을 처리하는 ArgumentResolver 인 RequestParamMethodArgumentResolver 에서 ConversionService 를 사용해서 타입을 변환한다.
이후 컨트롤러가 호출될때 인자로 IpPort의 객체가 전달되어 정보를 출력하게 된다.
5. 뷰 템플릿에 컨버터 적용하기
컨트롤러에서 view로 데이터를 전달할때, 객체를 문자로 변환하여 화면에 렌더링 해주어야 한다. 이 작업을 해보자.
● 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";
}
model에 숫자 10000과 IpPort 객체를 담아서 뷰에 전달하고 있다.
● converter-view.html
<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} 의 10000은 String "10000"이 출력된 것 이다.
${{number}}는 String을 Integer로 변환하는 IntegerToStringConverter를 사용하여 숫자 10000을 출력한 것 이다.
${ipPort}는 객체 그대로를 출력한 것 이다. 객체를 출력하려 했기 때문에 객체.toString()이 호출된 결과이다.
${{ipPort}}는 IpPortToStringConverter를 상용해 객체를 String으로 타입 컨버팅 하여 출력한 것 이다.
컨버팅 되는 동안 출력된 결과를 보면 알수있다.
IntegerToStringConverter와 IpPortToStringConverter가 사용된것을 알 수 있다.
● 폼에 적용하기
@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;
}
}
- converter-form 일부
<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>
처음 Get방식으로 "/converter/edit" 에 요청을 보내면 IpPort 객체를 Form에 담아서 model에 추가해주고 있다.
따라서 처음 화면에 IpPort에 대한 정보가 담겨서 보여진다. 다음과 같이 말이다.
th:field는 {{ }} 처럼 2중 괄호를 사용하지 않았는데도 컨버전 서비스가 적용되었다.
타임리프의 th:field 는 앞서 설명했듯이 id , name 를 출력하는 등 다양한 기능이 있는데, 여기에 컨버전 서비스도 함께 적용된다.
이제 제출 버튼을 눌러보자! 결과는 다음과 같다.
Form이 전달될때 Post 방식으로 전달 되었다.
이때 전달된 데이터는 "127.0.0.1:8080" 이라는 String이 전달된다.
위 HTML을 보면 value="127.0.0.1:8080" 이라는 String값을 확인할수 있다.
따라서 @ModelAttribute가 적용되게 된다. 넘어온 String 데이터를 Form 객체로 변환해야 하는데, Form객체 안에 IpPort 객체 또한 존제한다. 따라서 IpPort 객체를 만들게 된다. 이때 String 이 IpPort 로 변환되게 된다.
'BackEnd > Spring MVC' 카테고리의 다른 글
[Spring] 파일 업로드 (0) | 2022.03.21 |
---|---|
[Spring] 스프링 타입 컨버터 - 2 (0) | 2022.03.18 |
[Spring] API 예외 처리 - 2 (0) | 2022.03.15 |
[Spring] API 예외 처리 - 1 (0) | 2022.03.14 |
[Spring] 예외 처리와 오류 페이지 - 2 (0) | 2022.03.14 |
댓글