(가장 고전적인 방식의 데이터 바인딩을 살펴보자)
main\java\me\jinmin\Event
package me.jinmin;
public class Event {
private Integer id;
private 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 + '\'' +
'}';
}
}
main\java\me\jinmin\EventController
package me.jinmin;
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 {
@GetMapping("/event/{evnet}")
public String getEvent(@PathVariable Event event){
System.out.println(event);
return event.getId().toString();
}
}
{event} : event의 id
들어온 숫자 타입의 id를 Event 타입(도메인 타입)으로 변환해서 스프링이 받아야한다.
❗Test (Spring boot WebMvc Test)
MockMvc : 웹 App.을 서버에 배포하지 않고 스프링 MVC 동작을 재현.
Test를 위한 필요 의존성
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
test\java\me\jinmin\EventControllerTest
package me.jinmin;
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.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@RunWith(SpringRunner.class)
@WebMvcTest
public class EventControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void getTest() throws Exception {
mockMvc.perform(get("/event/1"))
.andExpect(status().isOk())
.andExpect(content().string("1"));
}
}
결과 : 오류 ⇒ String을 Event 타입으로 변환하지 못했다.
main\java\me\jinmin\EventEditor
package me.jinmin;
import java.beans.PropertyEditorSupport;
public class EventEditor extends PropertyEditorSupport {
@Override
public String getAsText() {
Event event = (Event) getValue();
return event.getId().toString();
}
@Override
public void setAsText(String text) throws IllegalArgumentException {
setValue(new Event(Integer.parseInt(text));
}
}
공유하고 있는 Value는 PropertyEditor가 가지고 있는 값. ⇒ ❤(중요)이 값이 서로 다른 쓰레드에게 공유가 되고 State-Full하다 → Non-Thread-Safe 하다. → 구현체를 여러 쓰레드에 공유해서 쓰면 안된다.(=Bean으로 등록하면 안된다.)
그러면 빈으로 등록안하고 어떻게 사용할까? ⇒ EventController에서 DataBinder의 구현체를 구현하자.
package me.jinmin;
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.registerCustomEditor(Event.class, new EventEditor());
}
@GetMapping("/event/{evnet}")
public String getEvent(@PathVariable Event event){
System.out.println(event);
return event.getId().toString();
}
}
test\java\me\jinmin\EventControllerTest 구동 결과.
print:
Event{id=1, title='null'}
💜결론💜, Controller가 어떤 요청을 처리하기 전에 데이터 바인더
@InitBinder
public void init(WebDataBinder webDataBinder){
webDataBinder.registerCustomEditor(Event.class, new EventEditor());
}
메소드에 들어있는 PropertyEditor(WebDataBinder)를 사용해서 Editor로 부터 받아온 Value값 (=문자열로 들어온 1)을 숫자로 변환 후 Event 객체로 변환. ⇒ Test가 아무런 문제없이 실행됐다.
단점 : Object와 String 간의 변환만 가능(⇒ 대부분의 경우이기 때문에 조심스레 사용해왔다.)
Conveter : S(Source) 타입을 T(Target) 타입으로 변환하는 아주 일반적인 변환기
상태 정보 없음 = Stateless = Thread-Safe
Thread-Safe 하기 때문에 각 클래스를 빈으로 등록해도 되지만 ConverterRegistry(❤ 직접x, Spring Web MVC Configurer)에 등록하여 사용할 것.
\main\me\jinmin\EventController
package me.jinmin;
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 {
@GetMapping("/event/{event}")
public String getEvent(@PathVariable Event event){
System.out.println(event);
return event.getId().toString();
}
}
\main\me\jinmin\EventConverter
package me.jinmin;
import org.springframework.core.convert.converter.Converter;
public class EventConverter {
public static class StringToEventConverter implements Converter<String, Event> {
@Override
public Event convert(String source) {
return new Event(Integer.parseInt(source));
}
}
public static class EventToStringConverter implements Converter<Event, String>{
@Override
public String convert(Event source) {
return source.getId().toString();
}
}
}
\main\me\jinmin\WebConfig
package me.jinmin;
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());
}
}
Spring Web MVC Configurer 를 통해서 Converter를 등록.\test\java\me\jinmin\EventControllerTest 및 테스트 구동 결과
package me.jinmin;
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"))
.andExpect(status().isOk())
.andExpect(content().string("1"));
}
}
print:
Event{id=1, title='null'}
/event/1의 1이 Converter를 통해 Event 타입으로 변환이 돼서 Controller에서 Event 타입으로 받을 수 있음을 보여준다.PropertyEdidor의 대체제
Object와 String 간 변환
Locale에 따른 문자열 다국화(옵션)
Tread-Safe 하기 때문에 빈으로 등록 가능하지만 FormatterRegistry(❤ 직접x, Spring Web MVC Configurer)에 등록하여 사용.
\main\java\me\jinmin\EventController 는 위 Converter와 동일
\main\java\me\jinmin\EventFormatter
package me.jinmin;
import org.springframework.format.Formatter;
import java.text.ParseException;
import java.util.Locale;
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();
}
}
\main\java\me\jinmin\WebConfig
package me.jinmin;
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());
}
}
\test\java\me\jinmin\EventControllerTest 코드 및 결과는 Converter와 동일.
PropertyEditor와 다르게 실제 변환 작업은 ConversionService의 인터페이스를 통해 Tread-Safe하게 사용.
스프링 MVC, Spring xml 빈 설정 파일, SpEL(SPring Expression Language)에서 사용.
❗DefaultFormattingConversionService
여러 기본 Converter와 Formatter를 등록 (상속 관계)

package me.jinmin;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.convert.ConversionService;
import org.springframework.stereotype.Component;
@Component
public class AppRunner implements ApplicationRunner {
@Autowired
ConversionService conversionService;
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println(conversionService.getClass().toString());
}
}
print:
class org.springframework.boot.autoconfigure.web.format.WebConversionService
ConversionService가 아닌 WebConversionService(스프링 부트가 제공하는 클래스)가 출력. (실제로 많이 사용되지는 않지만 교육상 알아야 함)
스프링 부트에서는 Formatter와 Converter 의 빈이 등록이 됐다면 해당 빈을 찾아서 자동 등록해준다. (⇒ 즉, 기존에 형성한 WebConfig를 통한 등록이 필요 없다.) (확인해보자!)
첫 번째로, WebConfig를 지우고 EventConverter의 내부 클래스를 @Component로 빈으로 등록하여 앱을 구동해보자.
package me.jinmin;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
public class EventConverter {
@Component
public static class StringToEventConverter implements Converter<String, Event> {
@Override
public Event convert(String source) {
return new Event(Integer.parseInt(source));
}
}
@Component
public static class EventToStringConverter implements Converter<Event, String>{
@Override
public String convert(Event source) {
return source.getId().toString();
}
}
}
결과

두 번째로, WebConfig를 지우고 EventFormatter를 @Component로 빈으로 등록하여 앱을 구동해보자.
package me.jinmin;
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();
}
}
Conveter)와 동일Test에서는 어떻게 구동될까?
✨(보충 설명) 계층형 테스트 → 웹과 관련한 빈만 등록 해준다. 주로 Controller들만 등록이 되기에 Formatter와 Converter가 제대로 등록이 안되면 Test가 깨진다.
\test\java\me\jinmin\EventControllerTest
Formatter Test
@WebMvcTest ⇒ @WebMvcTest({EventFormatter.class, EventController.class})Conveter Test
@WebMvcTest ⇒ @WebMvcTest({EventConverter.StringToEventConverter.class, EventController.class})두 가지의 결과
print:
Event{id=1, title='null'}
🔔(추천) Formatter를 사용 : Web과 관련하여 주로 사용하기때문에. (Conveter도 좋다.)