[Spring] 데이터 바인딩 추상화 : Converter와 Formatter

2022. 10. 30. 13:04JAVA/Spring

1. Converter 인터페이스

  • PropertyEditor의 단점을 개선하는 인터페이스
  • S 타입을 T 타입으로 변환할 수 있음
  • 쓰레드 세이프함

PropertyEditor 단점

  • Object와 String간의 변환만 할 수 있음
  • 데이터 바인딩 간의 String 문자열이 아닌 타입과 타입간의 변환을 할 수 없음

PropertyEditor 메서드 호출

  • getAsText : 어떤 객체를 String으로 변환하여 반환
  • setAsText : 문자열을 입력받아 다른 객체로 변환하여 반환

 

2. Converter 인터페이스 구현 실습

  • Converter 인터페이스를 구현하는 클래스 정의
  • WebMvcConfigurer 인터페이스를 구현하는 웹 설정 클래스 구현
    • addFormatters 메서드 재정의
    • FormatterRegistry 인스턴스를 이용하여 Converter 구현 인스턴스를 등록
  • 문자열이 인스턴스로 변환이 되는지 테스트 코드 추가

 

1. EventConverter 클래스를 정의하여 Event 클래스 타입에 대한 Source Converter와 Target Converter를 구현합니다.

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();
        }
    }
}
  • StringToEventConveter 클래스 : 문자열을 Event 클래스 타입으로 변환
  • EventToStringConveter 클래스 : Event 인스턴스를 문자열로 변환
  • 2개의 Converter 구현 클래스가 하나의 PropertyEditor를 대체

2. SpringBoot없이 SpringMVC를 사용하는 경우 Converter 등록하기

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

 

3. 테스트 코드를 작성합니다.

@WebMvcTest
public class EventControllerTest {
    @Autowired
    MockMvc mockMvc;

    @Test
    public void getTest() throws Exception {
        //given
        String url = "/event/";
        String id  = "1";
        //when
        String actual = mockMvc.perform(get(url+id))
                .andExpect(status().isOk())
                .andExpect(content().string(id))
                .andReturn().getResponse().getContentAsString();
        //then
        assertThat(actual).isEqualTo("1");
    }
}
  • 실행 결과 url의 문자열 “1”이 Event 타입으로 변환한 다음에 컨트롤러에서 id 문자열을 반환하는 것을 확인할 수 있습니다.

 

3. Formatter 인터페이스

  • PropertyEditor 대체제
  • Spring이 제공하는 웹 쪽에 특화되어 있는 인터페이스
  • Object와 String 간의 변환을 담당한다.
  • FormatterRegistry에 등록해서 사용

 

Converter 인터페이스와 Formatter 인터페이스의 차이

  • Formatter 인터페이스는 문자열을 Locale에 따라 다국화하는 기능을 제공 (선택)

 

4. Formatter 인터페이스 구현 실습

  • Formatter 인터페이스 구현 클래스 빈 정의
  • 테스트 코드 추가

1. Formatter 인터페이스를 구현할 클래스 정의

@Component
@AllArgsConstructor
public class EventFormatter implements Formatter<Event> {

    private final 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", null, locale);
        return object.getId().toString();
    }
}
  • Locale 정보를 기반으로 문자열 변환 가능 (선택)
  • 쓰레드 세이프하기 때문에 빈으로 등록해서 사용이 가능함
    • 빈으로 등록이 가능하기 때문에 의존 객체(필드 멤버)에 자동 주입해서 사용이 가능함

 

2. Formatter 인터페이스 구현 클래스를 등록할 웹 설정 클래스 정의

@Configuration
@AllArgsConstructor
public class WebConfig implements WebMvcConfigurer {
    private final MessageSource messageSource;

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addFormatter(new EventFormatter(messageSource));
    }
}

 

3. 테스트 코드를 작성 및 실행

@WebMvcTest
public class EventControllerTest {
    @Autowired
    MockMvc mockMvc;

    @Test
    public void getTest() throws Exception {
        //given
        String url = "/event/";
        String id  = "1";
        //when
        String actual = mockMvc.perform(get(url+id))
                .andExpect(status().isOk())
                .andExpect(content().string(id))
                .andReturn().getResponse().getContentAsString();
        //then
        assertThat(actual).isEqualTo("1");
    }
}

 

5. ConversionService

  • 실제 변환 작업은 이 인터페이스를 통해서 쓰레드-세이프하게 수행됨
  • 스프링 MVC, 빈 설정, SpEL에서 사용됨
  • DefaultFomrattingConversionService 클래스
    • ConversionService 인터페이스 구현체
    • FormatterRegistry, ConversionService 기능 수행
    • 여러 기본 Converter와 Formatter 구현 클래스 빈들을 자동 등록해줌
  • ConversionService를 사용하면 WebMvcConfigure 인터페이스 구현 클래스를 정의하고 addFormatters 메서드를 재정의하여 등록하지 않아도됨
    • 빈으로 등록된 Converter, Formatter 인터페이스 구현 클래스를 스프링 프레임워크가 자동으로 등록해줌

 

6. ConversionService 실습

실습1 : 자동주입된 ConversionService 인스턴스의 타입을 확인

 

1. ConversionService의 타입이 무엇인지 테스트 코드를 추가합니다.

@SpringBootTest
public class ConversionServiceTest {
    @Autowired
    ConversionService conversionService;
    
    @Test
    public void webConversionServiceClassTest(){
        //given
        
        //when
        boolean actual = conversionService instanceof WebConversionService;
        //then
        assertThat(actual).isTrue();
    }
}
  • 실행결과 : ConversionService가 WebConversionService로 주입됨
  • WebConversionService 클래스가 스프링부트가 기본적으로 제공하는 구현 클래스
  • WebConversionService 클래스는 DefaultFormattingConversionService 클래스를 상속하여 구현한 클래스
    • DefaultFormattingConversionService 클래스보다 조금더 많은 기능을 가짐
  • ConversionService 인스턴스의 convert 메서드를 통하여 변환할 수 있지만 ConversionService 의존 객체를 직접 주입하여 사용하지는 않음

 

실습 2 : FormatterRegistry에 Converter 또는 Formatter를 명시적으로 등록하지 않고 ConversionService가 Converter, Formatter 빈을 자동 등록했고 변환이 되는지 테스트

  • Formatter 인터페이스 구현 클래스 빈 정의
    • 빈으로 등록된 구현 클래스는 ConversionService에 의해서 자동 등록됨
  • 테스트코드 추가

 

1. 테스트 코드를 추가합니다.

@WebMvcTest({EventFormatter.class, EventController.class})
public class EventControllerTest {
    @Autowired
    MockMvc mockMvc;

    @Test
    public void getTest() throws Exception {
        //given
        String url = "/event/";
        String id  = "1";
        //when
        String actual = mockMvc.perform(get(url+id))
                .andExpect(status().isOk())
                .andExpect(content().string(id))
                .andReturn().getResponse().getContentAsString();
        //then
        assertThat(actual).isEqualTo("1");
    }
}

 

2. Event 클래스의 Formatter를 구현하고 빈으로 등록

@Component
@AllArgsConstructor
public class EventFormatter implements Formatter<Event> {

    private final 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", null, locale);
        return object.getId().toString();
    }
}

 

3. 테스트 코드를 실행하여 결과를 확인

Event(id=1)
  • 실행 결과 : FormatterRegistry에 명시적으로 등록하지 않음에도 문자열 “1”이 Event 타입으로 변환된 것을 확인함

 

정리

  • Converter와 Formatter를 이용하여 PropertyEditor를 대체한 타입 변환을 정의할 수 있음
  • Spring Web MVC를 사용하는 경우 Formatter를 사용하는 것을 권장함
  • ConversionService는 Convert와 Formatter 구현 클래스 빈을 자동 등록하여 따로 등록할 필요없이 변환 서비스를 지원하게 해줌

 

References

source code : https://github.com/yonghwankim-dev/spring_study/tree/master/spring_converter_formatter/src/main/java/kr/yh
[인프런] 스프링 프레임워크 핵심 기술