클라이언트로부터 요청이 들어왔을 때 요청 데이터를 바인딩해주는 요소들
➕ PropertyEditor, Converter, Formatter 추가
@RequestMapping("/board/{id}", method=RequestMethod.GET)
public String getBoard(@PathVariable(value="id") String id) {
...
}
@GetMapping("/board)
public String gerBoard(@RequestParam(name="id",required=true,defaultValue="") String id,
@RequestParam(name="name",required=false) String name) {
...
}
@PostMapping(value="/board")
public BaseResponse addPost(@RequestBody BoardInput input) {
...
}
@ResponseBody
@RequestMapping("/board")
public String modelAttributeV1(@ModelAttribute BoardData boardData) {
log.info("username={}, age={}", boardData.getUsername(), boardData.getAge());
...
}
@ResponseBody
@PostMapping("/board")
public HelloData getData(@RequestBody HelloData data) {
log.info("username={}, age={}", data.getUsername(), data.getAge());
...
}
✔️ 개발을 하다보면 타입을 변환해야 하는 경우가 상당히 많은데, Spring에서는 이렇게 필요한 타입으로 데이터 바인딩 해주는 여러 인터페이스를 제공함
✔️ 인터페이스 종류에는 PropertyEditor, Converter, Formatter가 있음
✔️ Spring이 제공하는 DataBinder 인터페이스를 통해 사용됨
✔️ Spring 3 이전까지 DataBinder가 변환 작업에 사용한 인터페이스
✔️ 값(상태 정보)을 저장하고 있어 동시성 문제가 있어서 타입을 변환할 때 마다 객체를 계속 생성해야 하는 단점이 있음
✔️ 위의 단점으로 인해 서로 다른 쓰레드끼리 값이 공유되므로 싱글톤 Scope 빈으로 등록하여 사용할 수 없음
✔️ Object - String 간의 변환만 가능
✔️ PropertyEditor 인터페이스를 상속해도 되지만, 구현해야할 메소드가 많아 PropertyEditorSupport 상속으로 필요한 메소드만 구현하는 것이 나음
예제
import java.beans.PropertyEditorSupport;
// implements PropertyEditor를 해도 되지만 구현해야할 메소드가 굉장히 많다.
// 따라서 extends PropertyEditorSupport로 필요한 메소드만 선택해서 구현한다.
// PropertyEditorSupport를 상속하면 보통 getAsText(), setAsText()를 override함
public class EventEditor extends PropertyEditorSupport {
// setAsText() : String -> Object
@Override
public void setAsText(String text) throws IllegalArgumentException {
// 사용자가 입력한 문자열을 int로 변환하여 Event 객체를 생성한 뒤 setValue() 호출
setValue(new Event(Integer.parseInt(text)));
}
// getAsText() : Object -> String
@Override
public String getAsText() {
Event event = (Event) getValue();
return event.getId().toString();
}
}
✔️ S(Source) 타입을 T(Target) 타입으로 변환할 수 있는 Generic한 변환기
✔️ Spring 3 부터 도입됨
✔️ Spring이 제공하는 ConversionService 인터페이스를 통해 사용 (ConversionService는 개별 컨버터를 모아두고 컨버터들을 묶어서 편리하게 사용할 수 있는 기능을 제공하는 인터페이스)
✔️ 값(상태 정보)를 저장하지 않으므로 동시성 문제가 발생하지 않음 (Thread-Safe)
✔️ Bean으로 등록하여 사용 가능
package org.springframework.core.convert.converter;
public interface Converter<S, T> {
T convert(S source);
}
public class BoardConverter {
public static class StringToBoardInputConverter implements Converter<String, BoardInput> {
@Override
public BoardInput convert(String source) {
return new BoardInput(Integer.parseInt(source));
}
}
public static class BoardInputToStringConverter implements Converter<BoardInput, String> {
@Override
public String convert(BoardInput source) {
return source.getId() + ";" + source.getName());
}
}
}
DefaultConversionService
는 스프링이 제공하는 ConversionService 인터페이스의 구현체인데, 이 것은 컨버터를 등록하는 기능과 사용하는 기능을 따로 제공한다.package org.springframework.core.convert;
import org.springframework.lang.Nullable;
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);
}
✔️ 컨버터를 실제로 스프링에 등록해서 사용하는 방법을 알아본다.
1. Web Config에 등록하는 방법
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new BoardConverter.StringToBoardInputConverter());
registry.addConverter(new BoardConverter.BoardInputToStringConverter());
}
}
- 참고로, 스프링에는 이미 여러 기본 컨버터들을 제공하는데, 이렇게 직접 컨버터를 추가하면
기본 컨버터보다 높은 우선 순위를 가진다.
RequestParamMethodArgumentResolver
에서 ConversionService
를 사용해서 타입을 변환한다.2. Converter를 빈으로 등록하는 방법 - Spring Boot 사용 시
@Component
public static class StringToBoardInputConverter implements Converter<String, BoardInput> {
@Override
public BoardInput convert(String source) {
return new BoardInput(Integer.parseInt(source));
}
}
⬆️ Convert를 적용하면, 스프링이 내부에서 사용하는 ConversionService에 해당 Converter들을 추가해줌
✔️ 참고 - 용도에 따라 다양한 방식의 컨버터를 사용 가능 (Spring에서 제공)
✔️ Thymeleaf(뷰 템플릿)은 뷰를 렌더링 시에 컨버터를 적용해서 렌더링 하는 방법을 지원한다.
@Controller
public class ConverterController {
@GetMapping("/converter-view")
public String converterView(Model model) {
model.addAttribute("member", 10000);
model.addAttribute("ipPort", new IpPort("127.0.0.1", 8000));
return "converter-view";
}
}
<!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>
✔️ 객체를 특정한 포맷에 맞추어 문자로 출력하거나 또는 그 반대의 역할을 하는 것에 특화된 기능이 바로 Fomatter이다. Converter의 특별한 버전이라고 보면 된다.
✔️ Spring 3부터 도입됨
✔️ Object - String 간 변환을 담당하는 Web에 특화된 인터페이스 -> 웹에서는 문자를 다른 타입으로 변환하거나, 다른 타입을 문자로 변환하는 상황이 대부분임
✔️ Spring이 제공하는 ConversionService 인터페이스를 통해 사용
✔️ 값(상태 정보)를 저장하지 않으므로 동시성 문제가 발생하지 않음 (Thread-Safe)
✔️ Bean으로 등록하여 사용 가능
✔️ 문자열을 Locale에 따라 다국화 처리 기능 제공 (Optional)
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> {
}
public class SampleFormatter implements Formatter<Number> {
@Override
//Number는 Interger나 Long과 같은 숫자 타입의 부모 클래스
public Number parse(String text, Locale locale) throws ParseException {
NumberFormat format = NumberFormat.getInstance(locale);
return format.parse(text);
}
@Override
public String print(Number object, Locale locale) {
return NumberFormat.getInstance(locale).format(object);
}
}
class SampleFormatterTest {
SampleFormatter formatter = new SampleFormatter();
@Test
void parse() throws ParseException {
Number result = formatter.parse("1,000", Locale.KOREA);
assertThat(result).isEqualTo(1000L); //parse()의 결과가 Long 이므로 비교 시 L 접미사를 넣어주어야 함
}
@Test
void print() {
String result = formatter.print(1000, Locale.KOREA);
assertThat(result).isEqualTo("1,000");
}
}
✔️ 참고 - 용도에 따라 다양한 Formatter 사용 가능 (Spring에서 제공)
✔️ FormattingConversionService는 Formatter를 지원하는 Conversion Service임
해당 서비스를 사용하면 내부에서 어댑터 패턴을 사용하여 Formatter가 Converter처럼 동작하도록 지원한다.
✔️ DefaultFormattingConversionService는 FormattingConversionService에 기본적인 통화, 숫자 관련 몇 가지 기본 Formatter를 추가해서 제공한다.
✔️ FormattingConversionService는 ConversionService 관련 기능을 상속받기 때문에 결과적으로 Converter 및 Formatter 둘 다 등록이 가능함
✔️ Spring Boot에서는 DefaultFormattingConversionService를 상속 받은 WebConversionService을 내부에서 사용함
1. Web Config에 등록하는 방법
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(new MyNumberFormatter());
}
}
2. Converter를 빈으로 등록하는 방법 - Spring Boot 사용 시
@Component
public class SampleFormatter implements Formatter<Number> {
@Override
//Number는 Interger나 Long과 같은 숫자 타입의 부모 클래스
public Number parse(String text, Locale locale) throws ParseException {
NumberFormat format = NumberFormat.getInstance(locale);
return format.parse(text);
}
@Override
public String print(Number object, Locale locale) {
return NumberFormat.getInstance(locale).format(object);
}
}
✔️ 객체의 각 필드마다 다른 형식으로 포맷을 지정하고 싶은 경우, 아래처럼 스프링이 제공하는 어노테이션 기반의 형식을 지정해서 유용하게 사용할 수 있음
@NumberFormat(pattern = "###,###")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Data
static class Form {
@NumberFormat(pattern = "###,###")
private Integer number;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime localDateTime;
}
✔️ 메시지 컨버터(HttpMessageConverter)에는 Conversion Service가 적용되지 않음
✔️ HttpMessageConverter의 역할은 HTTP 메시지 바디의 내용을 객체로 변환하거나 객체를 HTTP 메시지 바디에 입력하는 것이다. 예를 들어 Json을 객체로 변환할 때 Jackson 같은 라이브러리를 사용하는데, 이 때 포맷을 변경하고 싶을 때는 해당 라이브러리가 제공하는 설정을 통해 포맷을 지정하는 것이지, Conversion Service와는 무관함
✔️ Conversion Service는 @RequestParam, @ModelAttribute, @PathVariable, 뷰 템플릿 등에서 사용할 수 있다.
<참고 Reference>