포맷터 - Formatter
Converter는 입력과 출력 타입에 제한이 없는, 범용 타입 변환 기능을 제공한다.
이번에는 일반적인 웹 애플리케이션 환경을 생각해보자.
불린 타입을 숫자로 바꾸는 것 같은 범용 기능 보다는
개발자 입장에서는 문자를 다른 타입으로 변환하거나,다른 타입을 문자로 변환하는 상황이 대부분이다.
앞서 살펴본 예제들을 떠올려 보면 문자를 다른 객체로 변환하거나 객체를 문자로 변환하는 일이 대부분이다.
애플리케이션에서 객체를 문자로, 문자를 객체로 변환하는 예시
화면에 숫자를 출력해야 하는데, Integer String 출력 시점에 숫자 1000 문자 "1,000" 이렇게 1000 단위에 쉼표를 넣어서
출력하거나, 또는 "1,000" 라는 문자를 1000 이라는 숫자로 변경해야 한다.
날짜 객체를 문자인 "2021-01-01 10:50:11" 와 같이 출력하거나 또는 그 반대의 상황
(위의 두가지 경우가 되게 빈번한 상황이다.)
Locale
여기에 추가로 날짜 숫자의 표현 방법은 Locale 현지화 정보가 사용될 수 있다.
이렇게 객체를 특정한 포멧에 맞추어 문자로 출력하거나 또는 그 반대의 역할을 하는 것에 특화된 기능이
바로 포맷터( Formatter )이다. 포맷터는 컨버터의 특별한 버전으로 이해하면 된다
Converter vs Formatter
Converter 는 범용(객체 --> 객체) (컨버터를 만들때 primitive 타입은 사용할수없음)
Formatter는 문자에 특화 (객체 --> 문자, 문자 --> 객체) + 현지화(Locale)
(Converter 의 특별한 버전)
포매터는 객체 <-> 문자로 특화되어있다.
(반드시 객체 <-> 문자 만 가능하다 , Integer <-> Boolean 이런거는 컨버터에서 하는것
포매터는 Integer <-> String 이런식인데 , String으로 변환시킬때 Locale까지 판단해서 복잡한 변환을 좀더 편리하게 할 수 있게끔 제공해주는 클래스,어노테이션,기본 제공 포매터들이 많다.)
포맷터 - Formatter 만들기
포맷터( Formatter )는 객체를 문자로 변경하고, 문자를 객체로 변경하는 두 가지 기능을 모두 수행한다.
public interface Formatter<T> extends Printer<T>, Parser<T> {
}
Formatter는 Printer, Parser라는 인터페이스를 구현한다.
@FunctionalInterface
public interface Printer<T> {
/**
* Print the object of type T for display.
*/
String print(T object, Locale locale);
}
Printer의 print() 객체를 문자로 변경한다.
@FunctionalInterface
public interface Parser<T> {
/**
* Parse a text String to produce a T.
*/
T parse(String text, Locale locale) throws ParseException;
}
Parser의 parse() 문자를 객체로 변경한다
Formatter 만들어보기
숫자 1000 을 문자 "1,000" 으로 그러니까, 1000 단위로 쉼표가 들어가는 포맷을 적용해보자.
그리고 그 반대도 처리해주는 포맷터를 만들어보자.
MyNumberFormatter
@Slf4j
public class MyNumberFormatter implements Formatter<Number> {// the type of object this Formatter formats, 변환할 객체를 넣어주면 된다.(어떤타입을 문자로바꿀것인지)
// Number는 Integer,Long,Double등 숫자관련 객체들은 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); //java.text의 NumberFormat 이미 "1,000" -> 1000으로 바꿔주는 기능은 만들어져있다.
//locale정보를 받아서 숫자포맷을 꺼낸후
return format.parse(text); //parse()를 이용하여 숫자 객체로 변환시켜준다.
}
@Override
public String print(Number object, Locale locale) {
log.info("object = {} , locale = {} ", object, locale);
//오브젝트를 문자로 바꿔야함
NumberFormat numberFormat = NumberFormat.getInstance(locale); // locale 정보를 주고 NumberFormat을 꺼내온다.
return numberFormat.format(object); //format()이라는 메소드를 이용하여 숫자객체를 문자로 바꾼다.
}
}
"1,000" 처럼 숫자 중간의 쉼표를 적용하려면 자바가 기본으로 제공하는 NumberFormat 객체를 사용하면 된다.
이 객체는 Locale 정보를 활용해서 나라별로 다른 숫자 포맷을 만들어준다.
parse() 를 사용해서 문자를 숫자로 변환한다.
참고로 Number 타입은 Integer , Long 과 같은 숫자 타입의 부모 클래스이다.
print() 를 사용해서 객체를 문자로 변환한다
잘 동작하는지 테스트 코드를 만들어보자
MyNumberFormatterTest
class MyNumberFormatterTest {
MyNumberFormatter formatter = new MyNumberFormatter();
@Test
void parse() throws ParseException {
Number number = formatter.parse("1,000", Locale.KOREA);
assertThat(number).isEqualTo(1000L); //Number객체지만 실제 내부에서는 Long타입으로 만들어지는것같다. 그러므로 1000L로 테스트
}
@Test
void print() {
String result = formatter.print(1000, Locale.KOREA);
assertThat(result).isEqualTo("1,000");
}
}
parse() 의 결과가 Long 이기 때문에 isEqualTo(1000L) 을 통해 비교할 때 마지막에 L 을 넣어주어야 한다.
참고
스프링은 용도에 따라 다양한 방식의 포맷터를 제공한다.
Formatter 포맷터
AnnotationFormatterFactory 필드의 타입이나 애노테이션 정보를 활용할 수 있는 포맷터
공식문서참고 (https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#format)
포맷터를 지원하는 컨버전 서비스
컨버전 서비스에는 컨버터만 등록할 수 있고, 포맷터를 등록할 수 는 없다.
그런데 생각해보면 포맷터는 객체 --> 문자, 문자 --> 객체로 변환하는 특별한 컨버터일 뿐이다.
포맷터를 지원하는 컨버전 서비스를 사용하면 컨버전 서비스에 포맷터를 추가할 수 있다.
내부에서 어댑터 패턴을 사용해서 Formatter가 Converter처럼 동작하도록 지원한다.
FormattingConversionService는 포맷터를 지원하는 컨버전 서비스이다.
DefaultFormattingConversionService는
FormattingConversionService에 기본적인 통화, 숫자 관련 몇가지 기본 포맷터를 추가해서 제공한다.
테스트 (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);
}
}
DefaultFormattingConversionService 상속 관계
FormattingConversionService는 ConversionService 관련 기능을 상속받기 때문에
결과적으로 컨버터도 포맷터도 모두 등록할 수 있다.
그리고 사용할 때는 ConversionService가 제공하는 convert를 사용하면 된다.
추가로 스프링 부트는
DefaultFormattingConversionService를 상속 받은 WebConversionService를 내부에서 사용한다.
포맷터 적용하기
포맷터를 웹 애플리케이션에 적용해보자.
WebConfig - 수정
@Configuration
public class WebConfig implements WebMvcConfigurer {
// Ctrl + O를 이용해서 오버라이드할 메소드를 찾아볼 수 있다.
@Override
public void addFormatters(FormatterRegistry registry) { //컨버터 등록은 addFormatters()를 이용하면된다.
//우선순위 떄문에 주석처리 (아래 등록한 MyNumberFormatter도 Number -> 문자, 문자 -> Number 를 변환해주는 포매터이므로)
// 포매터와 컨버터가 같은 동작을 할떄, 컨버터가 더 높은 우선순위를 가진다 (포매터 테스트를 위해 주석처리를 한다)
// registry.addConverter(new StringToIntegerConverter());
// registry.addConverter(new IntegerToStringConverter());
//컨버터 등록 WebConversionService 에 컨버터가 등록된다.
registry.addConverter(new StringToIpPortConverter());
registry.addConverter(new IpPortToStringConverter());
//포매터 등록) WebMvcConfigurer 를 implements 하기만하고 등록메소드만 이용하면 스프링이 내부에 있는
// DefaultFormattingConversionService 를 상속 받은 WebConversionService에 포매터를 등록시켜준다.
registry.addFormatter(new MyNumberFormatter());
}
}
포매터 실행 확인용 컨트롤러
@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";
}
모델에 number이라는 속성명으로 10000이라는 속성값을 보낸다 여기서 10000은 int이자 Integer이다.
(int와 Integer는 같다고 봐도되고, 같은 컨버터를 사용한다)
뷰 템플릿
<li>${number}: <span th:text="${number}" ></span></li>
<li>${{number}}: <span th:text="${{number}}" ></span></li>
여기서 ${{number}}로 인해 컨버터 또는 포매터가 동작할텐데
우선순위는 다음과 같다.
1. 새로 등록된 컨버터
2. 새로 등록된 포매터
3. 스프링이 기본으로 등록한것들
WebConfig를 보면 새로 등록된 컨버터중에는 Integer를 변환 시켜주는 컨버터는 없었다. Integer -> ?
그러면 그다음 등록된 포매터를 살펴본다.
그때 Number -> String으로 바꿔주는 MyNumberFormatter를 찾게되고 해당 포매터를 실행하게된다.
결과
Source의 타입이 Integer(Number)이였으므로 MyNumberFormatter가 동작한 모습
포매터 실행 확인용 컨트롤러2
@GetMapping("hello-v2")
public String helloV2(@RequestParam Integer data) {
System.out.println("data = " + data);
return "ok";
}
TargetType이 Integer인 모습을 확인가능
결과
Source인 파라미터값은 항상 String으로 들어온다.
그렇다면
SourceType은 String
변환시켜야하는 TargetType은 Integer라면 어떤 컨버터,포매터가 동작할까?
Number <-> String으로 변환해주는 MyNumberFormatter가 동작하여 data가 1000으로 잘 변환된것을 확인할 수 있다.
스프링이 제공하는 기본 포맷터
스프링은 자바에서 기본으로 제공하는 타입들에 대해 수 많은 포맷터를 기본으로 제공한다.
IDE에서 Formatter 인터페이스의 구현 클래스를 찾아보면
수 많은 날짜나 시간 관련 포맷터가 제공되는 것을 확인할 수 있다.
그런데 포맷터는 기본 형식이 지정되어 있기 때문에, 객체의 각 필드마다 다른 형식으로 포맷을 지정하기는 어렵다.
스프링은 이런 문제를 해결하기 위해 애노테이션 기반으로 원하는 형식을 지정해서 사용할 수 있는
매우 유용한 포맷터 두 가지를 기본으로 제공한다.
@NumberFormat : 숫자 관련 형식 지정 포맷터 사용하는 어노테이션
(NumberFormatAnnotationFormatterFactory라는 포매터가 사용된다.)
@DateTimeFormat : 날짜 관련 형식 지정 포맷터 사용하는 어노테이션
(Jsr310DateTimeFormatAnnotationFormatterFactory라는 포매터가 사용된다.)
예제를 통해서 알아보자.
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";
}
//넘어온 파라미터값을 이용해서 각각의 필드가 포매터로 인해 포매팅이 되서 Form객체를 생성해줄수 있는지 체크하기위한 메소드
@PostMapping("/formatter/edit")
public String formatterEdit(@ModelAttribute Form form) {
return "formatter-view";
}
@Data
static class Form {
@NumberFormat(pattern = "###,###") //@NumberFormat 에 패턴을 지정해서, String <-> Integer 의 포매팅 패턴을 지정해준다.
private Integer number;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") //@DateTimeFormat 에 패턴을 지정해서, String <-> LocalDateTime 의 포매팅 패턴을 지정해준다.
private LocalDateTime localDateTime; // LocalDateTime 는 자바8의 날짜 객체
}
}
결과
0. 데이터
@Data
static class Form {
@NumberFormat(pattern = "###,###") //@NumberFormat 에 패턴을 지정해서, String <-> Integer 의 포매팅 패턴을 지정해준다.
private Integer number;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") //@DateTimeFormat 에 패턴을 지정해서, String <-> LocalDateTime 의 포매팅 패턴을 지정해준다.
private LocalDateTime localDateTime; // LocalDateTime 는 자바8의 날짜 객체
}
@NumberFormat , @DateTimeFormat을 사용하면 스프링에서 지정해놓은 포매터가 동작한다.
그때 패턴을 지정해서 어떤패턴으로 (해당 객체 <-> 문자 ) 포매팅할것인지 정할 수 있다.
1. API
@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";
}
form 객체를 생성후
필드의 I
nteger number에는 10000,
LocalDateTime localDateTime에는 현재시간이 저장되어있는 LocaldateTime객체를 저장한다.
1. View
<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>
th:filed에 값을 넣으면 자동으로 컨버터,포매터가 동작한다.
th:object로 넘어온 커맨드객체는 form객체이다.
Form 클래스를 살펴보면
number라는 필드에는 @NumberFormat가 사용되서
NumberFormatAnnotationFormatterFactory라는 포매터가 지정되어있고
localDateTime라는 필드에는 @DateTimeFormat가 사용되서
Jsr310DateTimeFormatAnnotationFormatterFactory라는 포매터가 지정되어있다.
그래서 각각 포매터가 실행되고, 어노테이션의 pattern 속성들을 이용해서 formatting이 진행된다.
1. 결과
각각의 필드가 자신들의 포매터로 인해 설정한 패턴대로 포매팅된것을 확인할 수 있다.
Integer -> String
LocalDateTime -> String
테스트2
2. API
//넘어온 파라미터값을 이용해서 각각의 필드가 포매터로 인해 포매팅이 되서 Form객체를 생성해줄수 있는지 체크하기위한 메소드
@PostMapping("/formatter/edit")
public String formatterEdit(@ModelAttribute Form form) {
return "formatter-view";
}
넘어오는 파라미터값은 다음과 같다.
2.view
<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>
${{...}}를 이용해서 컨버터,포매터를 사용한다.
2. 결과
String으로 들어온 "10,000"과
String으로 들어온 "2023-02-13 13:35:17" 이
각각의 포매터로 인해 Integer, LocalDateTime 객체로 변환되어 필드에 저장되고
Form 객체가 생성되어 form에 들어간다.
그런 form객체가 @ModelAttribute로 인해 자동으로 모델에 들어가 뷰로 넘겨진다.
그렇게 렌더링된 뷰이다. 중괄호가 하나쓰인 타임리프 문법에서는 컨버터,포매터가 동작하지않는다
그래서 객체의 toString()이 동작한 모습
SourceType,TargetType에 따라 컨버터,포매터가 동작하는것이 일반적인 동작방식이지만
애초에 각 필드자체에 어노테이션을 붙여서 포매터를 지정해놨기때문에 해당 포매터가 동작한다.
이렇게 날짜나 숫자같은 패턴 포맷이 상당히 많이 쓰이는데 그때 사용하면 유용하다.
@NumberFormat , @DateTimeFormat 의 자세한 사용법이 궁금한 분들은
다음 링크를 참고하거나 관련 애노테이션을 검색해보자.
정리
컨버터를 사용하든, 포맷터를 사용하든 등록 방법은 다르지만,
사용할 때는 컨버전 서비스를 통해서 일관성 있게 사용할 수 있다
(하지만 둘다 등록할 수 있는 Conversion이 스프링에 등록되어있으므로 WebMvcConfigurer를 구현하고
addFormatters() 안에서 registry에 컨버터와 포매터를 등록하면된다.)
주의!!!
메시지 컨버터( HttpMessageConverter )에는 컨버전 서비스가 적용되지 않는다.
특히 객체를 JSON으로 변환할 때 메시지 컨버터를 사용하면서 이 부분을 많이 오해하는데,
HttpMessageConverter의 역할은
HTTP 메시지 바디의 내용을 객체로 변환하거나 객체를 HTTP 메시지 바디에 입력하는 것이다.
예를 들어서 JSON을 객체로 변환하는 메시지 컨버터는 내부에서 Jackson 같은 라이브러리를 사용한다.
객체를 JSON으로 변환한다면 그 결과는 이 라이브러리에 달린 것이다.
따라서 JSON 결과로 만들어지는 숫자나 날짜 포맷을 변경하고 싶으면
해당 라이브러리가 제공하는 설정을 통해서 포맷을 지정해야 한다.
결과적으로 이것은 컨버전 서비스와 전혀 관계가 없다
컨버전 서비스는 @RequestParam , @ModelAttribute , @PathVariable , 뷰 템플릿 등에서 사용할 수 있다.
메시지컨버터는 body에 쓰여진 text,json같은 애들을 객체로 변환하거나, 객체,String같은 애들을 메시지 바디에 쓰는 일을 하고,
타입컨버터는 파라미터애들을 가지고 객체로 변환하거나 뷰템플릿에서 변환시켜줄때 사용
파라미터로 들어오는것은 @NumberFormat같은걸로 처리가능
메시지바디에 json으로 들어오는것은 @jsonformat으로 처리가능한것 같다.
'인프런 > 스프링 MVC 2편' 카테고리의 다른 글
끝) 파일업로드 (2) | 2023.02.13 |
---|---|
19) 스프링 타입 컨버터 (0) | 2023.02.12 |
18) API 예외처리 (3) (0) | 2023.02.12 |
17) API 예외 처리(2) (0) | 2023.02.10 |
16) API 예외 처리 (0) | 2023.02.09 |
댓글