ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [13] Data binding 추상화: Converter와 Formatter
    Spring/Spring 핵심 기술 2020. 4. 26. 07:56
    반응형
    //@Configuration
    public class WebConfig implements WebMvcConfigurer {
    
        @Override
        public void addFormatters(FormatterRegistry registry) {
            registry.addFormatter(new EventFormatter());
        }
    }

    [Converter]

    PropertyEditor는 사용하기 복잡하고 Object 대 String의 변환만 지원한다는 한계가 있습니다. 하지만 data binding은 여러 종류의 서로다른 type간 변환을 필요로 하기 때문에 이를 개선하고자 등장한 것이 Converter interface입니다. Converter interface는 PropertyEditor의 상태정보를 제거해 thread safe하게 사용할 수 있습니다.

     

    Converter 객체를 만들기 위한 class(여기서는 EventConverter.java)는 다음과 같이 Converter interface를 구현하여 작성합니다.

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

    ① Converter<S, T> generic type의 interface를 구현하면서 T convert(S) method를 overriding 합니다.

    ② Source type을 받아 Event type을 return하도록 convert() method를 작성합니다.

     

     

    이렇게 만든 Converter는  직접 bean으로 등록해서 사용거나 ConverterRegistry에 등록해서 사용합니다.

     

    converter의 test를 위해 WebMvcConfigurer interface (@EnableWebMvc를 사용하여 Spring MVC를 활성화 하기 위한 Java기반 설정을 원하는대로 만지기 위한 callback method를 정의하는 interface)를 구현한 configurer class(WebConfig.java)를 선언하고 FormatterRegistory 객체에 위에서 만든 converter type 객체를 넣어서 converter를 등록합니다.

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

     

    test를 위한 target 객체는 Event type 객체를 사용합니다.

    public class Event {
    
        private Integer id;
        private String title;
    
        public Event(Integer id) {
            this.id = id;
        }
    
        public String getTitle() { return title; }
    
        public void setTitle(String title) {
            this.title = title;
        }
    
        public Integer getId() { return id; }
    
        public void setId(Integer id) { this.id = id; }
    
        @Override
        public String toString() {
            return "Event{" +
                    "id=" + id +
                    ", title='" + title + "\'" +
                    "}";
        }
    }

     

     

    test를 위한 controller의 코드는 아래와 같습니다.

    @RestController
    public class EventController {
        
        @GetMapping("/event/{event}")
        public String getEvent(@PathVariable Event event) {
            System.out.println(event);
            return event.getId().toString();
        }
    }

     

    이전 post와 동일한 test code를 실행하여 data binding이 잘 되는지 확인해봅니다.

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

     

    test가 다음과 같이 통과됩니다.

     

    기본적으로 제공되고 있는 Converter들이 있어 모든 Converter를 만들 필요는 없습니다. 사용할 수 있는 Converter가 있으면 그것을 사용합니다.

     

     


     

    [Formatter]

    Formatter는 message 다국화 기능을 지원할 수 있는 좀 더 web쪽에 특화된 data binding interface입니다. Formatter class(EventFormatter.java)의 code는 다음과 같습니다.

    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<T> generic type interface를 구현합니다. parse(), print() 두 method를 구현해야합니다.

    ② String type을 받아 Formatter<T>의 T type으로 반환하는 T parse(String, Locale) method를 구현합니다.

    Formatter<T>의 T type을 String print(T, Locale) method를 구현합니다.

    ※MessageResource bean을 주입받아 Locale 따라 message를 변경하며 출력할 수도 있습니다.

     

    Formatter도 thread safe하기 때문에 bean으로 등록해서 사용할 수 있습니다. 또한 FormatterRegistry 객체에 addFormatter() method로 Formatter 객체를 등록해서 사용할 수도 있습니다. Formatter를 등록하여 사용하기 위해 WebConfig.java를 다음과 같이 수정합니다.

    @Configuration
    public class WebConfig implements WebMvcConfigurer {
    
        @Override
        public void addFormatters(FormatterRegistry registry) {
            registry.addFormatter(new EventFormatter());	//	①
        }
    }

    ① addFormatter() method로 Formatter 객체를 등록합니다.

     

    Formatter를 등록해서 사용하도록 code 수정 후 test code를 실행하면 통과함을 확인할 수 있습니다.

     


     

    [ConversionService]

    Converter와 Formatter는 ConversionService interface를 통해 thread safe하게 data binding을 수행합니다. ConversionService interface는 Spring MVC, bean(value)설정, SpEL에서 사용됩니다. 

     

    ConversionService interface의 구현체로 제공되는 DefaultFormattingConversionService class가 있는데, ConversionService 뿐 아니라 ConverterRegistry, FormatterRegistry interface도 구현하고 있어 이 class를 사용하여 편리하게 data binding 할 수 있습니다. 보통 ConversionService type의 bean으로 DefaultMormattingConversionService class의 객체를 등록하여 사용합니다. DefaultMormattingConversionService는 여러가지 기본 converter와 formatter를 사용할 수 있도록 등록해줍니다. 위의 예제에서 살펴본 String <-> Object 간의 data binding은 기본으로 등록되어있는 converter 또는 formatter를 사용하여 처리할 수 있습니다. (converter와 fomatter의 예를 들기 위해 살펴본 것이지 제공되고 있는 것을 다시 만들 필요는 없습니다.)

     

    DefaultFormattingConversionService class의 구현 interface들

     

    다음과 같이 runner class(AppRunner.java)를 하나 추가한 뒤 ConversionService 객체를 주입받아 그 type을 console에 출력해 보도록 합시다.

    @Component
    public class AppRunner implements ApplicationRunner {
        @Autowired
        ConversionService conversionService;
    
        @Override
        public void run(ApplicationArguments args) throws Exception {
            System.out.println(conversionService.getClass().toString());
        }
    }

     

    실행결과는 다음과 같습니다.

    DefaultFormattingConversionService type이 나올 것을 기대했지만 결과는 WebConversionService type으로 출력됩니다. WebConversionService는 Spring boot에서 제공되는 class로 DefaultFormattingConversionService를 상속받은 확장한 class입니다.

     

    ConversionService가 Converter와 Formatter를 사용하여 실제로 data를 converting 할 수 있는 class입니다. 그러므로 ConversionService를 통해 data binding 할 수 있습니다.

     

    다만 위의 예제와 같이 ConversionService를 bean으로 주입받아 사용할 일은 거의 없습니다.

     

     

     

    Spring boot에서는 Converter와 Formatter를 bean으로 등록하면 이를 사용하기 위한 (위의 예제와 같은)Spring Web MVC 설정을 별도로 만들 필요가 없습니다. 즉, Converter와 Formatter가 bean으로 등록이 되어 있다면 Spring boot는 그 bean들을 자동으로 ConversionService에 등록하기 때문에 web과 관련한 별도의 설정없이도 test code를 통과합니다.

     

     

    1.Converter를 bean으로 등록 후 test

    먼저 Converter를 bean으로 등록했을 경우 Converter를 별도로 등록하지 않아도 test code를 통과하는지 살펴보기 위해 Converter(EventConverter.java) class를 다음과 같이 수정하여 Converter가 bean으로 등록되도록 합니다.

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

     

    Formatter와 Web Config는 모두 bean으로 등록하지 않도록 수정합니다.

    //@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();
        }
    }
    //@Configuration
    public class WebConfig implements WebMvcConfigurer {
    
        @Override
        public void addFormatters(FormatterRegistry registry) {
            registry.addFormatter(new EventFormatter());
        }
    }

    이후 test code를 실행하면 통과하게 됩니다.

     

     

    2.Formatter를 bean으로 등록 후 test

    다음과 같이 Formatter class에 @Component annotation을 붙여 bean으로 등록합니다.

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

     

    Converter 를 bean으로 등록하지 않도록 다음과 같이 수정합니다.

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

     

    test code작성에 사용되는 @WebMvcTest annotation은 web과 관련한 controller만 bean으로 등록해 주므로 annotation의 value에 필요한 class들을 명시적으로 지정해야 합니다. 

    @RunWith(SpringRunner.class)
    @WebMvcTest({
        EventController.class, 
        EventFormatter.class})	//	①
    public class EventControllerTest {
    
        @Autowired
        MockMvc mockMvc;
    
        @Test
        public void get_요청입니다() throws Exception {
            mockMvc.perform(get("/event/1"))
                    .andExpect(status().isOk())
                    .andExpect(content().string("1"));
        }
    }

    ① bean으로 등록할 class들을 명시적으로 지정합니다. (test에 사용할 bean을 명시적으로 등록하는 것은 test을 더 명확하게 하므로 좋은 습관입니다.)

     

    이후 test code를 실행하면 마찬가지로 통과하게 됩니다.

     

     

    일반적으로 data binding은 web과 관련해서 사용하기 때문에 Converter와 Formatter 중 Formatter를 사용하는 것을 추천합니다.

     

    참고로 JPA의 Entity와 같은 경우는 이미 Converter가 등록되어 있습니다.

     

    실제로 Converter와 Formatter를 bean으로 등록 후 test code를 실행할 때 @WebMvcTest annotation에 어떤 class를 명시해서 bean으로 등록할지에 대한 고민이 있었습니다.

    1. Converter를 bean으로 등록한 경우는 @WebMvcTest에 class를 명시하지 않아도 test code를 통과합니다.
    2. Formatter를 bean으로 등록한 경우는 Controller class와 Formatter class를 모두 @WebMvcTest로 명시해야 test code를 통과합니다.

    위의 고민을 해결하기 위해 @WebMvcTest annotation 관련 문서를 확인해봤습니다.

    먼저 @WebMvcTest annotation의 정의는 다음과 같습니다.

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    @BootstrapWith(WebMvcTestContextBootstrapper.class)
    @ExtendWith(SpringExtension.class)
    @OverrideAutoConfiguration(enabled = false)
    @TypeExcludeFilters(WebMvcTypeExcludeFilter.class)
    @AutoConfigureCache
    @AutoConfigureWebMvc
    @AutoConfigureMockMvc
    @ImportAutoConfiguration
    public @interface WebMvcTest {
    
    	/**
    	 * Properties in form {@literal key=value} that should be added to the Spring
    	 * {@link Environment} before the test runs.
    	 * @return the properties to add
    	 * @since 2.1.0
    	 */
    	String[] properties() default {};
    
    	/**
    	 * Specifies the controllers to test. This is an alias of {@link #controllers()} which
    	 * can be used for brevity if no other attributes are defined. See
    	 * {@link #controllers()} for details.
    	 * @see #controllers()
    	 * @return the controllers to test
    	 */
    	@AliasFor("controllers")
    	Class<?>[] value() default {};
    
    	/**
    	 * Specifies the controllers to test. May be left blank if all {@code @Controller}
    	 * beans should be added to the application context.
    	 * @see #value()
    	 * @return the controllers to test
    	 */
    	@AliasFor("value")
    	Class<?>[] controllers() default {};
    
    	/**
    	 * Determines if default filtering should be used with
    	 * {@link SpringBootApplication @SpringBootApplication}. By default only
    	 * {@code @Controller} (when no explicit {@link #controllers() controllers} are
    	 * defined), {@code @ControllerAdvice} and {@code WebMvcConfigurer} beans are
    	 * included.
    	 * @see #includeFilters()
    	 * @see #excludeFilters()
    	 * @return if default filters should be used
    	 */
    	boolean useDefaultFilters() default true;
    
    	/**
    	 * A set of include filters which can be used to add otherwise filtered beans to the
    	 * application context.
    	 * @return include filters to apply
    	 */
    	Filter[] includeFilters() default {};
    
    	/**
    	 * A set of exclude filters which can be used to filter beans that would otherwise be
    	 * added to the application context.
    	 * @return exclude filters to apply
    	 */
    	Filter[] excludeFilters() default {};
    
    	/**
    	 * If Spring Security's {@link MockMvc} support should be auto-configured when it is
    	 * on the classpath. Also determines if
    	 * {@link org.springframework.security.config.annotation.web.WebSecurityConfigurer}
    	 * classes should be included in the application context. Defaults to {@code true}.
    	 * @return if Spring Security's MockMvc support is auto-configured
    	 * @deprecated since 2.1.0 in favor of Spring Security's testing support
    	 */
    	@Deprecated
    	@AliasFor(annotation = AutoConfigureMockMvc.class)
    	boolean secure() default true;
    
    	/**
    	 * Auto-configuration exclusions that should be applied for this test.
    	 * @return auto-configuration exclusions to apply
    	 */
    	@AliasFor(annotation = ImportAutoConfiguration.class, attribute = "exclude")
    	Class<?>[] excludeAutoConfiguration() default {};
    
    }

     

    또한 annotation에 대한 나머지 설명을 다음과 같습니다.

     

    @WebMvcTest annotation은 Spring MVC Component에만 포커스를 맞춘 Spring MVC test에 사용되는 annotation입니다. 

    이 annotation을 사용하면 완전한 auto-configuration 대신 MVC test에 관한 configuration만 적용됩다. (즉, @Controller, @ControllerAdvice, @JsonComponent, Converter/GenericConverter, Filter, WebMvcConfigurer 그리고 @Component, @Service 또는 @Repository를 제외한 HandlerMethodArgumentResolver bean들) 

    기본설정으로 @WebMvcTest가 적용된 test는 Spring Security와 MockMvc (HtmlUnit WebClient,  Selenium WebDriver에 대한 지원 포함)도 자동으로 구성합니다. MockMVC를 보다 정밀하게 제어하려면 @AutoConfigureMockMvc annotation을 사용할 수 있습니다. 

    일반적으로 @WebMvcTest는 @Controller bean이 필요로 하는 collaborator를 만들기 위해  @MockBean이나 @Import와 함께 조합하여 사용됩니다.

     

    @WebMvcTest로 적용되는 configuration 중에는 Converter도 포함되어 있음을 확인할 수 있습니다.

    따라서 @WebMvcTest에 명시적으로 Converter class type의 bean을 등록하지 않아도 test code를 통과하게 됩니다. Formatter의 경우는 @WebMvcTest configuration에 포함되어있지 않습니다. 그렇기 때문에 Formatter class를 명시적으로 지정해야 test code를 정상적으로 수행할 수 있습니다. 한가지 궁금한 점은 문서상에서 @WebMvcTest는 @Controller bean들을 configuration에 포함한다고 되어있는데 Formatter를 bean으로 등록하는 경우 @WebMvcTest에 Formatter class뿐 아니라 Controller class(EventController.class)도 포함해야만 test code를 통과한다는 것입니다. 이 부분에 대해서는 좀 더 확인이 필요합니다. 

     

     


     

     

    [등록된 Converter의 확인]

    ConversionService 객체를 출력하면 현재 등록된 Converter들을 확인 수 있습니다. 실제로 확인해보기 위해 AppRunner class를 다음과 같이 수정하고 application을 실행해 봅시다.

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

    ① ConversionService 객체를 출력하면 현재 등록되어있는 Converter들을 확인해볼 수 있습니다.

     

     

    댓글

Designed by Tistory.