본문 바로가기
  • 실행력이 모든걸 결정한다
Spring Series/Spring Framework

[Spring] 프로퍼티 바인딩 방식(1) - PropertyEditor

by 김코더 김주역 2022. 9. 17.
반응형

프로퍼티 바인딩은 텍스트 형식의 값을 적절한 타입으로 변환해서 객체의 프로퍼티에 넣어주는 것으로, 대표적으로 XML 설정이나 HTTP 요청 파라미터 처리에 필요하다.

여러 개의 포스팅에 걸쳐 Spring에서 프로퍼티 바인딩을 위해 사용할 수 있는 API들을 살펴볼 것이다. 먼저, PropertyEditor을 살펴보자.

 

PropertyEditor

- Spring이 기본적으로 사용하는 바인딩용 타인 변환 API로, 자바빈 표준에 정의된 인터페이스다.

- XML의 <property value="">에서 정의된 프로퍼티 값을 실제 프로퍼티에 저장할 때 활용된다.

 

1) 기본 프로퍼티 에디터

- Spring이 기본적으로 제공하는 프로퍼티 에디터들은 아래 문서에서 확인하자.

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/propertyeditors/package-summary.html

 

org.springframework.beans.propertyeditors (Spring Framework 5.3.23 API)

Class Summary  Class Description ByteArrayPropertyEditor Editor for byte arrays. CharacterEditor Editor for a Character, to populate a property of type Character or char from a String value. CharArrayPropertyEditor Editor for char arrays. CharsetEditor Ed

docs.spring.io

 

 

2) 커스텀 프로퍼티 에디터

- Spring이 지원하지 않는 타입을 파라미터로 사용해야 한다면 커스텀 프로퍼티 에디터를 만들어서 사용해야 한다. 그렇지 않으면 ConversionNotSupportedException 예외로 인해 HTTP 500 에러를 만나게 된다.

- PropertyEditor에서 변환을 위해 사용되는 메소드는 총 4개로, 필요에 따라 메소드들을 알맞게 구현해주면 된다.

  • setAsText() : String 타입의 문자열을 지정함
  • getValue() : 변환된 오브젝트를 반환함
  • setValue() : 변환할 오브젝트를 지정함
  • getAsText() : 변환된 String 타입의 문자열을 반환함

 

- 커스텀 프로퍼티 에디터를 만들 때는 PropertyEditor 인터페이스를 직접 구현하는 것보다는 구현체인 PropertyEditorSupport 클래스를 상속해서 필요한 메소드만 오버라이드해주는 편이 좋다. 아래 문서를 참조하여 PropertyEditorSupport가 가지는 메소드들을 확인해보길 바란다.

https://docs.oracle.com/javase/7/docs/api/java/beans/PropertyEditorSupport.html

 

PropertyEditorSupport (Java Platform SE 7 )

This method is intended for use when generating Java code to set the value of the property. It should return a fragment of Java code that can be used to initialize a variable with the current property value. Example results are "2", "new Color(127,127,34)"

docs.oracle.com

 

예시 - Level Enum 타입을 위한 커스텀 프로퍼티 에디터

public class LevelPropertyEditor extends PropertyEditorSupport {

    public void setAsText(String text) throws IllegalArgumentException {
        // String 타입의 문자열을 원하는 오브젝트로 변환해서 this.value에 저장
        this.setValue(Level.valueOf(Integer.parseInt(text)));
    }
    
    public String getAsText() {
        // this.value에 저장되어 있는 오브젝트를 String 타입의 문자열로 변환해서 반환
        return String.valueOf(((Level)this.getValue()).intValue());
    }
}

 

- 커스텀 프로퍼티 에디터는 디폴트 프로퍼티 에디터보다 높은 우선순위를 갖기 때문에, 디폴트 프로퍼티 에디터에서 지원하는 타입이라도 커스텀 프로퍼티 에디터를 적용해서 부가적인 조건을 부여해줄 수도 있다. 이 경우는 조금 뒤에서 다뤄보겠다.

 

 

3) @InitBinder

(1) @InitBinder란?

- @InitBinder가 붙은 메소드는 프로퍼티 바인딩 작업 전에 자동으로 호출된다.

 

(2) WebDataBinder

- @InitBinder 메소드의 파라미터로 사용할 수 있는 클래스다.

- AnntationMethodHandlerAdapter는 프로퍼티 바인딩 작업이 필요한 @RequestParam, @ModelAttribute, @PathVariable, @CookieValue 또는 @RequestHeader 어노테이션을 만나면 WebDataBinder를 만든다.

- WebDataBinder은 프로퍼티 바인딩 기능을 포함하기 때문에 커스텀 프로퍼티 에디터를 WebDataBinder에 직접 등록해주면 된다. 다음과 같이 스프링이 제공하는 @InitBinder 어노테이션을 붙인 WebDataBinder 초기화 메소드를 만들고 그 안에 등록 코드를 작성해주면 같은 컨트롤러 메소드의 프로퍼티 바인딩 작업에 적용된다.

@InitBinder
public void initBinder(WebDataBinder webDataBinder) {
    webDataBinder.registerCustomEditor(Level.class, new LevelPropertyEditor());
}

- 앞서, 디폴트 프로퍼티 에디터에서 지원하는 타입에도 커스텀 프로퍼티 에디터를 적용할 수 있다고 언급했다. 이 때, 모든 특정 타입에 대해 커스텀 프로퍼티 에디터를 적용해버리면 곤란한 상황이 발생할 수 있기 때문에 프로퍼티 이름을 지정해서 커스텀 프로퍼티 에디터를 등록하는 것이 좋다. 단, 단일 파라미터 바인딩에는 적용되지 않는다. 아래 예시에서는 int age 프로퍼티에만 커스텀 프로퍼티 에디터를 등록한다.

@InitBinder
public void initBinder(WebDataBinder webDataBinder) {
    webDataBinder.registerCustomEditor(int.class, "age", new AgePropertyEditor(0, 300));
}

 

(3) WebRequest

- @InitBinder 메소드의 파라미터로 사용할 수 있는 인터페이스다.

- WebRequest는 HttpServletRequest에 들어 있는 모든 HTTP 요청 정보를 담고 있기 때문에 이를 활용하여 프로퍼티 에디터를 설정할 수 있다.

 

(4) WebBindingInitializer

- 프로퍼티 에디터의 적용 범위를 하나의 컨트롤러가 아닌 모든 컨트롤러로 적용하고 싶을 때 사용할 수 있다.

- 아래와 같이 WebBindingInitializer 인터페이스를 상속받아 initBinder() 메소드를 구현한 구현체를 만들면 된다.

public class CustomWebBindingInitializer implements WebBindingInitializer {
    public void initBinder(WebDataBinder binder, WebRequest request) {
        binder.registerCustomEditor(Level.class, new LevelPropertyEditor());
    }
}

- WebBindingInitializer의 구현체는 아래와 같이 AnnotationMethodHandlerAdapter의 webBindingInitializer 프로퍼티에 지정해주면 된다.

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
    <property name="webBindingInitializer">
        <bean class="com.example.demo.custom.CustomWebBindingInitializer" />
    </property>
</bean>

※ WebBindingInitializer는 AnnotationMethodHandlerAdapter외에는 참조될 일이 없기 때문에 내부 bean으로 설정했다.

 

 

4) 프로토타입 빈 프로퍼티 에디터

- 프로퍼티 에디터는 상태를 가지기 때문에 멀티스레드 환경에서 싱글톤으로 만들어 공유하면 안된다.

- 프로퍼티 에디터가 다른 bean을 참조해야 하는 상황이라면 prototype scope를 가지는 bean으로 만들면 된다. 그러면 bean을 요청할 때마다 새로운 오브젝트를 가져올 수 있으면서 DI도 가능해진다.

@Component
@Scope("prototype")
public class LevelPropertyEditor extends PropertyEditorSupport {
    @Autowired LevelService levelService; // DI
    ...
}

 

@Controller
public class UserController {
    @Inject
    Provider<LevelPropertyEditor> levelEditorProvider;
    
    @InitBinder
    public void initBinder(WebDataBinder webDataBinder) {
        webDataBinder.registerCustomEditor(Level.class, levelEditorProvider.get());
    }
    ...
}

 

 

5) 모조 오브젝트 프로퍼티 에디터

- 오직 id 값만 가진 오브젝트를 모조(fake) 오브젝트라고 하고, 모조 오브젝트를 만드는 프로퍼티 에디터를 모조 프로퍼티 에디터라고 한다.

- 모조 오브젝트 방식은 폼에서 등록하거나 수정한 사용자의 정보를 단순히 저장하는 것이 목적인 경우에 유용하다. 프로퍼티 에디터에서 오브젝트 타입인 프로퍼티를 바인딩 할 때, 그 오브젝트의 모든 프로퍼티들을 채우기 위해 DB에 접근할 필요가 없다. 단순히 요청 파라미터로 전달받은 id만 받아두고 외래키로 저장할 때 사용하면 된다. 아래 예시를 참고하자.

public class User {
    ...
    Code userType; // 오브젝트 타입의 프로퍼티, 모조 오브젝트
    
    // setter, getter 메소드 추가
}

public class Code {
    int id;
    
    // setter, getter 메소드 추가
}

 

public class CodePropertyEditor extends PropertyEditorSupport {
    public void setAsText(String text) throws IllegalArgumentException {
        Code code = new Code();
        code.setId(Integer.parseInt(text));
        setValue(code);
    }
    
    public String getAsText() {
        return String.valueOf(((Code)getValue()).getId());
    }
}

User 오브젝트를 DB에 반영할 때, Code 타입인 필드는 아래와 같이 int로 변환해서 외래키로 저장하면 된다.

user.getUserType().getId()

- 폼의 <select> 태그 등을 통해 전달받은 식별자 값만 참조하는 성격의 오브젝트에 유용하게 쓸 수 있다.

- 서비스 계층의 일부 메소드는 자신에게 전달되는 오브젝트 안에 모조 오브젝트가 있다는 사실을 알고 있어야 한다는 단점이 있다. 이러한 이유로 모조 오브젝트를 사용하는 방법은 위험할 수 있다.

 

 

 

● 참고 자료 : 토비의 스프링 3.1 Vol.2

 

 

반응형

댓글