Spring ํ์ ์ปจ๋ฒํฐ (Converter, Formatter)
by JiwonDev# ํ์ ์ปจ๋ฒํฐ์ ํ์์ฑ
HTTP์ ์์ฒญ ํ๋ผ๋ฉํ๋ ๋ชจ๋ ๋ฌธ์์ด๋ก ์ฒ๋ฆฌ๋๋ค. ๋ง์ฝ ์ซ์๋ฅผ ์ฌ์ฉํ๊ณ ์ถ๋ค๋ฉด ์๋์ ๊ฐ์ด ํ์ ์ ๋ณํํด์ผํ๋ค.
@GetMapping("/hello-v1")
public String helloV1(HttpServletRequest request) {
String data = request.getParameter("data");//๋ฌธ์ ํ์
์กฐํ
Integer intValue = Integer.valueOf(data); //์ซ์ ํ์
์ผ๋ก ๋ณ๊ฒฝ
return "ok";
}
ํ์ง๋ง ์คํ๋ง์์๋ @์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํด์ HTTP ํ๋ผ๋ฉํ๋ฅผ ์ํ๋ ํํ๋ก ๋ณํํ ์ ์๋ค.
@GetMapping("/hello-v2")
public String helloV2(@RequestParam Integer data) {
System.out.println("data = " + data);
return "ok";
}
@PostMapping("/user/add")
public String search(@ModelAttribute User user, BindingResult result) {
if (result.hasError()) {
return "/user/add"; // add.html์ ๋ชจ๋ธ(User)๊ณผ BindingResult๊ฐ ์ ๋ฌ๋๋ค.
}
userService.add(user);
return "/home";
}
@RequestParm, @ModelAttribute, @PathVariable ๋ถํฐ ์์ํด์, "/home"์ ๋ทฐ ๊ฐ์ฒด๋ก ๋ณํํ๊ฑฐ๋ ๊ฐ์ฒด ์ ๋ณด๋ฅผ JSON์ผ๋ก ์ ์กํ๋ ํ ์๋ ์๊ณ ์ข ๋ ์ถ์์ ์ธ ์ ๋ณด (IP, Port)๋ฅผ ๋ณํํ๋ ๋ฑ ์ ๋ง ์๋ง์ ๊ณณ์์ ํ์ ๋ณํ์ด ๋ฐ์ํ๋ค.
์ด๋ ์คํ๋ง์์ ๋ค์ํ ํ์ ์ปจ๋ฒํฐ๋ฅผ ๊ฐ์ง๊ณ ์๊ณ ์ด๋ฅผ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ด๋ค. ๋ง์ฝ ๊ฐ๋ฐ์๊ฐ ์๋ก์ด ํ์ ์ ๋ง๋ค์ด์ Converter๋ฅผ ๊ตฌํํ๊ณ ์ถ๋ค๋ฉด ์ด๋ป๊ฒ ์ฌ์ฉํด์ผ ํ ๊น?
์ฐธ๊ณ ๋ก ์คํ๋ง3.0 ์ด์ ์๋ ์๋ฐ์ PropertyEditor ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํด ํ์ ๋ณํ์ ํ์๋ค. ํ์ง๋ง ์ด๋ ๋์์ฑ ๋ฌธ์ ๊ฐ ์์ด ํ์ ๋ณํํ ๋ ๋ง๋ค ๊ฐ์ฒด๋ฅผ ๊ณ์ํด์ ์์ฑํด์ค์ผ ํ๋ ๋ฌธ์ ์ ์ด ์์๋ค.
# ์คํ๋ง์ Converter ์ธํฐํ์ด์ค
- ์คํ๋ง์์๋ ํ์ ์ปจ๋ฒํฐ๋ฅผ ๋ง๋ค๊ธฐ์ํด ์๋์ ๊ฐ์ ์ธํฐํ์ด์ค๋ฅผ ์ ๊ณตํด์ค๋ค.
package org.springframework.core.convert.converter;
public interface Converter<S, T> {
T convert(S source);
}
- ์ด ์ธํฐํ์ด์ค๋ฅผ ์ด์ฉํด์ ์๋์ ๊ฐ์ด ๊ฐ๋จํ๊ฒ ์ปจ๋ฒํฐ๋ฅผ ๊ตฌํํ ์ ์๋ค.
public class IntegerToStringConverter implements Converter<Integer, String> {
// ๋ด๊ฐ ๋ง๋ Integer -> String ์ปจ๋ฒํฐ
@Override
public String convert(Integer source) {
return String.valueOf(source);
}
}
public class StringToIntegerConverter implements Converter<String, Integer> {
// ๋ง์ฝ ๋ฐ๋๋ก ์ ํํ๋ ๊ธฐ๋ฅ๋ ํ์ํ๋ค๋ฉด ์ด๋ ๊ฒ ํ๋ ๋ ๋ง๋ค์ด ์ฃผ๋ฉด ๋๋ค.
@Override
public Integer convert(String source) {
return Integer.valueOf(source);
}
}
- ์ปจ๋ฒํฐ๋ฅผ ๋ง๋ค์๋ค๋ฉด ์ ์๋ํ๋์ง ๋ฐ๋์ ํ ์คํธํด๋ด์ผํ๋ค.
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");
}
}
# IpPort ๊ฐ์ฒด ์ปจ๋ฒํฐ ๋ง๋ค๊ธฐ
String <-> Integer ์ปจ๋ฒํฐ๋ ๊ตณ์ด ๋ง๋ค์ด์ผํ๋..? ์๋ฌธ์ด ์๊ธธ ์ ์๋ค. ์ด๋ฒ์๋ ์ข ๋ ๋ณต์กํ ์์ ๋ฅผ ๋ง๋ค์ด๋ณด์.
@Getter
@EqualsAndHashCode // ๋น๊ต๋ฅผ ์ํ lombok ์ฝ๋ ์์ฑ
public class IpPort {
private String ip;
private int port;
public IpPort(String ip, int port) {
this.ip = ip;
this.port = port;
}
}
- ์์ IpPort ๊ฐ์ฒด๋ฅผ String์ผ๋ก ๋ณํํ๋ ์ปจ๋ฒํฐ๋ฅผ ๊ตฌํํ๋ฉด ์๋์ ๊ฐ๋ค.
@Slf4j
public class IpPortToStringConverter implements Converter<IpPort, String> {
@Override
public String convert(IpPort source) {
//IpPort ๊ฐ์ฒด -> "127.0.0.1:8080"
return source.getIp() + ":" + source.getPort();
}
}
@Slf4j
public class StringToIpPortConverter implements Converter<String, IpPort> {
@Override
public IpPort convert(String source) {
//"127.0.0.1:8080" -> IpPort ๊ฐ์ฒด
String[] split = source.split(":");
String ip = split[0];
int port = Integer.parseInt(split[1]);
return new IpPort(ip, port);
}
}
- ๋ง์ฐฌ๊ฐ์ง๋ก ์ ์๋ํ๋์ง ํ
์คํธํด๋ณด์.
public class ConverterTest { @Test void stringToIpPort() { 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"); } @Test void ipPortToString() { 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)); } }
# ์ปจ๋ฒํฐ๋ฅผ ๋ง๋ค๊ณ ํ ์คํธํ๋ ๊ณผ์ ์ ๋ณต์กํ๋ค.
๋งค๋ฒ ์ปจ๋ฒํฐ๋ฅผ ๊ตฌํํ๊ณ , ํ ์คํธํ๋ ๊ณผ์ ์ ์ด์ฒ๋ผ ๋ณต์กํ๋ค. ๋ํ ๋ง๋ค์ด๋ ์ปจ๋ฒํฐ๋ ํ๋ก์ ํธ ์ ์ฒด์์ ์ฐ์ด๊ธฐ์ ๋์์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์๋ค. ์ปจ๋ฒํฐ๋ฅผ ๋ง๋ค๊ณ , ํ ์คํธํ๊ณ ์ฌ์ฉํ๋๊ฑธ ํ๋ฒ์ ๊ด๋ฆฌํ ์๋ ์์๊น?
โก ์คํ๋ง์ ConversionService
์ด๋ฏธ ์๊ณ ์๊ฒ ์ง๋ง, ์คํ๋ง์ ConversionService์์๋ ์๋น์ค์์ ์์ฃผ ์ฌ์ฉํ๋ ํ์ ์ปจ๋ฒํฐ๋ค์ด ์ด๋ฏธ ๋ฑ๋ก๋์ด์๋ค.
# ConversionService ์ธํฐํ์ด์ค
์คํ๋ง์์๋ ์ปจ๋ฒํฐ๋ฅผ ๋ฑ๋กํ๊ณ ์ฝ๊ฒ ์ฌ์ฉํ ์ ์๋๋ก ConversionService ์ธํฐํ์ด์ค๋ฅผ ์ ๊ณตํ๋ค. ์คํ๋ง์์ ๊ธฐ๋ณธ์ผ๋ก ์ ๊ณตํด์ฃผ๋ ์ปจ๋ฒํฐ๋ค์ด ๋ง์ ์ด์ง๊ฐํ๋ฉด ์ง์ ๋ง๋ค์ผ์ ์์ง๋ง, ๋ง๋ค๊ณ ์ถ๋ค๋ฉด ์ด ๋งํฌ๋ฅผ ์ฐธ์กฐํ์.
- ConversionService๋ ์ฌ์ฉ์ ์ด์ ์ ๋ง์ถ ์ธํฐํ์ด์ค์ด๋ค. ํด๋ผ์ด์ธํธ๋ ์ปจ๋ฒํฐ ๊ตฌํ์ฒด์ ์์กดํ์ง ์๋๋ค.
public interface ConversionService {
boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType);
boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
<T> T convert(@Nullable Object source, Class<T> targetType);
Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType,
TypeDescriptor targetType);
}
- ConverterRegistry๋ ๋ฑ๋ก์ ์ด์ ์ ๋ง์ถ ์ธํฐํ์ด์ค์ด๋ค. ์ปจ๋ฒํฐ ๊ตฌํ์ฒด๋ ๊ตฌ์ฒด์ ์ธ ์ปจ๋ฒํฐ๋ฅผ ์์ฑํ๋ค.
public interface ConverterRegistry {
void addConverter(Converter<?, ?> converter);
<S, T> void addConverter(Class<S> sourceType, Class<T> targetType,
Converter<? super S, ? extends T> converter);
void addConverter(GenericConverter converter);
void addConverterFactory(ConverterFactory<?, ?> factory);
void removeConvertible(Class<?> sourceType, Class<?> targetType);
}
- ์ฌ์ฉ๋ฒ์ ๊ฐ๋จํ๋ค. ์ธํฐ์
ํฐ์ฒ๋ผ @Configuration๋ฅผ ํตํด ์คํ๋ง ConversionService์ ๋ฑ๋กํ๊ณ , ์ฌ์ฉํ๋ฉด ๋๋ค.
โก ์คํ๋ง ๋ด์ฅ ์ปจ๋ฒํฐ์ ๋๊ฐ์ ๋์(str->Integer)์ ํ๋ค๋ฉด, ์๋์ ๊ฐ์ด ์ง์ ์ถ๊ฐํ ์ปจ๋ฒํฐ๋ฅผ ๋จผ์ ์ฌ์ฉํ๋ค.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
// ์คํ๋ง ๋ด๋ถ์ ์๋ ConversionService์ ๋ฑ๋ก๋๋ค.
// ConversionService conversionService = new DefaultConversionService();
// ์ง์ ์ถ๊ฐํ ์ปจ๋ฒํฐ๋, ์คํ๋ง ๊ธฐ๋ณธ ์ปจ๋ฒํฐ๋ณด๋ค ๋์ ์ฐ์ ์์๋ฅผ ๊ฐ์ง๋ค.
registry.addConverter(new StringToIntegerConverter());
registry.addConverter(new IntegerToStringConverter());
registry.addConverter(new StringToIpPortConverter());
registry.addConverter(new IpPortToStringConverter());
}
}
- ์ด์ ์คํ๋ง์์ ์ฌ์ฉํ๋ @RequestPararm๋ฑ์์ ์ฐ๋ฆฌ๊ฐ ๋ง๋ ์ปจ๋ฒํฐ๊ฐ ์ถ๊ฐ๋ก ๋์ํ๋ค.
โก ์ ํํ๋ ์คํ๋ง MVC์ ArumentResolver๊ฐ ๋ฑ๋ก๋ ConversionService ์ธํฐํ์ด์ค๋ฅผ ์ฌ์ฉํด ํ์ ์ ๋ณํํ๋ค.
@GetMapping("/hello-v2") // registry.addConverter(new StringToIntegerConverter());
public String helloV2(@RequestParam Integer data) {
System.out.println("data = " + data);
return "ok";
}
@GetMapping("/ip-port") // registry.addConverter(new StringToIpPortConverter());
public String ipPort(@RequestParam IpPort ipPort) {
System.out.println("ipPort IP = " + ipPort.getIp());
System.out.println("ipPort PORT = " + ipPort.getPort());
return "ok";
}
# ํ์๋ฆฌํ(View Template)์ ์ปจ๋ฒํฐ ์ฌ์ฉํ๊ธฐ
- ํ์๋ฆฌํ๋ ๋ณํํ์ง ์์ ๋ชจ๋ธ ๊ฐ์ฒด๋ฅผ ์ ๋ฌํ๋๋ผ๋ ํ์๋ฆฌํ ์ฝ๋์์ ์คํ๋ง ์ปจ๋ฒํฐ๋ฅผ ์ ์ฉํ ์๋ ์๋ค.
@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";
}
}
- ์๋์ ๊ฐ์ด ํ์๋ฆฌํ ๋ณ์ ํํ์์ ${{...}} ๋ฅผ ์ฌ์ฉํ๋ฉด ์ปจ๋ฒํฐ๋ฅผ ์ฌ์ฉํด์ ๋ ๋๋งํ๋ค.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
...
</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><!-- ๊ทธ๋ฅ str(ipPort) ์ฒ๋ฆฌ -->
<li>${{ipPort}}: <span th:text="${{ipPort}}"></span></li><!-- ์คํ๋ง ์ปจ๋ฒํฐ ์ฌ์ฉ -->
</ul>
</body>
</html>
- ๋ํ ํ์๋ฆฌํ์ th:field๋ ์๋์ผ๋ก ์คํ๋ง ์ปจ๋ฒํฐ๋ฅผ ์ ์ฉํด์ HTTP ์์ฒญ๋ฉ์์ง๋ฅผ ๋ณด๋ธ๋ค.
โก ์ปจ๋ฒํฐ๋ฅผ ์ฌ์ฉํ์ง์๊ณ ๋ฌธ์์ด ๊ทธ๋๋ก ์ ์กํ๋ ค๋ฉด th:value๋ฅผ ์ฌ์ฉํ๋ฉด ๋๋ค.
<form th:object="${form}" th:method="post">
th:field <input type="text" th:field="*{ipPort}"><br/> <!-- ${{ipPort}} -->
th:value <input type="text" th:value="*{ipPort}"><br/> <!-- ${ipPort} -->
<input type="submit"/>
</form>
- ์ฐ๋ฆฌ๋ ๋น์ฐํ๋ค๊ณ ์๊ฐํ๊ณ @ModelAttribute ๋ฅผ ์ด์ฉํด ๋ทฐ์์ ๊ฐ์ฒด๋ฅผ ๋ฐ์์์ง๋ง, ์ค์ ๋ก๋ HTTP์์ ์ ์ก๋ ๋ฌธ์์ด์ ๊ฐ์ฒด๋ก ์ปจ๋ฒํฐํ๋ ๊ณผ์ ์ด ์๋ค๋ ๊ฑธ ์ธ์งํ์.
@Data
class IpPortForm {
private IpPort ipPort;
public IpPortForm(IpPort ipPort) {
this.ipPort = ipPort;
}
}
...
@GetMapping("/converter/edit")
public String converterForm(Model model) {
IpPort ipPort = new IpPort("127.0.0.1", 8080);
IpPortForm form = new IpPortForm(ipPort); // Form ๊ฐ์ฒด๋ฅผ ๋ชจ๋ธ๋ก ์ ๋ฌ
model.addAttribute("form", form);
return "converter-form";
}
@PostMapping("/converter/edit")
public String converterEdit(@ModelAttribute IpPortForm form, Model model) {
// @ModelAttribute ๋ฐ์๋ณด๋ฉด IpPort์ ์ปจ๋ฒํฐ๊ฐ ์ ์ฉ๋์ด์์.
// ๋ง์ฝ th:value๋ก ์ ์กํ๋ฉด ๋จ์ .toString() ๋ฌธ์์ด์ด ๋ค์ด๊ฐ์๋ค.
IpPort ipPort = form.getIpPort();
model.addAttribute("ipPort", ipPort);
return "converter-view";
}
# Formatter ํฌ๋งทํฐ
์์์ ๋ฐฐ์ด Converter๋ ํ์ ์ ์ ํ์์ด ๋ณํ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค. ํ์ง๋ง HTTP ์์ฒญ์ ๋๋ถ๋ถ์ String์ผ๋ก ๋ณํํ๋ ๊ฒฝ์ฐ๊ฐ ๋๋ถ๋ถ์ด๋ค. ๊ทธ๋์ ์คํ๋ง์์๋ ๋ฌธ์๋ฅผ ์ฒ๋ฆฌ์ ํนํ๋ ์ปจ๋ฒํฐ์ธ Fomatter ์ธํฐํ์ด์ค๋ฅผ ์ ๊ณตํ๋ค.
โก ์ฐธ๊ณ ๋ก ๋์ผํ ๋์์ ํ๋ Converter์ Formatter๊ฐ ์๋ค๋ฉด, ์ปจ๋ฒํฐ๊ฐ ๋จผ์ ์ ์ฉ๋๋ค. ์ฐ์ ์์๊ฐ ๋ ๋๋ค.
Formatter๋ ํน๋ณํ String Converter๋ผ๊ณ ์๊ฐํ๋ฉด ๋๋ค. ๋ฌธ์๋ฅผ ์ถ๋ ฅํ ๋ Locale ์ ๋ณด๋ฅผ ์ด์ฉํ์ฌ ๊ตญ์ ํํ๊ฑฐ๋ ์ํ๋ ๋ชจ์์ ๋ฌธ์์ด๋ก ํฌ๋งทํ ํ ์ ์๋ ์ถ๊ฐ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค.
โก ๋ณด๋ฉด ์ธํฐํ์ด์ค๋ฅผ ์ ๊ธฐํ๊ฒ ์์ํ๊ณ ์๋๋ฐ, ์ ์ด๋ฌ๋์ง ๊ถ๊ธํ๋ค๋ฉด ๊ฐ์ฒด์งํฅ์ ISP ์์น์ ์ฐพ์๋ณด์.
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> {
String print(T object, Locale locale);
T parse(String text, Locale locale) throws ParseException;
}
@Slf4j
public class MyNumberFormatter implements Formatter<Number> {
@Override
public Number parse(String text, Locale locale) throws ParseException {
//"1,000" -> 1000
NumberFormat format = NumberFormat.getInstance(locale);
return format.parse(text);
}
@Override
public String print(Number object, Locale locale) {
// 1000 -> "1,000"
return NumberFormat.getInstance(locale).format(object);
}
}
# ๋ด๊ฐ ๋ง๋ Formatter๋ฅผ ์คํ๋ง ConversionService์ ๋ฑ๋กํ๊ธฐ
- ์ปจ๋ฒํฐ๋ ํฌ๋งทํฐ์ ์ฌ์ฉ๋ฒ์ ๋๊ฐ๋ค. ์ ์ด์ ๋ฉ์๋ ์ด๋ฆ๋ addFormatters ์๊ธฐ๋ ํ๋ค.
โก ์คํ๋ง๋ถํธ์์๋ DefaultConversionService๋ฅผ ์์๋ฐ์ WebConversionService ๊ตฌํ์ฒด๋ฅผ ์ฌ์ฉํ๋ค.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
// ์์์ ๋ฑ๋กํ๋ ์ปจ๋ฒํฐ
// ConversionService conversionService = new WebConversionService();
registry.addConverter(new StringToIpPortConverter());
registry.addConverter(new IpPortToStringConverter());
//์ถ๊ฐ. ์คํ๋ง์์๋ ์ด๋ํฐ ํจํด๋ฑ์ ์ด์ฉํด Formatter๋ฅผ Converter์ฒ๋ผ ๋์ํ๋๋ก ๋ง๋ ๋ค.
registry.addFormatter(new MyNumberFormatter());
}
}
- ์ฐธ๊ณ ๋ก ConversionService๋ ์คํ๋ง์ ์์กด์ ์ธ ๊ฐ์ฒด๊ฐ ์๋๋ค. ์๋์ ๊ฐ์ด ๋ฐ๋ก ์ฌ์ฉํ ์๋ ์๋ค.
public class FormattingConversionServiceTest {
@Test
void formattingConversionService() {
// ConversionService ๊ฐ์ฒด ์์ฑ
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);
}
}
# Formatter๋ฅผ ์ค์ ์๋น์ค์ ์ฌ์ฉํด๋ณด๊ธฐ
์ปจ๋ฒํฐ๋ ๋ค๋ฅธ ๋๊ฐ์ด ์ฌ์ฉํ๋ฉด ๋๋ค. ๋ฑ๋ก์ ๋ฌ๋ผ๋ ์ฌ์ฉํ ๋๋ ๊ฐ์ ์ธํฐํ์ด์ค๋ฅผ ์ฌ์ฉํ๋ค.
@Slf4j
public class MyNumberFormatter implements Formatter<Number> {
@Override
public Number parse(String text, Locale locale) throws ParseException {
//"1,000" -> 1000
NumberFormat format = NumberFormat.getInstance(locale);
return format.parse(text);
}
@Override
public String print(Number object, Locale locale) {
// 1000 -> "1,000"
return NumberFormat.getInstance(locale).format(object);
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(new MyNumberFormatter());
}
}
<!-- formatter-view.html -->
<ul> <!-- ์๋ฒ โก ํด๋ผ์ด์ธํธ -->
<li>${form.number}: <span th:text="${form.number}" ></span></li>
<li>${{form.number}}: <span th:text="${{form.number}}" ></span></li> <!-- ํฌ๋งทํฐ ์ฌ์ฉ -->
<li>${form.localDateTime}: <span th:text="${form.localDateTime}" ></span></li>
<li>${{form.localDateTime}}: <span th:text="${{form.localDateTime}}" ></span></li> <!-- ํฌ๋งทํฐ ์ฌ์ฉ -->
</ul>
<!-- formatter-form.html -->
<form th:method="post" th:object="${form}">
<!-- ํด๋ผ์ด์ธํธ โก ์๋ฒ -->
number <input th:field="*{number}" type="text"><br/> <!-- th:field ๋ ์ปจ๋ฒํฐ ์ ์ฉ -->
localDateTime <input th:field="*{localDateTime}" type="text"><br/>
<input type="submit"/>
</form>
# ์คํ๋ง ์ด๋ ธํ ์ด์ ํฌ๋งทํฐ ํ์ฉ(@NumberFormat, @DateTimeFormat)
์คํ๋ง์ ์๋ ๊ธฐ๋ณธ ํฌ๋งทํฐ๋ค์ ์ฌ์ฉํ๋ค๊ฐ ๋๋ง์ ํฌ๋งทํฐ๊ฐ ํ์ํ๋ค๋ฉด ์ด๋ ๊ฒ ๋ง๋ค์ด์ ๋ฑ๋กํ๋ฉด ๋๋ค. ํ์ง๋ง ์๊ฐํด๋ณด๋ฉด ๋ ์ง, ๋ฌธ์์ ๊ฐ๋จํ ํฌ๋งทํ ์ ์ถ๊ฐํ ๋๋ ๋งค๋ฒ ์ด๋ฐ ๊ณผ์ ์ ๊ฑฐ์ณ ๊ฐ์ฒด๋ฅผ ์์ฑํ๊ธฐ์๋ ๋๋ฌด ๋ฒ๊ฑฐ๋กญ๋ค.
โก ์คํ๋ง์์๋ @XxxFormat(pattern="...")์ผ๋ก ๊ธฐ๋ณธ ํฌ๋งทํฐ๋ฅผ ๋ฑ๋กํ ์ ์๊ฒ ๊ตฌํํด๋์๋ค.
์คํ๋ง์์ ์ ๊ณตํ๋ ๊ธฐ๋ณธ ํฌ๋งทํฐ
@Data
class Form { // ๋ชจ๋ธ์ผ๋ก ์ฌ์ฉํ๋ ๊ฐ์ฒด์ ์ด๋ ๊ฒ Formatter๋ฅผ ์ง์ ํด์ค ์ ์๋ค.
@NumberFormat(pattern = "###,###") // "100,000" -> 100000
private Integer number;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") // "2021-08-31 ..." -> Date ๊ฐ์ฒด
private LocalDateTime localDateTime;
}
@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";
}
}
# ์ฃผ์์ฌํญ (HttpMessageConverter)
@ResponceBody์ ์ฌ์ฉ๋๋ ๋ฉ์์ง ์ปจ๋ฒํฐ(HttpMessageConverter)์ Conversion Service์๋ ์ ํ ์๊ด์๋ค.
์ด ๋์ ๋ณ๊ฐ์ ๊ฒ์ผ๋ก ๋ฉ์์ง ์ปจ๋ฒํฐ์ ๊ฒฝ์ฐ์๋ ์ธ๋ถ๋ผ์ด๋ธ๋ฌ๋ฆฌ(Jackson)๋ก ๋์ํ๋๊ฑฐ์ง ์คํ๋ง ์์ฒด์ ์ปจ๋ฒ์ ์๋น์ค์์ Converter, Formatter๋ฅผ ์ฌ์ฉํ๋๊ฒ ์๋๋ค. ์ฆ ๋ณ๊ฒฝํ๊ณ ์ถ๋ค๋ฉด ์คํ๋ง @Configuration์ ์ปจ๋ฒํฐ๋ฅผ ์ถ๊ฐํ๋๊ฒ ์๋๋ผ ์ธ๋ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๊ด๋ จ ์ค์ ์ ๋ฐ๊ฟ์ผํ๋ค.
์คํ๋ง Conversion Service๋ @ModelAttribute, @RequestParam, @PathVariable ๋ฑ์ ์ ์ฉ๋๋ค.
# @ResponseBody๋ฅผ ์ปค์คํ
ํ๋ ๋ฐฉ๋ฒ
๋ฌผ๋ก @ReponseBody์ ์ฌ์ฉ๋๋ HttpMessageConverter์ ๊ตฌํ์ฒด๋ฅผ ๋ณ๊ฒฝํ๋ ๋ฐฉ๋ฒ๋ ์๋ค.
๋ค๋ง ๊ธฐ๋ณธ ๊ตฌํ์ฒด(Jackson)์ ๊ตณ์ด ๋ณ๊ฒฝํด์ ์ฌ์ฉํ๋ฉด ๋ฒ๊ฑฐ๋กญ๊ธฐ๋ํ๊ณ , ๊ตณ์ด ๊ทธ๋ ๊ฒ๊น์ง ํ ์ด์ ๋ ์๋ค.
Rest API ์๋ฒ๋ฅผ ๊ตฌํํ ๋, @ResponseBody ์ ์ปจ๋ฒํฐ๋ฅผ ๋ณ๊ฒฝํ๊ณ ์ถ๋ค๋ฉด ์คํ๋ง ๊ธฐ๋ณธ ๊ตฌํ์ฒด์ธ Jackson์์ ์ ๊ณตํด์ฃผ๋ ์ด๋ ธํ ์ด์ ์ผ๋ก ํด๊ฒฐํ์.
- ๋ฐ์ดํธ ๋ฐ์ดํฐ -> ByteArrayHttpMessageConverter
- ๋ฌธ์์ด ๋ฐ์ดํฐ -> StringHttpMessageConverter
- ๊ฐ์ฒด, HashMap -> MappingJackson2HttpMessageConveter (๋ด๋ถ์ ์ผ๋ก Jackson ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ฒ๋ฆฌ๋ฅผ ์์ํ๋ค.)
import com.fasterxml.jackson.annotation.*;
์๋ฅผ ๋ค์ด, Request์ ๋ด๊ธด Json Body์ ์ด๋ฆ ๊ท์น์ด ๋ค๋ฅด๋ค๋ฉด
@JsonProperty๋ฅผ ์ด์ฉํ์ฌ ๋ฐ๊ฟ ์ ์๋ค. ๋ง์ฝ ์๊ฐ๊ณผ ๊ฐ์ด ํฌ๋งทํ ๋ ๋ฐ์ดํฐ๋ @JsonFormat์ ์ฌ์ฉํ๋ฉด ๋๋ค.
@RestController
@RequestMapping("/api")
public class StudentController {
public void post(@RequestBody Student student) {
System.out.println(student);
}
}
@Data
public class Student {
@JsonProperty("my_name")
private String myName;
@JsonProperty("my_age")
private String myAge;
@JsonProperty("my_country")
private String myCountry;
@JsonFormat(
shape = JsonFormat.Shape.STRING,
pattern = "dd-MM-yyyy hh:mm:ss")
public Date eventDate;
}
๋ง์ฝ ํ๋์ฉ ์ง์ ํ๋๊ฒ ๊ท์ฐฎ๋ค๋ฉด, @JsonNaming์ ์ด์ฉํ์ฌ ํฌ๋งทํ ์ ๋ฐ๊ฟ ์ ์๋ค. ( my_fisrt_age โก MyFirstAge)
@Data
@JsonNaming(value = PropertyNamingStrategy.SnakeCaseStrategy.class)
public class Student {
private String myName; // my_name ๊ฐ ์ฌ๊ธฐ์ ์ ์ฅ๋จ
private String myAge; // my_age ๊ฐ ์ฌ๊ธฐ์ ์ ์ฅ๋จ
private String myCountry; // my_country ๊ฐ ์ฌ๊ธฐ์ ์ ์ฅ๋จ
}
ํน์ ๋ฉ์๋๊ฐ ํด๋น ํ๋๋ฅผ ๋ฐํํ๋๋ก ์ค์ ํ ์๋ ์๋ค.
public class MyBean {
public int id;
private String name;
@JsonGetter("name")
public String myGetTheName() {
return name;
}
@JsonSetter("name")
public void mySetTheName(String name) {
this.name = name;
}
}
ํ๋๊ฐ ์๋ ์์ฑ์๋ก ๊ฐ์ฒด๋ฅผ ๋ง๋ค๊ณ ์ถ๋ค๋ฉด, @JsonCreator ๋ฅผ ์ฌ์ฉํ๋ฉด ๋๋ค.
public class BeanWithCreator {
public int id;
public String name;
@JsonCreator
public BeanWithCreator(
@JsonProperty("id") int id,
@JsonProperty("theName") String name) {
this.id = id;
this.name = name;
}
}
๋ง์ฝ ๋ง๋ค์ด๋ DTO๊ฐ ์ฌ์ฌ์ฉ๋๋ค๋ฉด, ์๋์ ๊ฐ์ด ํ ํ๋์ ์ฌ๋ฌ ๊ฐ์ ์ง์ ํ ์ ์๋ค.
public class AliasBean {
@JsonAlias({ "fName", "f_name" })
private String firstName;
private String lastName;
}
๋ง์ง๋ง์ผ๋ก ์ง๋ ฌํํ๋ฉด ์๋๋ ํ๋๋ @JsonIgnore, @JsonIgnoreProperties ๋ก ํ๋๊ฐ ์๋ ๊ฒ์ฒ๋ผ ์ฒ๋ฆฌํ ์ ์๋ค.
public class BeanWithIgnore {
@JsonIgnore
public int id; // ์ง๋ ฌํ๊ฐ ๋์ง ์์. ๊ฒฐ๊ณผ์๋ name ๋ง ๋จ๊ฒ๋จ
public String name;
}
@JsonIgnoreProperties({ "id" })
public class BeanWithIgnore {
public int id; // ์ง๋ ฌํ๊ฐ ๋์ง์์. ๊ฒฐ๊ณผ ๊ฐ์๋ name๋ง ๋จ๊ฒ๋จ.
public String name;
}
public class User {
public int id;
public Name name;
@JsonIgnoreType // inner class๋ ์ด๋ ๊ฒ ignore๋ฅผ ์ค์ ํ ์ ์๋ค.
public static class Name {
public String firstName;
public String lastName;
}
}
๋๋ null ์ธ ๊ฐ๋ง ์ง๋ ฌํ ๋์์์ ์ ์ธํ๊ณ ์ถ๋ค๋ฉด, ์๋์ ๊ฐ์ด @JsonInclude๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ๋ ์๋ค.
@JsonInclude(Include.NON_NULL)
public class MyBean {
public int id; // null ์ธ ๊ฒฝ์ฐ ์ ์ธ๋จ.
public String name; // null ์ธ ๊ฒฝ์ฐ ์ ์ธ๋จ
}
๋ง์ง๋ง์ผ๋ก DTO๋ฅผ ์์ํ๊ฑฐ๋ inner class๋ฅผ ์ฌ์ฉํ ๊ฒฝ์ฐ์๋ ๋ณํ์ด ๊ฐ๋ฅํ๋ค.
๐งจ ๋ค๋ง ์ด๋ ๊ฒ ๋ณต์กํ๊ฒ ์ฌ์ฉํ์ง ๋ง๊ณ , ๊ทธ๋ฅ ํด๋์ค๋ฅผ 2๊ฐ๋ก ๋๋ ์ ์ฐ์.
public class Zoo {
public Animal animal;
public Zoo(Animal animal) {
this.animal = animal;
}
@JsonTypeInfo( // ์ํ๋ ๋ชจ์ {"animal": {"type":"dog", "name":"...", "barkVolume":0.0} }
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type") // "type"์ ํ์
์ ๋ณด๊ฐ ๊ธฐ๋ก๋๋๋ก ๋ง๋ค๊ธฐ
@JsonSubTypes({ // ์ด๋ค ํ์
์ธ์ง ๊ธฐ๋กํ๊ณ ์ถ๋ค๋ฉด, @JsonSubTypes๋ฅผ ์ด์ฉํด์ ์ ์
@JsonSubTypes.Type(value = Dog.class, name = "dog"),
@JsonSubTypes.Type(value = Cat.class, name = "cat") })
public static class Animal {
public String name;
}
@JsonTypeName("dog")
public static class Dog extends Animal {
public double barkVolume;
public Dog(String name) {
super.name = name;
}
}
@JsonTypeName("cat")
public static class Cat extends Animal {
public int lives;
boolean likesCream;
}
}
'๐ฑ Spring Framework > Spring MVC' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
๋ก๊ทธ ์ถ์ ๊ธฐ๋ก ์์๋ณด๋ ๋์์ธํจํด (0) | 2022.02.10 |
---|---|
Spring ํ์ผ ์ ๋ก๋, ๋ค์ด๋ก๋ (0) | 2021.08.31 |
Spring ์์ธ์ฒ๋ฆฌ - API, @ExceptionHandler (0) | 2021.08.30 |
Spring ์์ธ์ฒ๋ฆฌ - ์ค๋ฅํ์ด์ง(404,500) (0) | 2021.08.30 |
Spring Login#2 ํํฐ, ์ธํฐ์ ํฐ, ArugmentResolver (0) | 2021.08.30 |
๋ธ๋ก๊ทธ์ ์ ๋ณด
JiwonDev
JiwonDev