HelloController - 문자 타입을 숫자 타입으로 변경
@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("int value = " + intValue);
return "ok";
}
}
실행
http://localhost:8080/hello-v1?data=10
HTTP 요청 파라미터는 모두 문자로 처리된다. 따라서 요청 파라미터를 자바에서 다른 타입으로 변환하여 사용하려면, 숫자 타입으로 변환하는 것 같은 과정을 거쳐야 한다.
이번에는 스프링 MVC 가 제공하는 “@RequestParam” 을 사용해보자.
HelloController - helloV2
@GetMapping("/hello-v2")
public String helloV2(@RequestParam Integer data) {
System.out.println("data = " + data);
return "ok";
}
실행
http://localhost:8080/hello-v2?data=10
HTTP 쿼리 스트링으로 전달하는 data=10 부분에서 10은 숫자가 아닌 문자 10이다.
스프링이 제공하는 “@RequestParam” 을 사용하면, 이 문자 10 을 Integer 타입의 숫자 10으로 편리하게 받을 수 있다.
→ 어떻게??
스프링이 중간에서 타입을 변환해주었기 때문이다.
“@ModelAttribute” 예시
@GetMapping("/hello-my1")
public String helloMy1(@ModelAttribute UserData data) {
System.out.println("data = " + data.getData());
return "ok";
}
실행
http://localhost:8080/hello-my1?data=10
이는 컨버터 인터페이스 를 통해 구현되고, 이를 사용하여 새로운 컨버터를 만들수도 있다.
Converter Interface
package org.springframework.core.convert.converter;
public interface Converter<S, T> {
T convert(S source);
}
타입 컨버터란?
타입 컨버터를 사용하려먼 “org.springframework.core.convert.converter.Converter” 인터페이스를 구현하면 된다.
Integer ↔ String 예제.
StringToIntegerConverter
@Override
public Integer convert(String source) {
log.info("convert source = {}", source);
Integer integer = Integer.valueOf(source);
return integer;
}
IntegerToStringConverter
@Override
public String convert(Integer source) {
log.info("convert source = {}", source);
return String.valueOf(source);
}
String ↔ Object 예제.
@Getter
@EqualsAndHashCode
public class IpPort {
private String ip;
private int port;
public IpPort(String ip, int port) {
this.ip = ip;
this.port = 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();
}
}
@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);
}
}
왜? 타입 컨버터를 하나하나 사용하면서 사용할까??
사실 이렇게 사용하지는 않는다. 만든 타입 컨버터를 직접 호출해서 사용하는 방식을 위해 만드는것이 아니다!
그러면? → 타입 컨버터를 등록하고 관리하면서 편리하게 변환 기능을 제공하는 역할을 할 수 있다!
이렇게 타입 컨버터를 하나하나 직접 찾아서 타입 변환에 사용하는 것은 매우 불편하다. 그래서 스프링은 개별 컨버터를 모아두고 그것들을 묶어서 편리하게 사용할 수 있는 기능을 제공하는데, 이것이 바로 컨버전 서비스
“ConversionService” 이다.
→ 확인하는 기능과, 컨버팅 기능을 제공한다.
ConversionServiceTest - 컨버전 서비스 테스트 코드
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());
//문자 10 -> 숫자 10
assertThat(conversionService.convert("10", Integer.class)).isEqualTo(10);
//숫자 10 -> 문자 10
assertThat(conversionService.convert(10, String.class)).isEqualTo("10");
// 문자열 127.0.0.1:8080 -> IpPort
assertThat(conversionService.convert("127.0.0.1:8080", IpPort.class)).isEqualTo(new IpPort("127.0.0.1", 8080));
//IpPort -> 문자열 127.0.0.1:8080
assertThat(conversionService.convert(new IpPort("127.0.0.1", 8080), String.class)).isEqualTo("127.0.0.1:8080");
}
}
→ conversionService 에 우리가 만든 Converter 들을 추가하고, convert 를 사용할 수 있다.
convert(”변환 하고자 하는 객체”, 변환하고 싶은 반환 타입)
타입 변환을 원하는 사용자는 컨버전 서비스 인터페이스에만 의존하면 된다.
단, 등록하는 부분과 사용하는 부분을 분리하고, 스프링 빈과 같이 의존관계 주입을 사용해야 한다.
인터페이스 분리원칙 - SOLID 의 ISP (Interface Segregation Principle)
ConversionService ↔ ConverterRegistry , 사용과 등록을 분리한다.
따라서 스프링은 내부에서 ConversionService를 사용해서 타입을 변환한다.
웹 애플리케이션에 Converter 를 적용하자.
@GetMapping("/hello-v2")
public String helloV2(@RequestParam Integer data) {
System.out.println("data = " + data);
return "ok";
}
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());
}
}
실행 결과
http://localhost:8080/hello-v2?data=10
이제 위 URL로 요청한다면, 우리가 만든 StringToIntegerConverter가 사용된다.
그렇지만, 위의 registry 등록을 모두 주석 처리 하더라도 기본 컨버터를 제공하기 때문에 실행은 된다.
하지만, 추가한 직접 만든 컨버터는 기본 컨버터보다 높은 우선순위를 가지기 때문에, 직접 추가한 컨버터가 사용된다.
그렇다면, 직접 만든 IpPort 변환 컨버터를 사용해보자!
Controller 추가
@GetMapping("/ip-port")
public String ipPort(@RequestParam IpPort ipPort) {
System.out.println("ip = " + ipPort.getIp());
System.out.println("port = " + ipPort.getPort());
return "ok";
}
실행 결과
http://localhost:8080/ip-port?ipPort=127.0.0.1:8080
2024-11-04T16:42:05.179+09:00 INFO 79581 --- [typeconverter] [nio-8080-exec-5] h.t.converter.StringToIpPortConverter : convert source = 127.0.0.1:8080
ip = 127.0.0.1
port = 8080
→ 우리가 만든 StringToIpPortConverter 가 잘 사용되었다.
converter-view.html
<!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>
→ ${…} : 컨버터 적용 x
→ ${{…}} : 컨버터 적용 o
실행 결과
${number}: 10000
${{number}}: 10000
${ipPort}: hello.typeconverter.type.IpPort@59cb0946
${{ipPort}}: 127.0.0.1:8080
${number}: 10000 : 컨버팅 하지 않아서 숫자 10000${{number}}: 10000 : 컨버팅 하여 문자 10000${ipPort}: hello.typeconverter.type.IpPort@59cb0946 : 컨버팅 하지 않아서 객체 주소${{ipPort}}: 127.0.0.1:8080 : 컨버팅 하여 String 타입 변환 시 IpPortToStringConverter 적용됨ConverterController 추가
@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.html
<!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>
실행 결과
http://localhost:8080/converter/edit


단! 이 과정에서는 JSON을 객체로 변환하는 과정이 아니다!
이때는 Json Parsing Library 인 Jackson → ObjectMapper 등이 사용되는 것이다.
여기서 Json 을 넘기고 파싱하는게 아니다. 헷갈리지 말자!
웹 애플리케이션에서 객체를 문자로, 문자를 객체로 변환하는 예시.
Integer → String 출력 시점에 숫자 1000 → 문자 1,000
또는 문자 1,000 → 숫자 1000
날짜 객체를 문자인 “2024-11-04 17:14:30” 와 같이 출력하거나 그 반대의 상황.
→ Locale 숫자와 날짜 표현은 현지화 정보가 사용될 수 있다.
Converter? Formatter?
Converter → 범용
Formatter → 문자에 특화되어 있고 + 현지화 - Locale 이다.
즉, Converter의 특별한 버전이라고 생각하면 된다.
목표 : “1,000” → 1000 & 1000 → “1,000”
MyNumberFormatter
@Slf4j
public class MyNumberFormatter implements Formatter<Number>{
@Override
public Number parse(String text, Locale locale) throws ParseException {
log.info("text={}, locale={}", text, locale);
//"1,000" -> 1000
NumberFormat format = NumberFormat.getInstance(locale);
return format.parse(text);
}
@Override
public String print(Number object, Locale locale) {
log.info("object={}, locale={}", object, locale);
NumberFormat instance = NumberFormat.getInstance(locale);
return instance.format(object);
}
}
TestCode
class MyNumberFormatterTest {
MyNumberFormatter formatter = new MyNumberFormatter();
@Test
void parse() throws ParseException {
Number result = formatter.parse("1,000", Locale.KOREA);
assertThat(result).isEqualTo(1000L);
}
@Test
void print() {
String result = formatter.print(1000, Locale.KOREA);
assertThat(result).isEqualTo("1,000");
}
}
→ 이제 이것을 컨버전 서비스에 포멧터를 등록해서 쓰는법을 알아보자!
DefaultFormattingConversionService 는 FormattingConversionService 에 기본적인 통화, 숫자 관련 몇가지 기본 포맷터를 추가해서 제공한다.
Test code
public class FormattingConversionServiceTest {
@Test
void formattingConversionSerivice() {
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));
//포멧터 사용
String convert = conversionService.convert(1000, String.class);
assertThat(convert).isEqualTo("1,000");
}
}
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());
}
}
🚨 주의 : StringToIntegerConverter, IntegerToStringConverter 를 주석처리 해야 한다.
왜? MyNumberFormatter 도 숫자 → 문자, 문자 → 숫자로 변경하기 때문에 둘의 기능이 겹친다.
우선순위는 컨버터가 높다, 포맷터가 적용되지 않고 컨버터가 적용된다.
실행 결과
http://localhost:8080/converter-view
${number}: 10000
${{number}}: 10,000
${ipPort}: hello.typeconverter.type.IpPort@59cb0946
${{ipPort}}: 127.0.0.1:8080
스프링은 자바에서 기본으로 제공하는 타입에 대해 수 많은 포맷터를 기본으로 제공한다.
유용한 포맷터 두가지를 알아보자.
@NumberFormat” : 숫자 관련 형식 지정 포맷터 사용 - NumberFormatAnnotationFormatterFactory@DateTimeFormat” : 날짜 관련 형식 지정 포맷터 사용 - Jsr310DateTimeFormatAnnotationFormatterFactoryFormatterController
@Slf4j
@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);
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;
}
}
애노테이션들로 Formatter 를 지정하여 사용할 수 있다!