Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
Tags
- 스프링 고급 - 스프링 aop
- 자바의 정석 기초편 ch11
- 타임리프 - 기본기능
- 자바의 정석 기초편 ch8
- 코드로 시작하는 자바 첫걸음
- 자바의 정석 기초편 ch5
- jpa 활용2 - api 개발 고급
- jpa - 객체지향 쿼리 언어
- 스프링 mvc2 - 로그인 처리
- 2024 정보처리기사 수제비 실기
- 게시글 목록 api
- 스프링 mvc1 - 서블릿
- 자바의 정석 기초편 ch4
- 스프링 db1 - 스프링과 문제 해결
- 스프링 db2 - 데이터 접근 기술
- 스프링 입문(무료)
- 자바의 정석 기초편 ch6
- 자바의 정석 기초편 ch9
- 자바의 정석 기초편 ch3
- 스프링 mvc2 - 검증
- 자바의 정석 기초편 ch12
- 자바의 정석 기초편 ch1
- 자바의 정석 기초편 ch13
- 자바의 정석 기초편 ch2
- 스프링 mvc2 - 타임리프
- 스프링 mvc1 - 스프링 mvc
- 2024 정보처리기사 시나공 필기
- 자바의 정석 기초편 ch7
- 자바의 정석 기초편 ch14
- @Aspect
Archives
- Today
- Total
나구리의 개발공부기록
스프링 타입 컨버터 - 프로젝트 생성, 소개, Converter, ConversionSevice, 스프링에 Converter적용, 뷰 템플릿에 컨버터 적용, Formatter, 포맷터 적용, 스프링이 제공하는 기본 포맷터 본문
인프런 - 스프링 완전정복 코스 로드맵/스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술
스프링 타입 컨버터 - 프로젝트 생성, 소개, Converter, ConversionSevice, 스프링에 Converter적용, 뷰 템플릿에 컨버터 적용, Formatter, 포맷터 적용, 스프링이 제공하는 기본 포맷터
소소한나구리 2024. 9. 9. 18:51 출처 : 인프런 - 스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
1. 프로젝트 생성
2. 스프링 타입 컨버터 소개
1) 예제 코드
- 예제 코드를 작성 후 /hello-v1?data=10, /hello-v2?data=10 이렇게 웹 브라우저에서 실행을 하면 쿼리스트링으로 전달되는 값은 숫자처럼 보여도 문자임(URL 경로는 문자)
- HTTP 요청 파라미터는 모두 문자로 처리 되기 때문에 helloV1 메서드 처럼 요청 파라미터를 자바에서 다른 타입으로 변환해서 사용하려면 타입을 변환해야함
- 스프링 MVC가 제공하는 @RequestParam을 사용하면 helloV2의 메서드처럼 바로 타입 변환을 할 수 있는데, 이것은 스프링이 중간에서 타입을 변환해주었기 때문임
- @ModelAttribute나 @PathVariable을 사용할 때도 Integer로 타입을 변환할 수 있는 이유도 위와 동일함.
package hello.typeconverter.controller;
@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";
}
}
2) 스프링의 타입 변환 적용 예시
- 스프링 MVC 요청 파라미터: @RequestParam, @ModelAttribute, @PathVariable
- @Value 등으로 YML 정보 읽기
- XML에 넣은 스프링 빈 정보를 변환
- 뷰를 렌더링 할 때
3) 스프링과 타입 변환
- 이렇게 타입 변환해야 하는 경우가 상당히 많은데, 개발자가 직접 하나하나 타입 변환을 해야하는 번거로운 작업을 스프링이 중간에 타입 변환기를 사용해서 변환해 주기 때문에 편리하게 해당 타입을 받을 수 있음
- 문자를 숫자로, 숫자를 문자로, Boolean 타입을 숫자로 변경하는 것도 가능함
- 개발자가 새로운 타입을 만들어서 변환하는 것도 가능함(새로운 클래스를 만들고 해당 클래스 내에서 다양한 변환을 정의하는 것)
(1) 컨버터 인터페이스
- 스프링은 확장 가능한 컨버터 인터페이스를 제공하며 개발자가 추가적인 타입 변환이 필요하면 이 컨버터 인터페이스를 구현해서 등록하면 됨
- 과거에는 PropertyEditor라는 것으로 타입을 변환했으나 동시성 문제가 있어 타입을 변환할 때 마다 객체를 계속 생성해야하는 단점이 있어 기능확장 시에는 Converter를 사용
@FunctionalInterface
public interface Converter<S, T> {
@Nullable
T convert(S source);
// ... 기타 코드
}
3. 타입 컨버터 - Converter
- Converter라는 이름의 인터페이스가 많으니 패키지 경로를 꼭 확인해서 사용할 것
- org.springframework.core.convert.converter.Converter
1) 원초적인 컨버터
(1) Integer -> String 변환 컨버터
package hello.typeconverter.converter;
// 숫자를 문자로 변환하는 컨버터
@Slf4j
public class IntegerToStringConverter implements Converter<Integer, String> {
@Override
public String convert(Integer source) {
log.info("convert source: {}", source);
return String.valueOf(source); // Integer -> String 변환
}
}
(2) String -> Integer 변환 컨버터
package hello.typeconverter.converter;
// 문자를 숫자로 변환하는 타입 컨버터
@Slf4j
public class StringToIntegerConverter implements Converter<String, Integer> {
@Override
public Integer convert(String source) {
log.info("convert source: {}", source);
return Integer.valueOf(source); // String -> Integer 변환
}
}
(3) Test
- 단순하고 원초적인 로직으로 컨버터를 구현하고 테스트를 해보면 당연히 컨버팅이 잘 되어 테스트가 통과됨
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) 사용자 정의 타입 컨버터
- 조금더 실무에서 자주쓰는 실용적인 예제로 IP, PORT를 입력하면 IpPort 객체로 변환하는 컨버터를 구현해보기
- "127.0.0.1:8080" 이렇게 들어오면 ip는 String, port는 Integer로 변환되는 컨버터
(1) IpPort 클래스 정의
- @EqualsAndHashCode : 모든 필드를 사용해서 equals(), hashcode()를 생성
- 실제 값이 같다면(논리적 값) a.equals(b)의 결과가 true가 됨
package hello.typeconverter.type;
@Getter
@EqualsAndHashCode // 롬복 - [equals(): 값 비교, hashCode(): 객체의 해시코드 반환]을 자동 생성
public class IpPort {
private String ip;
private int port;
public IpPort(String ip, int port) {
this.ip = ip;
this.port = port;
}
}
(2) String -> IpPort, IpPort -> String 으로 변환하는 컨버터 생성
- Converter<String, IpPort> 구현 -> String 을 IpPort로 변환
- Converter<IpPort, String> 구현 -> IpPosrt를 String으로 변환
package hello.typeconverter.converter;
@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]; // 앞쪽은 String = ip
int port = Integer.parseInt(split[1]); // 뒤쪽은 int = 포트
return new IpPort(ip, port); // IpPort 객체에 값을 넣어서 생성
}
}
package hello.typeconverter.converter;
// Ip(String), Port(Integer) -> "127.0.0.1:8080" 변환
@Slf4j
public class IpPortToStringConverter implements Converter<IpPort, String> {
@Override
public String convert(IpPort source) {
log.info("covert source: {}", source);
return source.getIp() + ":" + source.getPort(); // ip와 포트를 + 연산자로 문자열 결합
}
}
(3) Test 코드 추가
- 테스트 코드를 작성 후 실행해보면 테스트가 통과되어 컨버팅이 잘 된 것을 확인할 수 있음
public class ConverterTest {
@Test
void ipPortToString() {
IpPortToStringConverter converter = new IpPortToStringConverter();
IpPort ipPort = new IpPort("127.0.0.1", 8080); // IpPort 객체 생성
String result = converter.convert(ipPort); // String 변환
assertThat(result).isEqualTo("127.0.0.1:8080"); // 변환 비교
}
@Test
void stringToIpPort() {
StringToIpPortConverter converter = new StringToIpPortConverter();
String source = "127.0.0.1:8080"; // 문자열 생성
IpPort result = converter.convert(source); // 문자열을 IpPort 타입으로 변환
assertThat(result).isEqualTo(new IpPort("127.0.0.1", 8080)); // @EqualsAndHashCode 때문에, 값비교가 가능
}
}
3) Converter 정리
- 타입 컨버터 인터페이스는 단순해서 이해하기는 쉽다
- 하지만 이렇게 타입 컨버터가 필요할 때마다 하나하나를 직접 사용하면 개발자가 직접 컨버팅 하는 것과 차이가 없게 되는데 스프링이 개별 컨버터를 모아두고 묶어서 편리하게 사용할 수 있는 컨버전 서비스라는 기능을 제공함
- 스프링은 용도에 따라 다양한 방식의 타입 컨버터를 제공하고, 문자, 숫자, 불린, Enum 등 일반적인 타입에 대한 대부분의 컨버터를 기본으로 제공하므로 자세한 내용은 IDE의 구현체나 공식문서를 참고해서 확인
- https://docs.spring.io/spring-framework/reference/core/validation/convert.html
- Converter -> 기본 타입 컨버터
- ConverterFactory -> 전체 클래스 계층 구조가 필요할 때
- GenericConverter -> 정교한 구현, 대상 필드의 애노테이션 정보를 사용 가능
- ConditionalGenericConverter -> 특정 조건이 참인 경우에만 실행
4. 컨버전 서비스 - ConversionService
- 개별 컨버터를 묶어서 사용할 수 있는 기능을 제공
1) ConversionServiceTest - 컨버전 서비스 테스트 코드
- DefaultConversionService : ConversionService 인터페이스를 구현한 구현체
- .addConverter() 로 컨버터를 등록할 수 있음
- .convert() 메서드 하나로 작성된 컨버터의 룰에 따라 매개변수만 넣어주면 알아서 컨버터가 동작됨
public class ConversionServiceTest {
@Test
void conversionService() {
// DefaultConversionService : ConversionService 구현체중 하나, 컨버전을 등록할 수 있음
DefaultConversionService conversionService = new DefaultConversionService();
// 만들었던 컨버터들 모두 등록
conversionService.addConverter(new StringToIntegerConverter());
conversionService.addConverter(new IntegerToStringConverter());
conversionService.addConverter(new StringToIpPortConverter());
conversionService.addConverter(new IpPortToStringConverter());
// .convert()로 사용
// StringToIntegerConvert 동작
assertThat(conversionService.convert("10", Integer.class)).isEqualTo(10);
// IntegerToStringConvert 동작
assertThat(conversionService.convert(10, String.class)).isEqualTo("10");
// StringToIpPortConvert 동작
assertThat(conversionService.convert("127.0.0.1:8080", IpPort.class))
.isEqualTo(new IpPort("127.0.0.1", 8080));
// IpPortToStringConvert 동작
assertThat(conversionService.convert(new IpPort("127.0.0.1", 8080), String.class))
.isEqualTo("127.0.0.1:8080");
}
}
(1) 등록과 사용 분리
- 컨버터를 등록할 때는 StringToIntegerConverter와 같이 작성된 타입 컨버터를 명확하게 알아야 하지만 컨버터를 사용하는 입장에서는 타입 컨버터를 전혀 몰라도 컨버전 서비스 내부에 숨어서 동작하며 .convert() 메서드로 사용 가능함
- 컨버전 서비스를 등록하는 부분과 사용하는 부분을 분리하고 의존관계 주입을 사용하면 타입을 변환을 원하는 사용자는 구현체는 모르고 컨버전 서비스 인터페이스에만 의존하여 사용만 하면됨
(2) 인터페이스 분리 원칙 - (ISP; Interface Segregation Principle)
- DefaultConversionService는 컨버터 사용에 초점을 둔 ConversionService와 컨버터 등록에 초점을 둔 ConverterRegistry 두가지 인터페이스를 구현함
- 이렇게 각 인터페이스를 분리하면 컨버터를 사용하는 클라이언트와 컨버터를 등록하고 관리하는 클라이언트의 관심사를 명확하게 분리할 수 있고, 컨버터를 사용하는 클라이언트는 ConversionService만 의존하게 되면서 클라이언트가 꼭 필요한 메서드만 알게되도록 인터페이스를 분리하는 것을 ISP라고 함
- 스프링에서 코드를 구현할때 대부분 이런식으로 작성이 되어있음
5. 스프링에 Converter 적용하기
1) WebConfig - 컨버터 등록
- 스프링은 내부에서 ConversionService를 제공함
- WebMvcConfigurer가 제공하는 addFormatters()를 사용해서 추가하고 싶은 컨버터를 등록하면 스프링이 ConversionService에 컨버터를 추가해줌
package hello.typeconverter;
@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());
}
}
2) 동작 해보기
(1) 기존에 만들었던 helloV2() 컨버터 실행
- /hello-v2?data=19 로 접속
- 로그를 확인해보면 converter.StringToIntegerConverter : convert source: 19, data = 19 로 동작하는 것을 확인할 수 있음
- 그러나, 이렇게 등록 전에도 해당 코드는 동작이 잘 되었는데 이미 스프링이 내부에서 수많은 기본 컨버터를 제공하기 때문
- 컨버터를 추가하면 추가한 컨버터가 기본 컨버터보다 높은 우선 순위를 가짐
(2) HelloController에 직접 정의한 타입인 IpPort를 추가
- http://localhost:8080/ip-port?ipPort=127.0.0.1:8080로 접속
- 로그를 확인해보면 아래처럼 출력되어 변환이 잘 되었음
converter.StringToIpPortConverter : convert source: 127.0.0.1:8080
ipPort = 127.0.0.1
ipPort.getPort() = 8080 - @RequestParam, @ModelAttribute, @PathVariable이 모두 동일하게 적용됨
@GetMapping("ip-port")
public String ipPort(@RequestParam IpPort ipPort) {
System.out.println("ipPort = " + ipPort.getIp());
System.out.println("ipPort.getPort() = " + ipPort.getPort());
return "ok";
}
(3) 처리 과정
- RequestParam은 @RequestParam을 처리하는 ArgumentResolver인 RequestParamMethodArgumentResolver에서 ConversionService를 사용해서 타입을 변환함
- 부모 클래스와 다양한 외부 클래스를 호출하는 등 복잡한 내부 과정을 거치기 때문에 대략적으로 이렇게 처리되는 것으로 이해하고, 깊이 있게 확인하고자 하면 IpPortConverter에 디버그 브레이크 포인트를 걸어서 확인해보면 처리 과정들을 쭉 볼 수 있음
6. 뷰 템플릿에 컨버터 적용하기
- 타임리프틑 렌더링 시에 컨버터를 적용해서 렌더링 하는 방법을 편리하게 지원함
1) 컨버터 적용
(1) ConverterController
- model.addAttribute로 각 데이터 입력
package hello.typeconverter.controller;
@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";
}
}
(2) 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>
(3) 실행 결과 및 로그
- number의 경우 컨버터를 적용하든, 적용하지 않든 스프링이 기본으로 제공하는 컨버터가 있어서 10000이라는 String타입이 Integer로 자동으로 변환되어 출력이 됨
(컨버터를 적용하면 만들어준 컨버터가 적용되므로 IntegerToStringconverter가 적용된 것을 로그에서 볼 수 있음 - 하지만 ipPort의 경우 직접 정의한 객체이므로, 만든 컨버터를 적용하지 않으면 스프링이 이러한 상황은 컨버팅을 제공하지 않으므로 객체.toString으로 객체의 주소값이 출력이 되어버리게 됨
- 그래서 이런 경우에는 컨버전 서비스를 적용해주어야 정상적으로 출력이 됨
2) 폼에 적용하기
- HTTP Form에는 자동으로 컨버터가 적용이 됨
(1) ConverterController - 코드 추가
package hello.typeconverter.controller;
@Controller
public class 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";
}
// Form 객체를 하나 정의
@Data
static class Form {
private IpPort ipPort;
public Form(IpPort ipPort) {
this.ipPort = ipPort;
}
}
}
(2) converter-form.html 작성
- th:field : 컨버터 적용 -> 변환되어서 값이 출력됨
- th:value : 컨버터 적용 안함 -> 변환되지 않아서 객체의 주소값이 그대로 출력됨
<!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>
(3) 실행
- GET / converter/edit : th:filed가 자동으로 컨버전 서비스를 적용해서 IpPort -> String으로 변환됨
- POST /converter/edit : 제출을 누르면 POST 전송이 되는데, 넘어온 String을 @ModelAttribute를 사용해서 String -> IpPort로 변환 출력
7. 포맷터 - Formatter
- Converter는 입력과 출력 타입에 제한이 없는 범용 타입 변환기능을 제공
- 그러나 일반적인 웹 애플리케이션 환경을 보면 불린 타입을 숫자로 바꾸는 것 같은 범용 기능 보다는 문자를 다른 객체로 변환하거나, 다른 객체를 문자를 변환하는 상황이 대부분임
- ex) 숫자 출력 시 Integer -> String 출력 시점에 1000 -> "1,000" 이렇게 단위에 쉼표를 넣거나,
"1,000" 이라는 문자열을 1000이라는 숫자로 변경할때나,
날짜 객체를 문자인 "2021-01-01 10:50:11"과 같이 출력하거나 그 반대의 상황 등 - 날짜 / 숫자의 표현방법은 나라마다 표시 방식이 다를 수 있는데 Locale 현지화 정보화 정보가 사용될 수 있음
- 이런식으로 특정한 포맷에 맞추어 문자로 출력하거나 또는 그 반대의 역할을 하는 것에 특화된 기능이 바로 포맷터이며 컨버터의 특별한 버전이라고 이해하면 됨
Cpmverter vs Formatter
- Converter는 범용 : 객체 -> 객체
- Formatter는 문자에 특화 : 객체 -> 문자, 문자 -> 객체 + 현지화(Locale)
1) 포맷터 만들기
(1) Formatter 인터페이스 구조
- String print(T object, Locale locale) : 객체를 문자로 변경
- T Parse(String text, Locale 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> {
}
(2) MyNumberFormatter
package hello.typeconverter.formatter;
@Slf4j
// String 변환은 기본으로 되기 때문에 스트링을 제외할 변환 타입을 제네릭 타입에 넣어주면 됨
public class MyNumberFormatter implements Formatter<Number> {
@Override
public Number parse(String text, Locale locale) throws ParseException {
log.info("text={}, locale={}", text, locale);
// 특정 지역에 맞게 포멧팅하는 문법
NumberFormat format = NumberFormat.getInstance(locale);
return format.parse(text); // parse()로 String 을 포맷팅 변환
}
@Override
// Number : Integer, Long 과같은 숫자 타입의 부모 클래스임
public String print(Number object, Locale locale) {
log.info("object = {}, locale ={}", object, locale);
return NumberFormat.getInstance(locale).format(object); // .format()으로 Number 를 포맷팅 변환
}
}
(3) Test
- 테스트 하고자하는 클래스에 커맨드 + 쉬프트 + t를 하면 자동으로 test 디렉토리에 동일한 경로의 패키지를 생성하여 테스트 클래스를 생성해줌
- 만든 MyNumberFormatter()를 생성하여 Locale을 KOREA로 적용하여 test 진행
- .parse() : 반환타입이 Long 이라서 1000L 으로 반환됨 -> 필요시 Integer 변환
- .print() : Locale 정보가 반영된 포맷팅 문자 "1,000"이 반환됨
class MyNumberFormatterTest {
MyNumberFormatter formatter = new MyNumberFormatter();
@Test
void parse() throws ParseException {
Number result = formatter.parse("1,000", Locale.KOREA);
assertThat(result).isEqualTo(1000L); // Number 타입이지만 Long 으로 반환됨
}
@Test
void print() {
String result = formatter.print(1000, Locale.KOREA);
assertThat(result).isEqualTo("1,000");
}
}
8. 포맷터를 지원하는 컨버전 서비스
- 기본 컨버전 서비스는 컨버터만 등록할 수 있고 포맷터를 등록할 수 없는데, FormattingConversionService는 내부에서 어댑터 패턴을 사용해서 Formatter가 Converter 처럼 동작하도록 하여 컨버전 서비스에 포맷터를 추가할 수 있음
- DefualtFormattingConversionService는 FormattingConversionService에 기본적인 통화, 숫자 관련 몇가지 기본 포맷터를 추가해서 제공함
- FormattingconversionService는 ConversionService 관련 기능을 상속 받기 때문에 결과적으로 컨버터, 포맷터 모두 등록할 수 있으며 사용할 때는 구분없이 .convert로 사용하면 됨
1) Test
package hello.typeconverter.formatter;
public class FormattingConversionServiceTest {
@Test
void formattingConversionService() {
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
// 컨버터 등록
conversionService.addConverter(new StringToIpPortConverter());
conversionService.addConverter(new IpPortToStringConverter());
// 포맷터 등록
conversionService.addFormatter(new MyNumberFormatter());
// 컨버터 사용
IpPort result = conversionService.convert("127.0.0.1:8080", IpPort.class);
assertThat(result).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);
}
}
9. 포맷터 적용하기
- 웹 애플리케이션에 포맷터 적용
1) Webconfig - 수정
- 둘의 기능이 겹칠 경우 컨버터가 포맷팅보다 우선순위가 먼저 적용되므로 컨버터를 주석처리 진행
package hello.typeconverter;
@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());
}
}
2) 실행
(1) 객체 -> 문자
- localhost:8080/converter-view로 접속해보면 10,000으로 포맷팅이 된것을 확인할 수 있음
(2) 문자 -> 객체
- localhost:8080/hello-v2?data=10,000로 data에 파라미터 값으로 10,000을 넣어서 접속해보면 로그에 10000으로 변환된 것을 확인할 수 있음
10. 스프링이 제공하는 기본 포맷터
- Formatter 인터페이스의 구현 클래스를 찾아보면 수 많은 날짜나 시간 관련 포맷터가 제공되는 것을 확인할 수 있음
- 그러나 포맷터는 기본 형식이 지정되어 있기 때문에 객체의 각 필드마다 다른 형시으로 포맷을 지정하기 어려운데 애노테이션 기반으로 원하는 형식을 지정해서 사용할 수 있는 매우 유용한 포맷터 2가지를 기본으로 제공함
1) 사용해보기
- 각 코드들을 작성 후 접속해보면 지정한 포맷으로 출력되는 것을 확인할 수 있음
- 자세한 사용법은 @NumberFormat, @DateTimeFormat을 검색해보거나 공식 문서를 확인
https://docs.spring.io/spring-framework/reference/core/validation/format.html#format-CustomFormatAnnotations
(1) FormatterController
- @NumberFormat : 숫자 관련 형식 지정 포맷터 사용(NumberFormatAnnotationFormatterFactory)
- @DateTimeFormat : 날짜 관련 형식 지정 포맷터 사용, (Jsr310DateTimeFormatAnnotationFormatterFactory)
- 패턴으로 지정되어 있기 때문에 해당 패턴에 맞으면 String -> 객체, 객체 -> String으로 변환이 가능함
package hello.typeconverter.controller;
@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";
}
// @ModelAttribute 는 자동으로 model 에 값을 넣어줌
@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;
}
}
(2) 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>
(3) 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>
** 주의
- 메시지 컨버터(HttpMessageConverter)에는 컨버젼 서비스가 적용되지 않음
- 특히 객체를 JSON으로 변환할 때 메시지 컨버터를 사용하면서 오해가 생기는데, 컨버전 서비스와 전혀 관계가 없음
- HttpMessageConverter의 역할은 HTTP 메시지 바디의 내용을 객체로 변환하거나 객체를 HTTP 메시지 바디에 입력하는 것인데, JSON을 객체로 변환하는 메시지 컨버터는 내부에서 Jackson 같은 라이브러리를 사용함
- 객체를 JSON으로 변환한다면 그결과는 라이브러리에 달린 것이므로 JSON 결과로 만들어지는 숫자나 날짜 포맷을 변경하고 싶으면 해당 라이브러리가 제공하는 설정을 통해서 포맷을 지정해야함
- 컨버전 서비스는 @RequestParam, @ModelAttribute, @PathVariable, 뷰 템플릿 등에서 사용 가능함