Spring/Spring Core

[Spring] 데이터 바인딩 추상화 - PropertyEditor

TheWing 2020. 11. 5. 01:15

데이터 바인딩 추상화 - PropertyEditor

spring이 제공해주는 org.springframework.validation.DataBinder

1. Data Binding이란?

  • 기술적인 관점으로 보았을 때는 프로퍼티 값을 타겟 객체에 설정하는 기능이다
  • 사용자 관점으로 보았을 때는 사용자 입력값을 애플리케이션 도메인 모델에 동적으로 변환해 넣어 주는 기능이다. 예를 들면 입력값은 대부분 String(문자열) 인데, 그 값을 도메인 객체가 가지고 있는 자료형 (int, boolean, long, Date ) 등 Event, Book 같은 도메인 타입으로 변환해서 넣어주는 기능이다.
  • 사용자가 입력한 문자열 값을 도메인 타입에 맞춰 변환하여 사용 하는 것을 데이터 바인딩 이라고 합니다

2. Spring Data Binding

  • Spring은 다양한 인터페이스로 추상화해서 데이터 바인딩 기능을 제공한다
  • 데이터 바인딩 인터페이스는 주로 웹 MVC에서 사용하지만 Web에만 특화되어 있지 않고 여러 곳에서 사용되는 spring의 핵심 기술 중 하나이다
  • 가장 고전적인 예제를 보겠다
  • 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 + '\'' +
                '}';
    }
}
  • EventController 컨트롤러
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 애노테이션으로 Get Request를 /event/이벤트id로 받게된다
    @GetMapping("/event/{event}")
    //@PathVariable 애노테이션으로 {event}는 event 도메인 객체로 받아서 매개 변수에 넣어준다
    // EX) www.TheWing.com/event/1 이렇게 입력했다고 가정하면 1인 부분인 이벤트 id를 Event 타입에 맞게 변환을 해야한다
    public String getEvent(@PathVariable Event event) {
        System.out.println(event);
        return event.getId().toString();
    }
}
  • EventControllerTest 테스트 클래스
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 {
                //get 요청 1을 보낸다
                //status가 200으로 나오길 바란다
                //content가 1이 나오길 바란다.
        mockMvc.perform(get("/event/1"))
                .andExpect(status().isOk())
                .andExpect(content().string("1"));

    }
}

3. PropertyEditor 인터페이스

특징

  • Spring 3.0 이전 까지 DataBinder가 변환 작업하여 사용하던 인터페이스이다
  • Spring이 제공하는 DataBinder 인터페이스를 통해 사용된다
  • Thread-safe 하지 않는다
    • 상태 정보(값)를 저장하고 있기 때문에 여러 쓰레드를 공유해서 사용하면 안된다.
    • Bean scope에서 Singleton 빈으로 등록해서 사용하면 안된다. (Bean scope 에서 Thread scope로 사용하는 것이 그나마 낫다)
  • Object와 String 간의 변환만만 가능하여 사용 범위가 제한적이다

1) PropertyEditor 구현 예제

import java.beans.PropertyEditorSupport;

public class EventEditor extends PropertyEditorSupport {

    // 보통 PropertyEditorSupport를 상속하면 getAsText()와 setAsText() 이 두개를 구현하지만
        // 필요한 것은 Text를 event로 구현하기 위함이다
    // 값이 들어오는것은 String이지만 Integer로 변환해야한다

    @Override
    public String getAsText() {
        Event event = (Event)getValue();
        return event.getId().toString();
    }

        //
    //Thread safe하지 않기 때문에 여러 쓰레드에 공유해서 사용하면 안된다
    //그냥 빈으로 Component로 등록하면 안된다
    //웬만해서 절대 사용하면 안된다
    //

    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        setValue(new Event(Integer.parseInt(text)));
    }
}
  • PropertyEditor 인터페이스를 구현하게 되면 매우 많은 양의 메소드를 구현해야 하기 때문에 PropertyEditorSupport 클래스를 상속받아 필요한 메소드만 구현할 수 있다. 그 중 예시에서는 getAsText()setAsText() 메소드를 구현한다

PropertyEditor 구현체 사용시 주의사항

  • 서로 다른 쓰레드에게 공유되기 때문에 Thread-safe하지 않다.
    • PropertyEditorgetValue()setValue() 메소드로 공유되는 값은 PropertyEditor가 보유하고 있는 값이여서 값이 바뀔 위험이 있다. (원하는 값이 나오지 않음)
    • 일반적인 빈으로 Component로 등록하면 안된다
    • Bean scope로 Thread scope 빈으로 사용하면 그나마 괜찮다. 그래도 Singleton scope던 Thread scope이던 빈으로 등록해서 사용하지 않는 것이 좋다.

2) Controller 에 등록

  • PropertyEditor 를 사용하려면 @InitBinder 애노테이션을 사용해서 Controller에 등록하여 사용한다
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) {
        //Event class 타입을 처리할 PropertyEditor를 등록할 수 있다
        webDataBinder.registerCustomEditor(Event.class, new EventEditor());
    }

    @GetMapping("/event/{event}")
    public String getEvent(@PathVariable Event event) {
        System.out.println(event);
        return event.getId().toString();
    }
}
  • @InitBinderEvent class 타입을 처리할 PropertyEditor를 등록할 수 있다
  • WebDataBinder는 DataBinder의 구현체 중 하나이다
  • DataBinder의 PropertyEditor를 사용해서 String으로 받은 "1"을 Integer 1로 변환이 가능하다

테스트 결과

Event{id=1, title='null'}

결론

이러한 구현하는 방법 단점

  • 상당히 편리하지 않다
  • 구현 자체도 편리하지 않다
  • Thread-safe하지 않아서 사용하기 힘들다
  • Bean으로 등록해서 사용하기도 위험하다

다음 포스팅은 이러한 단점을 보완한 데이터 바인딩 추상화 : Converter와 Formatter 에 대해서 알아보겠다.

References