ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [12] 추상화 - Data Binding 추상화 : PropertyEditor
    Spring/Spring 핵심 기술 2020. 4. 23. 17:47
    반응형

    data binding은 어떤 Property의 값을 target 객체에 설정하는 기능을 말합니다. 즉, 사용자가 입력한 값을 application domain 객체에 동적으로 할당하는 기능입니다. 이과 같은 동적인 할당에 data binding이 필요한 이유는 사용자가 입력한 값이 주로 문자열이고 이런 문자열을 객체가 가지고 있는 다양한 type의 field, 심지어 domain 객체 자체에 대해서도 대응하도록 변환해야 하는 경우가 발생하기 때문입니다. 요약하자면 type이 맞지 않는 data들 사이에서 data의 대입이 가능하도록 type을 변환해주는 기능을 data binding이라고 부릅니다. 이 data binding을 지원해주는 Spring interface가 DataBinder(org.springframework.validation.DataBinder)입니다.

         

     

    DataBinder interface는 

    -PropertyEditor를 사용하는 DataBinder는 XML 설정 file의 문자열을 bean에 필요한 적절한 type으로 변환하여 넣어줄 때 사용됩니다.

    -Spring expression language에서도 사용됩니다.

     


     

    [PropertyEditor를 사용한 데이터 바인딩의 예]

    get 요청을 통해 받은 String type의  path variable을 domain class type의 객체로 받아 해당 객체의 field 중 하나를 반환하도록 하는 controller를 작성하여 application상에서 data가 의도한 대로 흘러가는지를 test code로 확인하는 예제입니다.

     

    먼저 임의의 domain class를 하나 만듭니다. 여기서는 Event.java를 사용하도록 합니다. 내용은 다음과 같습니다.

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

     

     

    이제 get요청을 받을 Controller(EventController.java)를 만듭니다. 코드는 다음과 같습니다.

    @RestController	//	①
    public class EventController {
    
        @GetMapping("/event/{event}")	//	②
        public String getEvent(@PathVariable Event event) { //	③
            System.out.println(event);  //Event 객체를 출력합니다.
            return event.getId().toString();    //Event의 id filed를 문자열로 바꿔 반환합니다.
        }
    }

    ① @RestController는 RESTful한 요청을 받을 수 있는 controller임을 뜻합니다. 이 annotation을 붙이는 것으로 @GetMapping을 사용하실 수 있습니다.

    path variable {event}는 URI로 전달되는 변수입니다. /event/1 이라는 URI를 요청하면 {event}로 1이 전달됩니다.

    ③ path variable {event}를 Event type으로 받습니다.

     

     

    위의 code가 잘 동작하는지 test code를 통해 확인합니다.

    1. project tool window를 보시면 source code가 담긴 SRC directory를 확인하실 수 있습니다.
    2. SRC 아래로 main과 test directory가 있는데 두 directory의 package를 동일한 형태로 맞춰줍니다.
    3. test 하위에도 Application entry point가 필요하므로 main하위의 main() method를 포함한 Spring 구동 class와 동일한 class를 만들어줍니다.
    4. EventController를 test할 code를 추가합니다. 통상 class명 뒤에 "Test"를 붙인 형태로 class를 만들어 줍니다.

    test code작성을 위한 project 구성

     

    test code의 내용은 다음과 같습니다.

    @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할 때 JUnit 내장 runner 대신 SpringRunner.class를 사용하도록 지정합니다.

    ② MVC test에 관한 configuration만 적용하는 annotation입니다. Spring Security와 MockMvc (HtmlUnit WebClient, Selenium WebDriver에 대한 지원 포함)도 자동으로 구성합니다.

    ③ WebMvc test를 위해 MockMvc 객체를 사용합니다.

    ④ mockMvc가 get request를 수행하도록 합니다.

    ⑤ result로 200이 나와서 이상이 없을 것을 기대합니다.

    ⑥ Content의 String type "1"일 것을 기대합니다.

     

     

    다음과 같이 test method 앞의 녹색 화살표를 click 하여 test code를 실행합니다.

    test code 실행

     

    위의 test를 실행하면 통과하지 못합니다.

    Status 500은 내부 Server 오류인데 console의 내용을 확인해보면 이 오류의 발생 원인은 String type을 Event type으로 변환하는데 실패했고 이를 처리하기 위한 editor(PropertyEditor)나 conversion strategy를 찾지 못했기 때문임을 알 수 있습니다.

       -->"Failed to convert value of type 'java.lang.String' to required type 'org.spring.dave.Event';"

       -->"no matching editors or conversion strategy found;"

     

     

    이 test code를 통과시키기 위해 우선 PropertyEditor(여기서는 EventEditor.java)를 만들어줍니다.

    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)));	//	⑤
        }
    }

    ① PropertyEditor로 사용할 class는 PropertyEditorSupport class를 상속받습니다. (PropertyEditor interface를 구현해도 되지만 많은 method를 구현해야 하므로 PropertyEditorSupport class를 상속받는 것으로도 PropertyEditor를 사용하기에 충분합니다.)

    override해야 하는 PropertyEditorSupport class의 method 두 개 중 하나입니다. 특정 객체로부터 원하는 data를 String type으로 가져오는 method입니다.

    java.beans.PropertyEditorSupport의 getValue() method는 data binding할 객체를 Object type으로 가져옵니다.

    override해야 하는 PropertyEditorSupport class의 method 두 개 중 하나입니다. String type을 받아 필요에 따라 적용하는 method입니다.

     java.beans.PropertyEditorSupport setValue() method는 data binding이 필요한 객체를 생성하면서 field 중 하나에 String type data를 대입하는 method입니다.

     

    이렇게 만든 data biding에 사용해야 할 텐데 PropertyEditor는 thread safe하지 않기 때문에 thread scope의 bean 외에는 bean으로 등록하면 안 됩니다. bean이 아니면 주입받을 수 없으므로 controller class 내부에 @InitBinder annotation을 붙인 method에서 binder를 등록하도록 하여 해당 controller에서 binder를 사용할 수 있도록 합니다. 다음은 controller에 binder를 적용하기 위해 controller class(EventController.java)를 수정한 코드입니다.

    @RestController
    public class EventController {
        @InitBinder //	①
        public void init(WebDataBinder webDataBinder) {
            webDataBinder.registerCustomEditor(Event.class, new EventEditor());	//	②
        }
    
        @GetMapping("/event/{event}")
        public String getEvent(@PathVariable Event event) {
            //System.out.println(event);
            return event.getId().toString();
        }
    }

    ① controller에서 binder를 사용하기 위해 초기화하는 method를 지정하는 annotation입니다.

    WebDataBinder type의 객체에 registerCustomEditor() method를 사용하여 Controller에서 사용할 PropertyEditor를 등록합니다.

     

     

    PropertyEditor로 data binding을 하도록 조치 후 다시 test를 실행해보면 문제가 되었던 String -> Event type 간 변환이 정상적으로 진행되어 pass 됩니다.

     

    댓글

Designed by Tistory.