데이터 바인딩 추상화(2) : Converter와 Formatter, ConversionService

de_sj_awa·2021년 6월 26일
0

스프링 3.0 이전까지 사용되던 PropertyEditor는 Object-String 간의 변환만 지원하며 구현도 번거롭고 스레드 세이프 하지 않기 때문에 빈으로 등록해서 사용할 수도 없다.

그래서 스프링 3.0 이후부터 PropertyEditor의 단점을 없앤 Converter라는 인터페이스가 등장했다.

1. Converter

  • S 타입을 T 타입으로 변환할 수 있는 매우 일반적인 변환기.
  • 상태 정보 없음 == Stateless == 쓰레드세이프
  • ConverterRegistry에 등록해서 사용
 public class StringToEventConverter implements Converter<String, Event> {  
    @Override 
    public Event convert(String source) { 
        Event event = new Event(); 
        event.setId(Integer.parseInt(source)); 
        return event; 
    } 
 }

도메인 클래스

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 + '\'' +
                '}';
    }
}
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();
        }
    }
}

ConverterRegistry에 등록

@Component
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new EventConverter.StringToEventConverter());
        //registry.addFormatter(new EventFormatter());
    }
}

그러나 ConverterRegistry에 등록하지 않더라도 기본적으로 integer나 String과 도메인 클래스의 변환은 기본적으로 등록되어있는 Converter나 Formatter가 자동으로 변환해준다.

Converter의 변환은 General하나, web에서는 사용자의 입력값이 주로 문자열로 들어오고, 또 객체들을 문자로 내보내는 경우가 많고 이 문자열을 MessageSource를 이용해 Locale에 맞게 변환해 메시지를 보내주는 경우가 많다. 따라서 web쪽에 특화되어있는 인터페이스를 스프링에서 제공하고 있는데, 이것이 바로 Formatter이다.

2. Formatter

  • PropertyEditor 대체제
  • Object와 String 간의 변환을 담당한다.
  • 문자열을 Locale에 따라 다국화하는 기능도 제공한다. (optional)
  • FormatterRegistry에 등록해서 사용
public class EventFormatter implements Formatter<Event> { 
    @Override 
    public Event parse(String text, Locale locale) throws ParseException {         
        Event event = new Event(); 
        int id = Integer.parseInt(text); 
        event.setId(id); 
        return event; 
    } 
    @Override 
    public String print(Event object, Locale locale) { 
        return object.getId().toString(); 
    } 
}

Formatter를 구현하게 되면 parse, print라는 두 메소드를 오버라이딩 해야 한다. parse는 문자열을 받아서 객체로 변환하고, print는 객체를 받아서 문자열로 변환한다. 이는 PropertyEditor(getAsText(), setAsText(String text))와 비슷한데, Formatter는 Locale 정보를 받아 변환할 수 있다.

또한 Formatter도 스레드 세이프 하기 때문에 빈으로 등록해서 사용할 수 있다.

@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();
    }
}

또한 Locale 정보를 받아 메시지 정보를 만들고 싶으면 빈으로 등록되어 있기 때문에 MessageSource를 @Autowired를 통해 주입받을 수 있다.

@Component
public class EventFormatter implements Formatter<Event> {

    @Autowired
    MessageSource messageSource;

    @Override
    public Event parse(String text, Locale locale) throws ParseException {
        return new Event(Integer.parseInt(text));
    }

    @Override
    public String print(Event object, Locale locale) {
        messageSource.getMessage("title", locale);
        return object.getId().toString();
    }
}

FormatterRegistry에 등록

@Component
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        //registry.addConverter(new EventConverter.StringToEventConverter());
        registry.addFormatter(new EventFormatter());
    }
}

PropertyEditor를 DataBinder(org.springframework.validation.DataBinder)를 통해 사용했다면, Converter와 Formatter는 ConversionService를 통해 사용하게 된다.

3. ConversionService

  • 실제 변환 작업은 이 인터페이스를 통해서 쓰레드-세이프하게 사용할 수 있음.
  • 스프링 MVC, 빈 (value) 설정, SpEL에서 사용한다.
  • DefaultFormattingConversionService(ConversionService 타입의 빈으로 이 클래스가 자주 사용됨, 이 클래스가 FormatterRegistry, ConversionService 두 인터페이스를 모두 구현함)
    - FormatterRegistry
    - ConversionService
    - 여러 기본 컨버터와 포매터 등록 해 줌.

Converter는 ConverterRegistry에 등록해야 되고, Formatter는 FormatterRegistry에 등록해야 하나, 사실 FormatterRegistry는 ConverterRegistry를 상속받고 있다. 그래서 FormatterRegistry에는 Converter도 등록할 수 있다. 또한 DefaultFormattingConversionService는 ConversionService 또한 구현하고 있다. 이를 확인하려면 다음과 같이 코드를 작성하면 된다.

@Component
public class AppRunner implements ApplicationRunner {

    @Autowired
    ConversionService conversionService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println(conversionService);
        System.out.println(conversionService.getClass().toString());
    }
}

그러나 스프링 부트에서 이를 출력해보면 DefaultFormattingConversionSerivce가 아닌 WebConversionService가 나온다.

스프링 부트

  • 웹 애플리케이션인 경우에 DefaultFormattingConversionSerivce를 상속하여 만든 WebConversionService를 빈으로 등록해 준다.
  • Formatter와 Converter 빈을 찾아 자동으로 등록해 준다.

또한 Formatter와 Converter가 빈으로 등록되어 있다면 스프링 부트가 자동으로 WebConversionService에 등록해준다.

그리고, 테스트 코드를 작성할 때 @WebMvcTest라는 애노테이션을 사용하면 웹과 관련된 빈만 등록해주기 때문에 Converter와 Formatter도 테스트하겠다고 빈으로 등록해주는 것이 좋다.

RunWith(SpringRunner.class)
@WebMvcTest({EventFormatter.class, EventController.class})
@WebMvcTest({EventConverter.EventToStringConverter.class, EventConverter.StringToEventConverter.class, EventController.class})
public class EventControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void getTest() throws Exception{
        mockMvc.perform(get("/event/1"))
                .andExpect(status().isOk())
                .andExpect(content().string("1"));
    }
}

참고

profile
이것저것 관심많은 개발자.

0개의 댓글