HTTP 요청을 통해 서버로 넘어온 정보 중, 서버에서 원하는 타입이 아닌 정보가 있을 수 있다. 각 필드들을 원하는 타입으로 변환하는 자바 코드를 사용해도 되지만, 스프링에서 제공하는 타입 컨버터 기능을 사용하면 자동으로 타입 변환이 되도록 할 수 있을 뿐만 아니라 사용자 지정 타입의 필드값으로 변경할 수 있는 등 여러 가지 장점이 있다.
1. 프로젝트 생성
아래 사진과 같이 생성하고, 늘 했던 방식과 같이 Build는 intelliJ가 하도록, Encoding은 UTF-8형식, Annotation Processor를 사용할 수 있도록 설정해준다.
2. Converter 인터페이스
스프링의 기본 타입 변환 지원
스프링은 기본적인 타입 변환은 자동으로 지원한다. 아래 예시를 보자.
helloV1 컨트롤러는 요청의 쿼리파라미터로 넘어온 문자 타입의 data를 직접 컨트롤러에서 Integer 타입으로 변환한다. 쿼리 파라미터로 넘어오는 정보는 모두 String인데, 이것을 숫자로 변환하는 것이다.
helloV2 컨트롤러에서는 @RequestParam으로 받는 쿼리 파라미터의 타입을 Integer로 선언해버렸다. 이것은 스프링에서 String으로 넘어오는 data 파라미터를 자동으로 Integer로 변환해주기 때문에 가능하다. 주석문에 작성한 것과 같이, @ModelAttribute, @PathVariable도 이러한 타입 변환이 지원된다.
java/hello/typeconverter/controller/HelloController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@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";
}
//@ModelAttribute, @PathVariable도 스프링 타입 변환이 적용된다.
}
|
cs |
Converter 인터페이스
만약 기본적으로 쓰는 String, Integer 등과 같은 타입이 아니라 새로운 타입을 만들어서 변환하고 싶다면, Converter 인터페이스를 사용하면 된다. Converter 라이브러리는 여러 개가 있는데, 아래 경로의 라이브러리를 Import 해와야 한다.
org.springframework.core.convert.converter.Converter
Converter를 직접 사용해보자! 4가지 컨버터를 만들고 테스트 해볼 것이다.
별로 어렵지 않다. Converter 인터페이스를 implements 받되, Generic에 해당하는 source와 반환 타입을 직접 입력해주어야 한다. 여기서는 Integer, String으로 작성해서 Integer 값을 String으로 변환한다. 그리고, convert 메서드를 Override 해서 사용해주면된다.
1
2
3
4
5
6
7
8
9
|
@Slf4j
public class IntegerToStringConverter implements Converter<Integer, String> {
@Override
public String convert(Integer source) {
log.info("convert source={}", source);
return String.valueOf(source);
}
}
|
cs |
나머지 컨트롤러들도 작성한다. 하나도 어려울 게 없다.
1
2
3
4
5
6
7
8
|
@Slf4j
public class StringToIntegerConverter implements Converter<String, Integer> {
@Override
public Integer convert(String source) {
log.info("convert source={}", source);
return Integer.valueOf(source);
}
}
|
cs |
1
2
3
4
5
6
7
8
9
|
@Slf4j
public class IpPortToStringConverter implements Converter<IpPort, String> {
@Override
public String convert(IpPort source) {
log.info("convert source={}", source);
//"127.0.0.1:8080"
return source.getIp() + ":" + source.getPort();
}
}
|
cs |
1
2
3
4
5
6
7
8
9
10
11
12
|
@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];
Integer port = Integer.parseInt(split[1]);
return new IpPort(ip, port);
}
}
|
cs |
IpPort 객체는 다음과 같다. String으로 받아오는 값을 IpPort로 변환하는 코드를 보면, 우리가 직접 만든 타입으로도 얼마든지 변환이 가능하다는 것을 알 수 있다. 실무에서 상당히 유용하게 쓰일 수 있을 것 같다.
@EqualsAndHashCode는 IpPort 객체를 .equals로 비교할 때, 실제 객체의 메모리 주소값과 상관없이, ip와 port만 같으면 같은 값으로 취급할 수 있게 해준다.
1
2
3
4
5
6
7
8
9
10
11
12
|
@Getter
@EqualsAndHashCode
public class IpPort {
private String ip;
private int port;
public IpPort(String ip, int port) {
this.ip = ip;
this.port = port;
}
}
|
cs |
테스트
테스트 코드는 다음과 같다. 간단하다. 그러나 향후 스프링이 제공하는 타입 컨버터, ConversionService를 이해하기 위해 밑바닥부터 이해해나간다고 생각하면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
public class ConverterTest {
@Test
void stringToInteger() {
StringToIntegerConverter converter = new StringToIntegerConverter();
Integer result = converter.convert("10");
assertThat(result).isEqualTo(10);
}
@Test
void integerToString() {
IntegerToStringConverter converter = new IntegerToStringConverter();
String result = converter.convert(10);
assertThat(result).isEqualTo("10");
}
@Test
void stringToIpPort() {
StringToIpPortConverter converter = new StringToIpPortConverter();
IpPort result = converter.convert("127.0.0.1:8080");
assertThat(result).isEqualTo(new IpPort("127.0.0.1", 8080));
}
@Test
void ipPortToString() {
IpPortToStringConverter converter = new IpPortToStringConverter();
String result = converter.convert(new IpPort("127.0.0.1", 8080));
assertThat(result).isEqualTo("127.0.0.1:8080");
}
}
|
cs |
3. ConversionService
ConvsersionService에 Converter 등록
ConversionService 인터페이스는 기본적인 타입들의 변환을 지원하고, 사용자가 작성한 컨버터를 등록할 수 있게도 해준다. 어떤 변환 기능들을 지원하는지는 뒤에서 알아보고, 일단 위에서 작성했던 컨버터들을 등록하고 테스트해본다.
java/hello/typeconverter/converter/ConversionServiceTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public class ConversionServiceTest {
@Test
void conversionService() {
//등록
DefaultConversionService conversionService = new DefaultConversionService();
conversionService.addConverter(new StringToIntegerConverter());
conversionService.addConverter(new IntegerToStringConverter());
conversionService.addConverter(new IpPortToStringConverter());
conversionService.addConverter(new StringToIpPortConverter());
//사용
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");
}
}
|
cs |
테스트 결과
각 타입 컨버터들이 호출되고 테스트가 정상적으로 통과되는 것을 확인할 수 있다. 만약, StringToIntegerConverter를 ConversionService에 등록하지 않은채로 ConversionService에 변환 요청을 하면, StringToIntegerConverter는 호출되지 않지만, 그래도 변환은 되는 것을 확인할 수 있다. 이것은 SpringService가 기본 타입들의 변환은 지원한다는 것을 의미한다.
StringToIntegerConverter는 등록하지 않고 변환을 시도한 경우
ConversionService에 적용된 인터페이스 분리 원칙(ISP, Interface Segregation Principal0
스프링은 ISP를 지키며 설계되었다. 클라이언트가 자신이 사용하지 않는 메서드에는 의존하지 않도록 하는 것이다. DefaultConversionService -> GenericConversionSerivce -> ConfigurableConversionService에 들어가보면 ConversionService와 ConversionRegistry로 분리된 2개의 인터페이스에 의존하는 것을 볼 수 있다. ConversionService는 canConvert, convert와 같은 메서드가 있어서 실제 타입 변환을 할 수 있도록 해주는 인터페이스이고, ConverterRegistry는 클라이언트가 만든 Converter를 등록할 수 있는 registry 메서드가 포함되어 있다.
이렇게 관심사별로 인터페이스를 분리 설계(AOP)함으로써 클라이언트가 꼭 필요한 메서드만 사용(의존)하게 된다. 만약 String -> Integer로 타입 변환만을 원한다면 ConversionService의 convert 메서드만 사용하게 될테고, 새로운 Converter를 등록할 때는 ConverterRegistry만 사용하게 된다. 스프링은 대부분 이런 식으로 ISP를 지키면서 구조가 설계되어 있다는 것을 알아두자.
4. Converter 사용하기
스프링에 Converter 적용
스프링에 Converter를 적용해보자. WebMvcConfigurer에 기본적으로 있는 addFormatters 메서드를 Override 하면 된다.
java/hello/typeconverter/WebConfig.java
1
2
3
4
5
6
7
8
9
10
11
|
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToIpPortConverter());
registry.addConverter(new IpPortToStringConverter());
registry.addConverter(new StringToIntegerConverter());
registry.addConverter(new IntegerToStringConverter());
}
}
|
cs |
그리고 나서 이전에 작성했던 컨트롤러에 메서드를 추가한 후 실행해보면, 정상적으로 Converter가 호출되고 Converting이 완료되는 것을 확인할 수 있다.
java/hello/typeconverter/controller/HelloController.java
1
2
3
4
5
6
|
@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";
}
|
cs |
※ @RequestParam은 RequestParamMethodArgumentResolver의 ConversionService를 사용해서 타입 변환
핸들러에 도달 전, HTTP 요청의 정보들(세션, 파라미터 등)을 알맞게 처리해주는 ArgumentResolver에서 ConversionService를 호출하여 타입을 변환한다. 이것을 확인하고 싶다면, debugger로 역추적해나갈 수도 있다. convert 가 작동하는 부분에 debugging point를 걸어주고 debugger를 실행하면, 아래 Debugger 화면에서 ArgumentResolver가 호출되는 부분을 확인할 수 있다.
타임리프에 컨버터 적용
객체->String 변환
화면을 렌더링 하는 타임리프에 컨버터를 적용해본다. 화면을 렌더링 해야하므로 어떤 객체를 String으로 변환해야 한다. 일반적인 변수를 표현할 때는 그냥 중괄호 두개 {{ ... }} 를 써주면 자동으로 Converter가 적용된다. 결과화면을 보면, 컨트롤러에서 모델에 담길 때는 각 숫자, IpPort 객체로 담겼던 변수값들이, 중괄호 두 개 문법이 적용된 부분에서는 모두 String으로 변환되어 표시된 것을 확인할 수 있다.
resources/templates/converter-view.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<!DOCTYPE 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>
|
cs |
java/hello/typeconverter/controller/ConverterController.java
1
2
3
4
5
6
|
@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";
}
|
cs |
폼 객체로 객체->String, String->객체 변환
우선 @GetMapping("/converter/edit") 으로 접속하면, 새로운 IpPort 객체를 만들고 그것을 Form 객체에 담아서 ModelAttribute로 전달한다. 이때, converter-form.html 에서는 th:field와 th:value 2가지 값으로 form.ipPort 값을 표시하는데, 맨 아래 결과에서도 볼 수 있듯 중괄호 2개 {{ }} 문법을 쓰지 않으면 th:field는 Converter가 자동 적용되고, th:value는 Converter가 적용되지 않는다. (물론 중괄호 2개 문법 적용이 가능하며, 2개 적용 시 모두 Converter 적용됨)
그리고 @PostMapping("/converter/edit")으로 form 객체에 ipPort값을 담아서 @ModelAttribute로 전달해본다. 이 경우 ipPort 값은 String으로 전달되는데, 호출된 결과 페이지인 converter-view 페이지에서는 ipPort의 값이 IpPort 객체로 표시되는 것을 확인할 수 있다. 이것은 @ModelAttribute 작성 시 자동으로 Converter가 적용되기 때문이다.
resources/templates/converter-form.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<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>
</body>
</html>
|
cs |
java/hello/typeconverter/controller/ConverterController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
@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;
}
}
|
cs |
converter-form.html
converter-view.html
참조
1. 인프런_스프링 MVC 2편 - 백엔드 웹개발 핵심 기술_김영한 님 강의
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2
'Programming-[Backend] > Spring' 카테고리의 다른 글
[스프링 웹MVC-2] 20. 파일 업로드 (0) | 2021.09.21 |
---|---|
[스프링 웹MVC-2] 19. 타입 컨버터 - Formatter (0) | 2021.09.19 |
[스프링 웹MVC-2] 17. API 예외 처리 - 스프링 ExceptionResolver (0) | 2021.09.13 |
[스프링 웹MVC-2] 16. API 예외 처리 - 스프링부트 기본 오류 처리, HandlerExceptionResolver (0) | 2021.09.09 |
[스프링 웹MVC-2] 15. 예외 - 인터셉터와 스프링 부트의 오류 페이지 (0) | 2021.09.08 |