[스프링 코어] 3. 데이터 바인딩 추상화

최진민·2021년 1월 31일
0

스프링 코어

목록 보기
4/8
post-thumbnail
  • 데이터 바인딩 ❓
    • 기술적 관점 : 어떤 property 값을 타겟 객체에 설정하는 기능
    • User 관점 : 사용자 입력값을 App. 도메인 모델에 동적으로 할당
      • 할당시 바인딩이 필요한 이유 : 사용자가 입력한 값은 주로 문자열, 그 문자열을 객체가 갖는 다양한 property 타입에 맞추기 위해.
  • 스프링이 제공하는 인터페이스

PropertyEditor

(가장 고전적인 방식의 데이터 바인딩을 살펴보자)

  • 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 간의 변환만 가능(⇒ 대부분의 경우이기 때문에 조심스레 사용해왔다.)


Converter & Formatter

Converter

  • 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'}
      • Test에서 요청한 /event/1의 1이 Converter를 통해 Event 타입으로 변환이 돼서 Controller에서 Event 타입으로 받을 수 있음을 보여준다.

Formatter

  • 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와 동일.

ConversionService

  • 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(스프링 부트가 제공하는 클래스)가 출력. (실제로 많이 사용되지는 않지만 교육상 알아야 함)

    • 스프링 부트에서는 FormatterConverter 의 빈이 등록이 됐다면 해당 빈을 찾아서 자동 등록해준다. (⇒ 즉, 기존에 형성한 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들만 등록이 되기에 FormatterConverter가 제대로 등록이 안되면 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도 좋다.)

profile
열심히 해보자9999

0개의 댓글