본 포스팅은 인프런 - 스프링 MVC 2편을 강의를 바탕으로 공부하고 정리한 글입니다.
📢 본 포스팅에 앞서 예제를 위한 프로젝트를 생성하자.
📌 프로젝트 생성
- Project: Gradle
- Project Language: Java
- Spring Boot: 2.7.0 (정식버전 중 가장 최신 버전 선택)
- Project Metadata
- Group: hello
- Artifact: typeconverter
- Name: typeconverter
- Package name: hello.typeconverter
- Packaging: Jar
- Java: 11
- Dependencies : Spring Web, Thymeleaf, Lombok
📌 프로젝트 실행
- 인텔리제이로 프로젝트를 Open하고, 동작하는지 확인한다.
- 기본 메인 클래스 실행 (TypeconverterApplication.main())
- http://localhost:8080 호출해서 Whitelabel Error Page가 나오면 정상 동작
타입 컨버터
어플리케이션을 개발하다 보면 타입을 변환해야 하는 경우가 상당히 많다.
예를 들어 문자를 숫자 타입으로 변경한다고 가정해보자
📂 controller/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("intValue = " + intValue);
return "ok";
}
}
- HTTP 요청 파라미터는 모두 문자로 처리된다.
→ String data = request.getParameter("data") - 따라서 요청 파라미터를 자바에서 다른 타입으로 변환해서 사용하고 싶다면 숫자 타입으로 변화나는 과정을 거쳐야한다.
→ Integer intValue = Integer.valueOf(data)
👉🏻 스프링 @RequestParam 사용
📂 controller/HelloController
@GetMapping("/hello-v2")
public String helloV2(@RequestParam Integer data) {
System.out.println("data = " + data);
return "ok";
}
- 스프링이 제공하는 @RequestParam을 사용하면 문자를 Integer 타입의 숫자로 받을 수 있다. (스프링이 내부적으로 타입을 변환해줌)
위와 마찬가지로 @ModelAttribute, @PathVariable에서도 스프링이 내부적으로 타입 변환을 해준다.
💡 스프링의 타입 변환 적용의 예
- 스프링 MVC 요청 파라미터 : @RequestParam, @ModelAttribute, @PathVarialble
- @Value 등으로 YML 정보 읽기
- XML에 넣은 스프링 빈 정보를 반환
- 뷰를 렌더링 할 때
이렇게 타입을 변환해야 하는 경우가 상당히 많다.
이때 개발자가 직접 하나하나 타입을 변환 해야 한다면 상당히 번거로울 것이다.
하지만 스프링이 중간에 타입 변환기를 사용해 타입을 변환해주기 때문에 개발자는 따로 변환해 줄 필요 없이 해당 타입을 편리하게 받을 수 있다.
만약 개발자가 새로운 타입을 만들어 변환하고 싶다면 어떻게 할 수 있을까?
컨버터
컨버터 인터페이스
스프링은 확장 가능한 인터페이스를 제공한다.
따라서 스프링에 추가적인 타입 변환이 필요할 경우 개발자는 이 컨버터 인터페이스를 구현해 등록해주면 된다.
💡 컨버터 인터페이스
public interface Converter<S, T> {
T convert(S source);
}
- 컨버터 인터페이스는 모든 타입에 적용 가능하다.
- X→Y, Y→X 쌍방향으로 타입 변환이 필요한 경우 두 개의 컨버터를 만들어 등록하면 된다.
⭐ Converter라는 이름의 인터페이스가 많으니 조심해야 한다.
org.springframework.core.convert.converter.Converter를 사용해야 한다.
스프링은 용도에 따라 다양한 방식의 타입 컨버터를 제공한다.
1. Converter → 기본 타입 컨버터
2. ConverterFactory → 전체 클래스 계층 구조가 필요할 때
3. GenericConverter → 정교한 구현, 대상 필드의 어노테이션 정보 사용 가능
4. ConditionalGenericConverter → 특정 조건이 참인 경우에만 실행
더욱 자세한 내용은 공식문서를 참고하자.
컨버터 사용하기
이제 타입 컨버터를 어떻게 사용하는지 코드로 알아보자.
예제1 (문자 → 숫자)
우선 단순하게 문자 → 숫자로 바꾸는 타입 컨버터를 만들어보자.
👉🏻 타입 컨버터 (문자 → 숫자)
📂 converter/StringToIntegerConverter
@Slf4j
public class StringToIntegerConverter implements Converter<String, Integer> {
@Override
public Integer convert(String source) {
log.info("convert source = {}", source);
return Integer.valueOf(source);
}
}
👉🏻 타입 컨버터 (숫자 → 문자)
📂 converter/IntegerToStringConverter
@Slf4j
public class IntegerToStringConverter implements Converter<Integer, String> {
@Override
public String convert(Integer source) {
log.info("convert source = {}", source);
return String.valueOf(source);
}
}
👉🏻 타입 컨버터 테스트 코드
📂 test/.../converter/ConverterTest
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");
}
}
예제2 (문자 → 객체)
이번에는 127.0.0.1:8080과 같은 IP, PORT를 입력하면 IpPort 객체로 변환하는 사용자 정의 타입 컨버터를 만들어보자.
👉🏻 IpPort 객체
📂 type/IpPort
@Getter
@EqualsAndHashCode
public class IpPort {
private String ip;
private int port;
public IpPort(String ip, int port) {
this.ip = ip;
this.port = port;
}
}
- @EqualsAndHashCode
- 모든 필드를 사용해 equals(), hashcode()를 생성한다.
- 따라서 모든 필드의 값이 같다면, a.equals(b)의 결과가 참이 된다.
👉🏻 타입 컨버터 (문자 → IpPort 객체)
📂 converter/StringToIpPortConverter
@Slf4j
public class StringToIpPortConverter implements Converter<String, IpPort> {
@Override
public IpPort convert(String source) {
log.info("converter source = {}", source);
String[] split = source.split(":");
String ip = split[0];
Integer port = Integer.valueOf(split[1]);
// ex) 127.0.0.1:8080 -> IpPort 객체
return new IpPort(ip, port);
}
}
👉🏻 타입 컨버터 (IpPort 객체 → 문자)
📂 converter/IpPortToStringConverter
@Slf4j
public class IpPortToStringConverter implements Converter<IpPort, String> {
@Override
public String convert(IpPort source) {
log.info("converter source = {}", source);
// ex) IpPort 객체 -> 127.0.0.1:8080
return source.getIp() + ":" + source.getPort();
}
}
👉🏻 타입 컨버터 테스트 코드
📂 test/.../converter/ConverterTest
@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();
IpPort ipPort = new IpPort("127.0.0.1", 8080);
String result = converter.convert(ipPort);
assertThat(result).isEqualTo("127.0.0.1:8080");
}
한계점
- 예제를 통해 타입 컨버터 인터페이스를 구현해 직접 사용해보았다. 그런데 그 과정이 개발자가 직접 타입 변환을 해주는 것과 큰 차이가 없는 것을 알 수 있다.
- 따라서 타입 컨버터를 등록하고 관리하면서 편리하게 변환 기능을 제공하는 역할을 하는 무언가가 필요하다.
컨버전 서비스
컨버전 서비스 인터페이스
스프링은 타입 컨버터를 하나하나 직접 찾아 타입 변환에 사용하는 불편함을 해결하기 위해 개별 컨버터를 모아두고 그것들을 묶어 편리하게 사용할 수 있는 컨버전 서비스 기능을 제공한다.
💡 컨버전 서비스 인터페이스
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);
}
- 컨버팅이 가능한가 확인하는 기능(canConvert)과, 컨버팅 기능(convert)을 제공한다.
컨버전 서비스 사용하기
그럼 예시를 통해 컨버전 서비스를 확인해보자.
👉🏻 컨버전 서비스 테스트 코드
📁test/.../converter/ConversionServiceTest
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
- ConversionService 인터페이스 구현체로, 다음 두 인터페이스를 구현했다
- ConversionService : 컨버터 사용에 초점
- ConverterRegistry : 컨버터 등록에 초점
- 이렇게 인터페이스를 분리하면 컨버트의 등록과 사용의 관심사를 명확하게 분리할 수 있다. → 인터페이스 분리 원칙(ISP)
- 컨버터를 등록할 때는 타입 컨버터를 알아야 하지만, 컨버터를 사용할 때는 타입 컨버터를 몰라도 된다.
- 즉, 컨버터를 사용하는 사용자는 컨버터를 어떻게 등록하고 관리하는지는 전혀 모른채 컨버전 서비스 인터페이스에만 의존하면 된다.
- 이렇게 인터페이스를 분리하는 것을 ISP라 한다.
- ConversionService 인터페이스 구현체로, 다음 두 인터페이스를 구현했다
ISP에 대해 더 알고 싶다면 여기를 참고하자.
정리
- 스프링은 내부에서 컨버전 서비스를 사용해 타입을 변환한다.
웹 어플리케이션에 컨버터 적용하기
그럼 이제 앞서 알아본 컨버터를 실제 웹 어플리케이션에 적용해보도록 하자.
스프링에 적용
👉🏻 컨버터 등록
📂 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());
}
}
- 스프링은 내부에서 ConversionService를 제공한다.
- 따라서 WebMvcConfigurer가 제공하는addFormatters()를 사용해 추가하고 싶은 컨버터를 등록해주면 된다.
- 그럼 스프링은 내부에서 사용하는 ConversionService에 해당 컨버터를 추가해준다.
👉🏻 컨버터(StringToIntegerConverter) 동작 확인 - 기존 코드
📂 controller/HelloController
@GetMapping("/hello-v2")
public String helloV2(@RequestParam Integer data) {
System.out.println("data = " + data);
return "ok";
}
👉🏻 실행 로그 (http://localhost:8080/hello-v2?data=10)
StringToIntegerConverter : convert source=10
data = 10
쿼리 파라미터(data=10)는 문자 타입으로, 이것을 Integer data로 변환하는 과정에서 직접 등록한 StringToIntegerConverter가 작동하는 로그를 확인할 수 있다.
정리
- 이전을 떠올려 보면 StringToIntegerConverter를 등록하기 전에도 타입 변환이 알아서 잘 이뤄졌다. 그것은 스프링은 내부에 수 많은 기본 컨버터들을 제공하기 때문이다.
- 컨버터를 수동으로 추가해줄 경우 추가한 컨버터가 기본 컨버터보다 높은 우선순위를 가진다.
그럼 기본 컨버터가 적용되지 않는 직접 정의한 타입인 IpPort를 사용해보도록 하자.
👉🏻 컨버터(StringToIpPortConverter) 동작 확인 - 추가
📂 controller/HelloController
@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)
StringToIpPortConverter : convert source=127.0.0.1:8080
ipPort IP = 127.0.0.1
ipPort PORT = 8080
- 등록해준 StringToIpPortConverter가 작동하며, 쿼리 파라미터(ipPort=127.0.0.1:8080)가 문자타입에서 IpPort 객체 타입으로 잘 변환된 것을 확인할 수 있다.
💡 처리 과정
- @RequestParam은 @RequestParam을 처리하는 ArgumentResolver인 RequestParamMethodArgumentResolver에서 ConversionService를 사용해 타입을 변환한다.
- 이는 복잡한 내부 과정을 거치기 때문에 대략 이렇게 처리되는 것 정도로만 이해해도 충분하다.
- 만약 더 깊이있게 과정을 확인하고 싶다면 StringToIpPortConverter에 디버그 브레이크 포인트를 걸어 확인해보자!
뷰 템플릿에 적용
타임리프는 컨버터를 적용해 렌더링 하는 방법을 편리하게 지원한다.
💡 타임리프
타임리프 표현식 | 설명 |
${...} | 변수 표현식 |
${{...}} | 컨버전 서비스 적용 표현식 |
- 타임리프는 괄호 두개를 사용하면 자동으로 컨버전 서비스를 사용해 변환된 결과를 출력해준다.
- 물론 스프링과 통합되어 스프링이 제공하는 컨버전 서비스를 사용하기 때문에, 우리가 등록해준 컨버터들을 사용할 수 있다.
타임리프 문법 | 설명 |
th:field | id, name을 출력하는 등 다양한 기능을 가짐 컨버전 서비스가 함께 적용됨 |
- th:field는 괄호 하나만 사용해도 타임리프가 자동으로 컨버전 서비스를 적용해준다.
우선 뷰 템플릿에 컨버터를 적용하는 예제를 통해 알아보자.
👉🏻 컨트롤러 생성
📂 controller/ConverterController
@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";
}
}
👉🏻 뷰 템플릿 생성
📂 templates/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>
👉🏻 실행 결과 (http://localhost:8080/converter-view)
// 실행 로그
IntegerToStringConverter : convert source=10000
IpPortToStringConverter : convert source=hello.typeconverter.type.IpPort@59cb0946
- ${{number}}
- 뷰 템플릿은 데이터를 문자로 출력한다.
- 따라서 컨버터를 적용하면 Integer 타입을 String 타입으로 변환하는 IntegerToStringConverter를 실행한다.
- 하지만 이부분은 컨버터를 실행하지 않더라고타임리프가 숫자를 문자로 자동으로 변환해주기 때문에 같은 결과가 나온다.
- ${{ipPort}}
- 뷰 템플릿은 데이터를 문자로 출력하기 때문에 컨버터를 적용하지 않은 결과는 객체의 참조값이 출력된다.
- 컨버터를 적용하면 IpPort 타입을 String 타입으로 변환하는 IpPortToStringConverter가 적용되어 127.0.0.1:8080이 출력된다.
이번에는 컨버터를 폼에 적용해보도록 하자.
👉🏻 컨트롤러 - 추가
📂 controller/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;
}
}
- Form 객체 : 데이터를 전달하는 폼 객체로 사용한다.
- GET /converter/edit : IpPort를 뷰 템플릿 폼에 출력한다.
- POST /converter/edit : 뷰 템플릿 폼의 IpPort 정보를 받아서 출력한다.
👉🏻 뷰 템플릿 생성
📂 templates/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>
- 타임리프의 th:field는 괄호 하나만 사용해도 자동으로 컨버전 서비스를 적용시켜 출력해준다.
👉🏻 실행 결과 (http://localhost:8080/converter/edit)
- GET /converter/edit
- th:field 가 자동으로 컨버전 서비스를 적용해주어서 ${{ipPort}} 처럼 적용되었다.
- 따라서 IpPort → String 으로 변환된다.
- POST /converter/edit
- @ModelAttribute 를 사용해서 String → IpPort 로 변환된다.
정리
- 뷰 템플릿에 보여줄 때는 객체를 문자로 변환하고, 폼에서 값이 넘어올 때는 문자를 객체로 변환해주는 것을 알 수 있다.
포맷터
객체를 특정한 포맷에 맞춰 문자로 출력하거나 또는 그 반대의 역할을 하는 것에 특화된 기능이 포맷터(Formatter)이다.
포맷터는 컨버터의 특별한 버전으로 이해하면 된다.
💡 Converter vs Formatter
- Converter : 범용 타입 변환 기능을 제공 (객체 → 객체)
- Formatter : 문자에 특화된 타입 변환 기능 제공 (객체 → 문자, 문자 → 객체) + Locale 기능 제공
- 예) 1000 단위에 쉼표를 넣어서 출력하는 경우 (숫자 1000 → 문자 1,000)
- 예) 날짜 객체를 문자인 2022-01-01 10:50:11와 같이 출력하는 경우
- Locale (현지화) : 날짜 숫자의 표현 방법은 현지화 정보가 사용될 수 있다.
포맷터 인터페이스
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) : 문자 → 객체
포맷터 사용하기
그럼 예제를 통해 포맷터를 어떻게 사용하는지 알아보자.
숫자 1000을 문자 "1,000"으로 변환되는 포맷을 적용해보도록 하자.
👉🏻 포맷터 생성
📂 fomatter/MyNumberFormatter
@Slf4j
public class MyNumberFormatter implements Formatter<Number> {
@Override
public Number parse(String text, Locale locale) throws ParseException {
log.info("text={}, locale={}", text, locale);
// 문자 -> 객체(Number)
return NumberFormat.getInstance(locale).parse(text);
}
@Override
public String print(Number object, Locale locale) {
log.info("object={}, locale={}", object, locale);
// 객체(Number) -> 문자
return NumberFormat.getInstance(locale).format(object);
}
}
- NumberFormat
- "1,000"처럼 숫자 중간의 쉼표를 적용하려면 자바가 기본으로 제공하는 NumberFormat 객체를 사용하면 된다.
- 이 객체는 Locale 정보를 활용해 나라별로 다른 숫자 포맷을 만들어준다.
- parse()
- 문자 → 객체(Number) 변환
- 이때 변환된 Number의 타입은 Integer, Long과 같은 숫자 타입의 부모 클래스이다.
- print()
- 객체(Number) → 문자 변환
👉🏻 테스트 코드 작성
class MyNumberFormatterTest {
MyNumberFormatter formatter = new MyNumberFormatter();
@Test
void parse() throws ParseException {
Number result = formatter.parse("1,000", Locale.KOREA);
assertThat(result).isEqualTo(1000L); // Long 타입 주의
}
@Test
void print() {
String result = formatter.print(1000, Locale.KOREA);
assertThat(result).isEqualTo("1,000");
}
}
- parse()의 결과값이 Long이기 때문에 isEqualTo(1000L)을 통해 비교할 때 L을 넣어줘야 한다.
👉🏻 실행 결과 로그
MyNumberFormatter - text=1,000, locale=ko_KR
MyNumberFormatter - object=1000, locale=ko_KR
스프링은 용도에 따라 다양한 방식의 포맷터를 제공한다.
• Formatter 포맷터
• AnnotatonFormatterFactory 필드의 타입이나 어노테이션 정보를 활용할 수 있는 포맷터
자세한 내용은 공식 문서를 참고하자.
컨버전 서비스 - 포맷터 지원
이전에 사용했던 ConversionService에는 컨버터만 등록할 수 있고 포맷터는 등록할 수 없다.
따라서 포맷터를 지원하는 별도의 컨버전 서비스를 사용해 포맷터를 추가해줘야 한다.
FomattingConversionService는 포맷터를 지원하는 컨버전 서비스이다. 내부에서 어댑터 패턴을 사용해 포맷터가 컨버터처럼 동작하도록 지원해준다.
더 나아가 DefaultFormattingConversionService는 FomattingConversionService에 기본적인 통화, 숫자 관련 몇가지 기본 포맷터를 추가해서 제공한다.
💡 DefaultFormattingConversionService 상속 관계
- FormattingConversionService는 ConversionService 관련 기능을 상속받기 때문에 컨버터와 포맷터 모두 등록할 수 있다.
- 사용할 때는 ConversionService가 제공하는 convert()를 사용하면 된다.
DefaultFormattingConversionService 사용하기
예제를 통해 알아보자.
👉🏻 테스트 코드 작성
📂 test/.../formatter/FormattingConversionServiceTest
public class FormattingConversionServiceTest {
@Test
void formattingConversionService() {
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);
}
}
웹 어플리케이션에 포맷터 적용하기
이제 실제 웹 어플리케이션에 포맷터를 적용해보도록 하자.
👉🏻 포맷터 등록
📂 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는 반드시 주석처리 해줘야 한다.
👉🏻 실행 결과 (http://localhost:8080/converter-view)
컨버전 서비스를 적용한 결과 MyNumberFormatter가 적용되어 숫자 10000이 쉼표가 들어간 10,000 문자로 변환되어 출력된 것을 확인할 수 있다.
스프링이 제공하는 기본 포맷터
스프링은 자바에서 기본으로 제공하는 타입들에 대해 수 많은 포맷터를 기본으로 제공한다.
그런테 포맷터는 기본 형식이 지정되어 있기 때문에, 객체의 각 필드마다 다른 형식으로 포맷을 지정하기는 어렵다.
스프링은 이러한 문제를 해결하기 위해 어노테이션 기반으로 원하는 형식을 지정해서 사용할 수 있는 매우 유용한 두가지의 포맷터를 기본으로 제공한다.
포맷터 종류 | 설명 |
@NumberFormat | 숫자 관련 형식 지정 포맷터 사용 NumberFormatAnnotationFormatterFactory |
@DateTimeFormat | 날짜 관련 형식 지정 포맷터 사용 Jsr310DateTimeFormatAnnotationFormatterFactory |
@NumberFormat, @DateTimeFormat
예제를 통해 확인해보도록 하자.
👉🏻 컨트롤러 생성
📂 controller/FormatterController
@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, Model model) {
return "formatter-view";
}
@Data
static class Form {
@NumberFormat(pattern = "###,###")
private Integer number;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime localDateTime;
}
}
👉🏻 뷰 템플릿 생성
📂 templates/formatter-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">
number <input type="text" th:field="*{number}"><br/>
localDateTime <input type="text" th:field="*{localDateTime}"><br/>
<input type="submit"/>
</form>
</body>
</html>
📂 templates/formatter-view.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<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>
</body>
</html>
👉🏻 실행 결과 (http://localhost:8080/formatter/edit)
- GET /formatter/eidt
- POST /formatter/eidt
어노테이션을 사용해 지정해준 포맷으로 출력된 것을 확인 할 수 있다.
@NumberFormat, @DateTimeFormat의 자세한 사용법이 궁금하다면, 관련 어노테이션을 검색해보거나 해당 링크를 참고하자.
정리
- 컨버터를 사용하든, 포맷터를 사용하든 등록 방법은 다르지만 사용할 때는 컨버전 서비스를 통해 일관성 있게 사용할 수 있다.
'🌱 Spring > Web MVC' 카테고리의 다른 글
API 예외처리 (0) | 2022.05.19 |
---|---|
예외처리, 오류 페이지 (0) | 2022.05.16 |
로그인 (필터, 인터셉터) (0) | 2022.05.10 |
로그인 (쿠키, 세션) (0) | 2022.05.02 |
Bean Validation (0) | 2022.04.30 |
Validation (0) | 2022.04.26 |
타임리프(Thymeleaf) (1) | 2022.03.23 |
스프링 MVC 웹 페이지 만들기 (0) | 2022.03.22 |