- 문자를 숫자로 변환 or 숫자를 문자로 변환 같은 타입 변환
@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";
}
http://localhost:8080/hello-v2?data=10
- @RequestParam을 사용하면 문자 10을 Integer 타입의 숫자 10으로 편리하게 변환해준다.
- @ModelAttribute , @PathVariable도 마찬가지
→스프링이 중간에서 타입을 변환해주었기 때문
Converter
개발자가 직접 새로운 타입을 만들어서 변환
public interface Converter<S, T> {
T convert(S source);
}
ex) 문자로 "true" → Boolean 타입으로 받고 싶으면 String → Boolean 타입으로 변환되도록 컨버터 인터페이스를 만들어서 등록
StringToIntegerConverter
@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
IntegerToStringConverter
@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
사용자 정의 타입 컨버터
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) 의 결과가 참이 된다.
StringToIpPortConverter
@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);
}
}
String → IpPort
IpPortToStringConverter
@Slf4j
public class IpPortToStringConverter implements Converter<IpPort, String> {
@Override
public String convert(IpPort source) {
log.info("convert source={}", source);
return source.getIp() + ":" + source.getPort();
}
}
IpPort → String
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);
}
★ 인터페이스 분리 원칙 - ISP(Interface Segregation Principle)
▶ 인터페이스 분리 원칙은 클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다.
DefaultConversionService 는 다음 두 인터페이스를 구현한다.
- ConversionService : 컨버터 사용에 초점
- ConverterRegistry : 컨버터 등록에 초점
컨버터를 사용하는 클라이언트와 컨버터를 등록하고 관리하는 클라이언트의 관심사를 명확하게 분리
컨버터를 사용하는 클라이언트는 ConversionService 만 의존되며 컨버터를 어떻게 등록하고 관리하는지는 전혀 몰라도 된다.
스프링에 Converter 적용하기
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());
}
}
@GetMapping("/hello-v2")
public String helloV2(@RequestParam Integer data) {
System.out.println("data = " + data);
return "ok";
}
실행 시 직접 등록한 StringToIntegerConverter 가 작동한다.
@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";
}
실행 시 직접 등록한 StringToIpPortConverter 가 작동한다.
뷰 템플릿에 컨버터 적용하기
- 객체를 문자로 변환하는 작업을 확인
@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";
}
}
실행 시 직접 등록한 IntegerToStringConverter 가 작동한다.
실행 시 직접 등록한 IpPortToStringConverter 가 작동한다.
폼에 적용하기
@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";
}
@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 객체를 데이터를 전달하는 폼 객체로 사용
포맷터 - Formatter 소개
객체를 문자로, 문자를 객체로 변환하는 예
- 화면에 숫자를 출력해야 하는데, 출력 시점에 숫자 1000 문자 "1,000" 이렇게 1000 단위에 쉼표를 넣어서 출력하거나, 또는
문자"1,000" → 1000 로 변경해야 한다. - 날짜 객체를 문자인 "2021-01-01 10:50:11" 와 같이 출력하거나 또는 그 반대의 상황
Locale
- 여기에 추가로 날짜 숫자의 표현 방법은 Locale 현지화 정보가 사용될 수 있다.
포맷터( Formatter ): 객체를 특정한 포멧에 맞추어 문자로 출력하거나 또는 그 반대의 역할을 하는 것에 특화된 기능
Converter vs Formatter
- Converter 는 범용(객체 객체)
- Formatter 는 문자에 특화(객체 → 문자, 문자 → 객체) + 현지화(Locale)
Converter 의 특별한 버전
포맷터 - 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> {
}
@Slf4j
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);
}
@Override
public String print(Number object, Locale locale) {
log.info("object={}, locale={}", object, locale);
return NumberFormat.getInstance(locale).format(object);
}
}
- "1,000"처럼 숫자 중간에 쉼표 넣을 때 NumberFormat 객체 사용
- print() 를 사용해서 객체 → 문자
포맷터를 지원하는 컨버전 서비스
- DefaultFormattingConversionService는 FormattingConversionService 에 통화, 숫자 등 기본 포맷터를 추가 제공
@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());
}
}
우선순위는 컨버터가 우선하므로 포맷터가 적용되지 않고, 컨버터가 적용된다.
스프링이 제공하는 기본 포맷터
- @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";
}
@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;
}
}
<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>
결과
- ${form.number}: 10000
- ${{form.number}}: 10,000
- ${form.localDateTime}: 2021-01-01T00:00:00
- ${{form.localDateTime}}: 2021-01-01 00:00:00
◎ 참고 : 공식 메뉴얼
https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#format-
★ 정리
컨버터를 사용하든, 포맷터를 사용하든 등록 방법은 다르지만, 사용할 때는 컨버전 서비스를 통해서 일관성 있게 사용 가능
주의!
- HttpMessageConverter 의 역할은 HTTP 메시지 바디의 내용 → 객체 or 객체 → HTTP 메시지 바디에 입력
- "JSON → 객체" 담당 메시지 컨버터는 내부에서 Jackson 같은 라이브러리를 사용
- 결과적으로 컨버전 서비스와 전혀 관계가 없다.
컨버전 서비스는 @RequestParam , @ModelAttribute , @PathVariable , 뷰 템플릿 등에서 사용할 수 있다.
'Spring > MVC 2편' 카테고리의 다른 글
파일 업로드 (0) | 2023.08.08 |
---|---|
API 예외 처리 (0) | 2023.08.08 |
예외 처리 & 오류 페이지 (0) | 2023.08.07 |
로그인 처리2 - 필터, 인터셉터 (0) | 2023.08.07 |
로그인 처리1 - 쿠키, 세션 (0) | 2023.08.07 |