Data binding 추상화
Spring에서 말하는 데이터 바인딩은 데이터 동적 변환이다.
입력한 값을 도메인 모델에 맞춰 자동으로 변환 후 할당하는 것을 말한다.
쉬운 예시를 들어보면, 사용자가 문자열 ”2020-01-16”을 넘겼는데 날짜 타입인 Date
로 변환하고 User
클래스를 생성하여 registerDate라는 이름을 가진 필드에 넣어주는 것이다.
PropertyEditor
PropertyEditor
는 Spring이 데이터 바인딩을 위해 지원하는 인터페이스 중 하나이다.
이 인터페이스의 구현체는 PropertyEditorSupport
가 있다.
이 구현체를 사용할 땐 아래의 단점에 주의하는 것이 좋다.
- Thread unsafe한 구조이다.
- String, Object로만 변환이 가능하다.
PropertyEditorSupport
내부에는 Object
타입의 필드 value가 있다.
데이터가 들어오면 value에 그 값을 저장하고 필요할 때 꺼내오는 식이다.
게다가 여러 스레드에서 동시에 데이터 접근이 가능하기 때문에 Singleton Bean으로는 절대 사용해서는 안 된다.
이렇게 상태 정보를 저장하고 사용하는 유형을 stateful 이라고 한다.
stateful은 저장한 값 기반으로 로직을 수행하는 만큼 thread unsafe라는 특징이 따라오게 되어있다.
Spring boot 프로젝트를 생성하고 코드를 작성해보자.
User
- 데이터를 저장할 POJO
- setter, getter를 작성하기 귀찮아서 롬복을 사용했다.
@Setter
@Getter
public class User {
private int id;
private String name;
private int age;
User(int id) {
this.id = id;
}
}
UserPropertyEditor
- 구현체
PropertyEditorSupport
를 상속받아 만들었다.- getAsText로 받아오고 setAsText로 저장한다.
- getValue, setValue로 부모 클래스의 필드(value)에 값을 저장한다.
- 구현체
public class UserPropertyEditor extends PropertyEditorSupport {
@Override
public String getAsText() {
int id = ((User) getValue()).getId();
return String.valueOf(id);
}
@Override
public void setAsText(String text) throws IllegalArgumentException {
setValue(new User(Integer.parseInt(text)));
}
}
UserController
- User 정보를 http request로 받아올 컨트롤러이다.
- Webmvc가 필요하니 spring-boot-starter-web를 종속성에 추가해준다.
@InitBinder
는 바인더를 등록할 때 사용하는 annotation이다.
@RestController
public class UserController {
@InitBinder
public void init(WebDataBinder binder) {
binder.registerCustomEditor(User.class, new UserPropertyEditor());
}
@GetMapping("/id/{user}")
public String getUser(@PathVariable User user) {
System.out.println(user);
return String.valueOf(user.getId());
}
}
UserControllerTest
- 서버 열고 url 치면서 테스트하기 번거로우니 junit을 활용한다.
- Spring boot 최신 버전(v2.2.2)은 junit 구조가 달라서 아래의 코드는 읽지 못할 수 있다. 2.0.5 버전 정도를 사용하면 괜찮다.
- get(), status(), content()을 import 할 수 있는 패키지가 너무 많아서 참고할 목적으로 사용한 static import 패키지도 써두었다.
- /user/1 을 get request로 보내면 response로 200 코드와 1이라는 문자열을 받아야 한다.
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 UserControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void getTest() throws Exception{
mockMvc.perform(get("/user/1"))
.andExpect(status().isOk())
.andExpect(content().string(“1”));
}
}
결과는 당연히 성공이다.
/user/1에서 1을 User
클래스의 id 필드에 넣어서 객체를 생성했다.
Converter
PropertyEditorSupport
을 활용하여 데이터 바인딩을 하는 방법은 stateful하다는 단점이 있었다.
그 이유로 stateless한 새로운 방법이 개발되었다.
위에서 작성한 코드를 약간 수정한다.
UserController
@InitBinder
는 사용하지 않으므로 삭제한다.
@RestController
public class UserController {
// @InitBinder
// public void init(WebDataBinder binder) {
// binder.registerCustomEditor(User.class, new UserPropertyEditor());
// }
@GetMapping("/id/{user}")
public String getUser(@PathVariable User user) {
System.out.println(user);
return String.valueOf(user.getId());
}
}
UserConverter
Converter
인터페이스를 상속받아 구현한다.StringToEventConverter
에서 상속받은 인터페이스의 의 제네릭 타입은 <String, User>인데, String -> User로 변환하겠다는 의미이다.- 두 이너클래스 중 위의 클래스를 bean으로 만들었다.
public class UserConverter {
@Component
public static class StringToUserConverter implements Converter<String, User> {
@Override
public User convert(String s) {
return new User(Integer.parseInt(s));
}
}
public static class UserToStringConverter implements Converter<User, String> {
@Override
public String convert(User event) {
return String.valueOf(event.getId());
}
}
}
bean으로 만들지 않으려면
@Component
를 제거하고 아래의 코드를 추가로 작성한다.
WebConfiguration
- WebMVC를 설정하는 파일이다.
- addFormatters() 를 Override하고 addConverter로 등록한다.
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new UserConverter.StringToEventConverter());
}
}
두 방법 전부 실행해보면 데이터가 잘 변환되고 있음을 확인할 수 있다.
stateless도 장점이지만 구현도 굉장히 간편해졌다.
Formatter
Converter는 A를 B로 변환하고 싶다면 제네릭을 <A, B>로 작성해야 동작한다.
Formatter는 양방향으로 작용할 수 있는 두 개의 메소드를 제공하여 조금 더 유연하다.
직접 코드를 작성해보자.
UserFormater
Formatter
인터페이스를 상속받고 변환하고자 하는 클래스를 제네릭으로 기입한다.- parse, print를 상속받아 구현해야 한다.
public class UserFormatter implements Formatter<User> {
@Override
public User parse(String s, Locale locale) throws ParseException {
return new User(Integer.*parseInt*(s));
}
@Override
public String print(User user, Locale locale) {
return String.*valueOf*(user.getId());
}
}
WebConfiguration
- addFormatter로 등록한다.
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
// registry.addConverter(new UserConverter.StringToUserConverter());
registry.addFormatter(new UserFormatter());
}
}
결과는 물론 성공이다.
여기서 이상한 점이 하나 있는데, converter를 등록할 때도 addFormatters를 상속받아 구현했다.
잘 작동하는 이유는 Converter의 등록을 관리하는 ConverterRegistry
가FormatterRegistry
를 상속해서 만들었기 때문이다.
FormatterRegistry
는 DefaultFormattingConversionService
를 구현해서 만들었다.
DefaultFormattingConversionService
도 당연히 데이터 바인딩을 수행할 수 있다.
Spring boot에서는 ConversionService
를 @Autowired
로 주입받은 후 convert 메소드를 사용하면 된다.
conversionService.getClass() 를 콘솔에 찍어보면 application에 등록된 모든 converter, formatter 등을 확인해볼 수 있다.
작성한 전체 소스 코드는 아래에서 확인할 수 있다.
https://github.com/dhmin5693/Spring-core-study/tree/master/DataBinding
'Java > Spring framework' 카테고리의 다른 글
Spring framework core (12) - Spring AOP (0) | 2020.01.24 |
---|---|
Spring framework core (11) - SpEL (0) | 2020.01.24 |
Spring framework core (9) - Validation (0) | 2020.01.15 |
Spring framework core (8) - Resource (0) | 2020.01.15 |
Spring framework core (7) - ResourceLoader (0) | 2020.01.14 |