[Spring] 데이터 바인딩 - PropertyEditor, Converter 그리고 Formatter
[Spring] 데이터 바인딩 - PropertyEditor, Converter 그리고 Formatter
1. Data Binding(데이터 바인딩)의 정의
📝 데이터 바인딩
웹에서 사용자가 입력하여 전달되는 값은 주로 문자열이며, 웹 어플리케이션에서는 이 문자열을 도메인 객체의 프로퍼티 타입(int, date, boolean, ... 또는 도메인 객체 타입 그 자체)으로 변환이 필요하다.
이렇게 사용자가 입력한 문자열 값을 프로퍼티 타입에 맞춰 변환하여 할당하는 것을 데이터 바인딩이라 한다.
2. Spring의 데이터 바인딩
Spring은 데이터 바인딩 기능을 여러 인터페이스로 추상화하여 제공한다.
데이터 바인딩 인터페이스는 주로 웹 MVC에서 사용하지만 web에 국한되어 특화된 기능이 아니라 여러 곳에서 사용되는 spring의 핵심 기술 중 하나이다.
데이터 바인딩 인터페이스 PropertyEditor, Converter, Formatter의 사용 방법에 대해 알아보자.
예제에서 사용할 도메인, 컨트롤러, 테스트 클래스는 다음과 같다.
도메인 클래스
public class Event {
Integer id;
String title;
public Event(Integer id) {
this.id = id;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
@Override
public String toString() {
return "Event{" +
"id=" + id +
", title='" + title + '\'' +
'}';
}
}
컨트롤러
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class EventController {
// Get 요청을 /event/이벤트id로 받는다.
@GetMapping("/event/{event}")
// @PathVariable로 {event}에 해당하는 부분을 Event 도메인 객체로 받는다.
// => 즉 사용자가 입력한 숫자(이벤트id)를 Event 타입으로 변환해야한다.
public String getEvent(@PathVariable Event event) {
// 변환된 Event 타입을 사용해서 코딩...
System.out.println(event);
return event.getId().toString();
}
}
테스트 클래스
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@WebMvcTest
public class EventControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void getTest() throws Exception {
mockMvc.perform(get("/event/1")) // GET /event/1이라고 요청을 보내면
.andExpect(status().isOk()) // HTTP 응답 200
.andExpect(content().string("1")); // 반환값은 1
}
}
'/event/이벤트id' 형식으로 GET 요청을 보내면 PropertyEditor, Converter, Formatter를 사용해서 이벤트 id를 Event 타입으로 변환할 것이다.
3. PropertyEditor 인터페이스
📝 PropertyEditor의 특징
- Spring이 제공하는 DataBinder 인터페이스를 통해 사용됨
- Spring 3 이전까지 DataBinder가 변환 작업에 사용한 인터페이스
- 값(상태 정보)을 저장하고 있어 thread-safe하지 않음
- 일반적인 싱글톤 scope 빈으로 등록해서 사용할 수 없음
- Object - String간의 변환만 할 수 있어 사용 범위가 제한적
1) PropertyEditor 구현
import java.beans.PropertyEditorSupport;
// implements PropertyEditor를 해도 되지만 구현해야할 메소드가 굉장히 많다.
// 따라서 extends PropertyEditorSupport로 필요한 메소드만 선택해서 구현한다.
// PropertyEditorSupport를 상속하면 보통 getAsText(), setAsText()를 override함
public class EventEditor extends PropertyEditorSupport {
// 특히 본 예제에서는 text(문자열)을 Event로 변환해야하므로 setAsText()만 구현하면 된다.
// 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();
}
}
PropertyEditor 인터페이스를 직접 구현해도 되지만 구현해야하는 메소드가 굉장히 많기 때문에 PropertyEditorSupport를 상속받으면 필요한 메소드만 선택해서 구현할 수 있다.
(이는 서블릿 프로그래밍에서 Servlet 인터페이스를 구현하는 것 보다 GenericServlet이나 HttpServlet을 상속하는 것과 유사하다.)
getAsText()와 setAsText() 두개를 override하면 되는데, getAsText()는 객체를 String으로, setAsText()는 String을 객체로 변환한다.
현재 예제의 경우 text를 Event로 변환해야 하므로 setAsText()만 override하면 되지만 구현 방식을 간략히 알 수 있도록
getAsText()도 함께 override하였다.
🚨 PropertyEditor의 구현체는 빈으로 등록해서 사용하지 않는다.
PropertyEditor의 getValue(), setValue() 메소드로 공유되는 값은 PropertyEditor가 갖고있는 값이다. 이 값은 서로 다른 thread끼리 공유되기 때문에 thread-safe하지 않다. 따라서 PropertyEditor의 구현체들을 빈으로 등록해서 사용하면 안된다.
이는 가령 1번 회원이 5번 회원의 정보를 변경한다던지 하는 치명적인 문제를 야기할 수 있다.
굳이 빈으로 등록하려면 한 쓰레드 내에서만 유효한 thread scope의 빈으로 등록해서 사용할 순 있으나 안전성을 위해 가능한 빈으로 등록하지 않는게 좋다.
2) 컨트롤러에 등록
구현한 PropertyEditor를 사용하기 위해 EventController에 등록한다.
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class EventController {
// 컨트롤러에서 사용할 바인더 등록
@InitBinder
public void init(WebDataBinder webDataBinder) {
// WebDataBinder에 Event 클래스 타입을 처리할 EventEditor 바인더 등록
webDataBinder.registerCustomEditor(Event.class, new EventEditor());
}
// Get 요청을 /event/이벤트id로 받는다.
@GetMapping("/event/{event}")
// @PathVariable로 {event}에 해당하는 부분을 Event 도메인 객체로 받는다.
// => 즉 사용자가 입력한 숫자(이벤트id)를 Event 타입으로 변환해야한다.
public String getEvent(@PathVariable Event event) {
// 변환된 Event 타입을 사용해서 코딩...
System.out.println(event);
return event.getId().toString();
}
}
@InitBinder 애노테이션으로 컨트롤러에서 사용할 바인더를 등록할 수 있다.
예제 코드와 같이 EventController에 EventEditor를 사용하도록 정의한다.
바인더를 등록할때 사용하는 WebDataBinder는 DataBinder의 구현체이다.
이렇게 PropertyEditor는 DataBinder를 통해 사용된다.
이제 컨트롤러가 요청을 처리하기 전에 PropertyEditor의 구현체인 EventEditor를 사용해서 /event/1과 같이 요청이 들어오면 문자열 1을 숫자로 변환한 뒤 Event 객체로 바꾸는 작업이 이루어진다.
3) 테스트
실행 결과
4. Converter 인터페이스
📝 Converter의 특징
- Spring3 부터 추가됨
- 참조 타입끼리 변환 가능한 일반적인(general) 기능의 변환기
- Spring이 제공하는 ConversionService 인터페이스를 통해 사용됨
- 값(상태 정보)을 저장하지 않으므로 thread-safe함
- 빈으로 등록해서 사용할 수 있음
1) Converter 구현
import org.springframework.core.convert.converter.Converter;
public class EventConverter {
// Converter<Source, Target>
public static class StringToEventConverter implements Converter<String, Event> {
@Override
public Event convert(String s) {
return new Event(Integer.parseInt(s));
}
}
public static class EventToStringConverter implements Converter<Event, String> {
@Override
public String convert(Event event) {
return event.getId().toString();
}
}
}
Converter 인터페이스를 implements하여 구현한다.
Converter는 두 개의 generic type을 받는데 첫 번째는 source 타입, 두 번째는 target 타입이다.
현재 예제의 경우 text를 Event로 변환해야 하므로 Converter<String, Event>만 구현하면 된다.
이렇게 만든 StringToEventConverter, EventToStringConverter가 PropertyEditor 예제의 EventEditor와 동일한 역할을 하게 된다.
2) Converter 사용
① Spring Web MVC만 사용하는 경우 - 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 EventConverter.StringToEventConverter());
}
}
Web 자바 config에서 addFormatters를 override하고 FormatterRegistry의 addConverter()로 등록한다.
레지스트리에 등록한 Converter는 모든 Controller에 대해 동작하게 된다.
실행 결과
②Spring Boot를 사용하는 경우 - Converter를 빈으로 등록
Spring boot는 Converter 빈을 찾아 자동으로 ConversionService에 등록해준다.
따라서 web config로 레지스트리에 직접 등록할 필요가 없다.
즉 Converter를 사용하기 위해 spring web mvc 설정을 새로 만들 필요가 없다.
EventConverter에 @Component를 붙여 빈으로 만들어주기만 하면 된다.
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
public class EventConverter {
@Component
// Converter<Source, Target>
public static class StringToEventConverter implements Converter<String, Event> {
@Override
public Event convert(String s) {
return new Event(Integer.parseInt(s));
}
}
@Component
public static class EventToStringConverter implements Converter<Event, String> {
@Override
public String convert(Event event) {
return event.getId().toString();
}
}
}
WebConfig에서 레지스트리에 등록하는 부분을 지우고 다시 테스트를 실행해보자.
실행 결과
4. Formatter 인터페이스
📝 Formatter의 특징
- Spring3 부터 추가됨
- PropertyEditor의 대체제
- Object - String 간 변환을 담당하는 web 특화 인터페이스
- Spring이 제공하는 ConversionService 인터페이스를 통해 사용됨
- 값(상태 정보)을 저장하지 않으므로 thread-safe함
- 빈으로 등록해서 사용할 수 있음
- 문자열을 Locale에 따라 다국화 처리 하는 기능 제공 (Optional)
1) Formatter 구현
import org.springframework.format.Formatter;
import org.springframework.stereotype.Component;
import java.text.ParseException;
import java.util.Locale;
@Component
public class EventFormatter implements Formatter<Event> {
@Override
public Event parse(String text, Locale locale) throws ParseException {
return new Event(Integer.parseInt(text));
}
@Override
public String print(Event object, Locale locale) {
return object.getId().toString();
}
}
Formatter 인터페이스를 implements하여 구현한다. 제네릭 타입으로 이 Formatter로 처리할 타입을 지정한다.
parse()와 print() 두 메소드를 구현해야 한다.
parse()는 PropertyEditor의 setAsText(), print()는 getAsText()와 유사하다.
2) Formatter 사용
사용 방법은 Convertet와 동일하다.
① Spring Web MVC만 사용하는 경우 - 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.addFormatter(new EventFormatter());
}
}
실행 결과
②Spring Boot를 사용하는 경우 - Formatter를 빈으로 등록
마찬가지로 EventFormatter에 @Component를 붙여 빈으로 만들어주기만 하면 된다.
import org.springframework.format.Formatter;
import org.springframework.stereotype.Component;
import java.text.ParseException;
import java.util.Locale;
@Component
public class EventFormatter implements Formatter<Event> {
@Override
public Event parse(String text, Locale locale) throws ParseException {
System.out.println("EventFormatter");
return new Event(Integer.parseInt(text));
}
@Override
public String print(Event object, Locale locale) {
return object.getId().toString();
}
}
실행 결과
5. ConversionService
PropertyEditor가 DataBinder를 통해 사용된다면 Converter와 Formatter는 ConversionService를 통해 사용된다.
FormatterRegistry의 addFormatter()로 등록한 Converter와 Formatter는 ConversionService에 등록되어 실제 변환 작업을 하게 된다.
Spring이 제공하는 여러가지 ConversionService 구현체 중 DefaultFormattingConversionService이 자주 사용된다.
DefaultFormattingConversionService는 위 그림과 같이 ConversionService와 FormatterRegistry, ConverterRegistry를 구현하여 해당 인터페이스들의 기능을 포함한다.
또한 여러 기본 Converter와 Formatter가 등록돼있다.
기본적으로 Converter를 레지스트리에 등록할때는 ConverterRegistry에, Formatter는 FormatterRegistry에 등록해서 사용하지만 FormatterRegistry는 ConverterRegistry를 상속받기 때문에 예제 코드에서와 같이 FormatterRegistry에 Converter를 등록할 수 있다.
Spring Boot - WebConversionService
Spring Boot를 사용하는 웹 어플리케이션의 경우에는 DefaultFormattingConversionService를 상속하여 만든 WebConversionService를 빈으로 등록해준다.
참고로 ConversionService에 등록돼있는 Converter, Formatter들을 확인하려면 ConversionService 인스턴스를 출력해보면 된다.
References
'Spring Framework > Spring Core' 카테고리의 다른 글
[Spring] 의존성 주입 애노테이션 정리 - @Autowired, @Resource, @Inject (0) | 2021.04.22 |
---|---|
[Spring] 스프링 XML 설정 파일 작성 방법 정리 (0) | 2021.04.22 |
Servlet에서 스프링 ApplicationContext 사용하기 (0) | 2021.04.22 |
[Spring] SpEL - Spring Expression Language (0) | 2021.04.22 |
[Spring] Validation 추상화 (0) | 2021.04.22 |
[Spring] Resource 추상화 (0) | 2021.04.22 |
[Spring] ResourceLoader로 텍스트 파일 출력하기 (Java 11) (0) | 2021.04.22 |
[Spring] ApplicationEventPublisher를 이용한 이벤트 프로그래밍 (0) | 2021.04.21 |