PropertyPlaceholderConfigurer를 @Bean으로 정의해서는 안되는 이유

2009.12.24

PropertyPlaceholderConfigurer는 프로퍼티 파일의 속성을 가지고 ${database.name}과 같은 프로퍼티 값을 바꿔주는 매우 유용한 기능을 가지고 있어서 자주 사용된다. 스프링의 기초정도만 배웠어도 DataSource를 직접 스프링에서 정의하는 할 때 DB연결정보는 프로퍼티 파일로 빼고 ${database.url}과 같은 식으로 쓰라는 것은 한번쯤 들어봤을 것이다.

context 네임스페이스를 쓰면 더욱 정의하기 편하다. 다음과 같이 적어주면 내부적으로 PropertyPlaceholderConfigurer가 만들어지고 모든 ${}로 정의된 빈의 프로퍼티 값을 프로퍼티 파일의 바꿔치기 해준다.

 <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>

 

프로퍼티 값은 애노테이션을 쓴다면 @Value에 의해서 지정할 수 있다.

그렇다면 다음과 같은 코드도 만들어 볼 수 있지 않을까?

public class BeanSP {

    @Value("#{systemProperties['os.name']}")

    String name;

    @Value("${database.username}")

    String username;

    @Bean

    public PropertyPlaceholderConfigurer databaseProperty() {

        PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer();

        ppc.setLocation(new ClassPathResource("database.properties", getClass()));

        return ppc;

    }

}

name은 SpEL을 사용해서 다이나믹하게 시스템 프로퍼티 값을 가져온 것이다. SpEL이 알아서 동작하니 신경쓸 건 없고.

두번째 username은 PropertyPlaceholderConfigurer에 의해서 변경되는 전형적인 ${}을 가지고 있으니 적절한 PropertyPlaceholderConfigurer와 프로퍼티 파일만 만들어주면 DB접속이름으로 변경될 것이라고 기대한다.

보통 PropertyPlaceholderConfigurer은 XML에 정의하지만 XML없는 컨텍스트를 사용하기 위해서 @Bean을 붙여 자바코드에 의한 빈 설정 기능을 사용했다. 이제 BeanSP만 빈으로 등록해주면 @Bean이 붙은 databaseProperty() 메소드에 의해서 PropertyPlaceholderConfigurer 타입의 빈이 등록될 것이고 그러면 당연히 username의 값도 변경될 것이라고 기대할 수 있다.

 

그러나 테스트 해보면 ${database.username}은 바뀌지 않는다. 파일도 잘 만들었을 뿐더러 확인해보면PropertyPlaceholderConfigurer 타입의 빈도 정상적으로 등록되어있다. XML로 바꿔서 등록하면 당연히 잘 된다.

그렇다면 @Bean을 사용해서 정의하면 왜 안될까?

 

이를 알려면 PropertyPlaceholderConfigurer @Bean애노테이션이 붙은 클래스를 스프링이 어떻게 처리하는지에 대한 두가지 지식이 모두 필요하다.

이 두가지 기능은 모두 BeanFactoryPostProcessor(BFPP)에 의해서 처리된다. BFPP는 빈 정의가 모두 준비되었을 때 진행하는 빈 팩토리 후처리기이다. 준비된 빈 정의 자체를 어떤 식으로든 바꿔치기 할 수 있기 때문에 상당히 유용하다.

PropertyPlaceholderConfigurer는 프로퍼티 파일의 속성과 빈 정의에 나온 프로퍼티 값을 비교해서 일치하면 프로퍼티 값 정의 자체를 바꿔주는 것이다. 그래서 ${database.name}이 user1과 같은 값 설정으로 바뀌고 나중에 빈을 만들면 이 빈 정의에 따라서 바뀐 값으로 만들어지는 것이다.

@Bean에 의해서 빈이 등록되는 것은 ConfigurationClassPostProcessor(CCPP) BFPP에 의해서 진행된다. 이 BFPP는 한 술 더 떠서 아예 새로운 빈 정의를 추가해준다. BFPP는 빈 정의를 어떤 식으로든 조작할 수 있는 강력한 기능을 가지도록 정의된 것으로 스프링은 이를 이용해서 많은 기능을 확장해왔다. 그래서 이 BFPP가 동작하는 동안에 @Bean이 붙은 빈 정의를 발견하면 그 내용을 토대로 새로운 빈 정의를 추가해준다.

자.. 문제는 BFPP가 실행되는 시점이다.  순서를 잘 보자.

1. 먼저 위의 BeanSP가 빈으로 등록된다. XML이든 스캐닝이든, 직접 주입이든 암튼 컨텍스트의 빈 설정으로 등록된다. 이 때 아직 @Bean에 의해서 CCPP가 만들어지지 않았다.

2. 이미 ConfigurationClassPostProcessor는 등록되어있다. AnnotationConfigApplicationContext라면 초기화 과정에서 추가해줄 것이고, XML이라면 <context:annotation-config>에 의해서 추가되어있을 것이다.

3. 기본 빈 등록이 끝났으므로 등록된 빈 중에서 BFPP를 찾는다. 지금까지 빈으로 등록된 BFPP는 ConfigurationClassPostProcessor뿐이다.

4. 모든 BFPP를 실행한다. 따라서 CCPP가 후처리기로 동작한다. 이 때 @Bean 메소드를 발견하고 PropertyPlaceholderConfigurer 타입의 빈을 등록해준다.

5. 끝.

문제는 3과 4의 순서이다. @Bean에 의해서 등록되는 빈은 이미 3번 과정에서 BFPP를 모두 찾아서 BFPP에 대한 실행이 시작된 이후에 일어난다. 따라서 4번 과정에서 등록된 PropertyPlaceholderConfigurer은 다시 3번으로 돌아가서 컨텍스트가 실행할 BFPP의 후보로 추가될 수 없다.

따라서 PropertyPlaceholderConfigurer는 빈으로 등록되지만 BFPP로 실행되는 시점이 지난 후기 때문에 무용지물이다.

 

결론은 "BFPP 빈은 다른 BFPP(CCPP)에서 등록되게 만들면 안된다".

XML을 사용하지 않으면서 위의 문제를 풀려면 다음과 같이 아예 독립적으로 PropertyPlaceholderConfigurer를 만들어 초기부터 빈으로 바로 등록되게 하면 된다.

@Component

public class DatabasePropertyPlaceHolder extends PropertyPlaceholderConfigurer {

    public DatabasePropertyPlaceHolder() {

        this.setLocation(new ClassPathResource("database.properties", getClass()));

    }

}

비슷한 원리가 BeanPostProcessor에도 적용될 수 있다. BPP에서 빈을 등록할 일은 없겠지만 기존 프로퍼티 내용을 조작하려고 하려면 BPP의 순서를 잘 염두에 둬야 한다.

또, BFPP가 단지 시간순서의 문제라면 BFPP빈의 실행순서를 조정하는 것은 가능하다. priority나 order를 지정하면 우선적으로 실행될 BFPP를 지정할 수 있다.

AC가 빈 등록이후 초기화 하는 작업의 순서를 알고 싶다면 AbstractApplicationContext의 refresh() 메소드를 참고하면 된다. 구체적인 후처리 작업에 대해서는 BFPP와 BPP를 beanFactory에서 가져와 출력해보면 된다. 스프링 초보자가 아니라면 적어도 자신이 만든 설정에 의해서 어떤 BF, Bean 후처리기들이 동작하는지쯤은 알아야 한다.


    출처 - http://toby.epril.com/?cat=81&paged=8




    다른 대안으로

    org.jasypt.spring31.properties.EncryptablePropertyPlaceholderConfigurer.

    EncryptablePropertyPlaceholderConfigurer(StringEncryptor stringEncryptor) 

    를 사용할 수 있다.







    시대착오적인 설정 파일 *.properties를 버리자 프로그래밍


    Eclipse의 파일 Lock 문제를 찾으면서 처음에는 Properties Editor Plugin을 원망했다. 게다가 대체품으로 선택한 Eclipse-RBE는 다소 불편한 인터페이스에 자꾸 NullPointerException을 떨궈서 사람 짜증나게 만들기 까지.

    하지만 그러다가 곧 생각이 바뀌어 버렸다.

    진짜 문제는 우리가 설정 파일로 시대착오적인 *.properties를 사용하고 있다는 그 사실이다.

    자바의 *.proerties 파일은 텍스트 파일을 가장한 바이너리 파일이라고 보면 된다. 특수한 편집기나 변환기가 없으면 비영문권 사용자에게는 결코 텍스트 파일로써 다뤄질 수 없는 설정 파일 형식이다. 이런 것을 사용하면서 이를 편집하게 도와주는 플러그인을 탓한 것이다.

    지금은 XML이나 YML 같은 잘 정형화되어있고 어느 편집기에서나 열어서 볼 수 있는 훌륭한 설정 파일 포맷이 나온지 이미 몇 년이 지난 시점이다. 그런 와중에 properties를 사용하는 나 자신을 탓하지 않고 플러그인을 탓하다니. 습관이 무섭다.

    Java도 또한 이 문제점을 인지하고 Properties 객체를 XML 파일을 통해서 생성할 수 있게 만든지 이미 오래전이다. Properties XML은 형식도 매우 직관적이다. http://wiki.kwonnam.pe.kr/java/properties 를 참조하자.

    게다가 더욱 훌륭한 것은 Spring Framework를 사용할 경우 <context:property-placeholder/>, <util:properties/>, 그리고 메시지 Resource Bundle 등이 모두 다 이 XML 형식의 프라퍼티를 완벽하게 지원한다는 것이다.

    그래서 오늘은 특정 디렉토리의 모든 *.properties를 XML Properties로 변환해주는 간단한 툴을 만들고, Spring Bean 설정 파일에서 properties를 사용하는 부분들을 모두 XML 파일을 가리키도록 고쳐서 팀 프로젝트의 모든 *.properties 파일을 없애버렸다. 생각보다 오래걸리지도 않았다. 그리고 log4j.properties도 삭제해 버리고 log4j.xml로 전면 교체해 버렸다.

    다른 Java 개발자들에게도 꼭 권한다. 프로젝트에서 *.properties를 삭제해 버리자. 어렵고 복잡한 일도 아니면서 설정 파일 읽기와  편집 효율성이 훨씬 더 증대된다.

    그리고 하면서 느끼게 된 사실이 또 있는데, Spring의 <context:property-placeholder/> 사용하지 말자. 이일민씨의 블로그 글에도 보면 되도록 <util:properties/>와 SpEL을 사용하자는 것이 있는데. 이번에 수정하면서 약간은 다른 이유로 매우 동감하게 되었다.

    properties-placeholder는 부모 자식 관계에 있는 Application Context XML 설정파일들의 경우에도 동일한 properties 파일임에도 모든 Bean 설정 파일에 property-placeholder를 명시해줘야 하는 문제가 있다.

    하지만 <util:properties/>를 사용하게 되면 부모 컨텍스트에 딱 한번만 설정하고 그 외에는 SpEL로 명시해주면 된다. 그 외에도 SpEL을 쓰게 되면 어노테이션 기반 객체 주입시에도 사용이 가능하지만 properties-placeholder는 억지로 String 등의 Bean 객체로 생성하지 않는 이상 어노테이션에서 직접 읽을 방법이 없다(내가 아는한. 있다면 좀 댓글로 알려주세요).

    따라서 나는 <context:property-placeholder/>를 버리고 <util:properties/>와 SpEL 사용을 강력하게 권한다.


    출처 - http://kwon37xi.egloos.com/4665590








    스프링에서 스캔된 컴포넌트에 외부 Property 값 주입하기

    프로그래밍 이야기박성철 2009/05/05 05:09

    서설

    스프링 2.5에 새로 추가된 기능 중 예상 외로 인기 있는 기능이 컴포넌트 스캔이다. Classpath를 따라서 탐색 하다가 정해진 필터링 규칙에 맞는 스프링 빈을 찾아 컨테이너에 등록해주는 기능인데 처음 들었을 때 잘 못 쓰면 독이 되겠다 싶었다. Autowiring은 몰라도 최소한 어떤 빈이 사용되는지는 XML에 등록해 주는 것이 관리 하기 좋지 않겠냐는 생각이었다. 빈 등록까지 자동으로 해버리면 애플리케이션 구성을 추적하기 어려워 유지보수가 곤란할 것이기 때문이다. 결국 나는 서비스 계층 후면은 XML을 사용한 전통적인 빈 설정으로, Web Tier는 빈 스캐닝과 Autowiring을 이용하는 방식으로 용처를 정리해서 사용하고 있다. 

    하지만 내가 결정권이 없는 여러 프로젝트에서 빈 스캐닝 기능을 사용하는 일이 많은 것이 현실이다. 그리고 절대 저질러서는 안 되는 범죄도 아니고 대부분 그리 복잡한 애플리케이션도 아니니 잘못 되었다고 말할 것도 아니다. 결국 두어 번 이런 상황에서 일을 하게 되었다. (사실 XML로 설정을 한다고 해도 관리가 허술하면 복잡하기는 마찮가지다. 설정 관리자라는 롤이라도 두어야 하는 걸까?)

    이런 저런 것 떠나서 빈 스캔 기능을 사용하면 가장 곤란한 것이 특정 값을 빈에 설정할 수 없다는 것이다. 스프링은 단순히 빈 간의 종속성 문제만 해결해줄 뿐 아니라 빈의 초기 상태를 설정할 수도 있다. 그런데 XML이 아닌 방법으로 스프링에 의존하지 않고 POJO의 순수성을 유지한채 특정 값을 설정하는 방법이 마땅치 않다. (적어도 내 지식으로는 그렇다.)

    애노테이션 환경에서 Property 값 설정

    PropertyInjectionConfigurer는 스프링의 PropertyPlaceholderConfigurer나 PropertyOverrideConfigurer와 비슷한 기능을 한다. 즉, 빈 초기화 단계에서 지정된 Property 파일의 값을 특정 빈의 필드에 넣거나 특정 메소드를 이 값을 가지고 호출한다. 다만 XML 설정이 아닌 애노테이션 방식을 사용한다는 것이 다르다. @Property 애노테이션을 Property 값을 받기 원하는 필드나 메소드에는 달아 놓으면 PropertyInjectionConfigurer가 빈 초기화 단계에 이 애노테이션을 인식해서 원하는 값을 찾아 설정을 해준다.

    물론 지금 1.0 M4 상태인 Spring JavaConfig나 1.0 M3 상태인 스프링 3가 정식 발표 된다면 문제가 안 되는 부분일 수 있다. 그러니 스프링 2.5만을 사용하고 JavaConfig를 기다릴 수 없는 (또는 JavaConfig를 사용 안 할) 프로젝트에만 사용하면 될 듯 하다. 그리고 JavaConfig의 @ExternalValue가 맘에 안 드는 사람도 있을 듯 하다.

    파일 다운로드

    property_injection.tar.gz

    소스 파일을 이클립스에서 Java 프로젝트(Spring IDE plugins가 설치되어 있으면 spring project)로 Import해서 열면 바로 사용이 가능하다. 일단 처음에는 테스트가 있으니 작동 유무를 이를 통해서 확인 한다.

    기존에 프로젝트 코드 베이스에 포함되어 있던 클래스 하나만 분리해서 만든 프로젝트이니 굳이 jar로 만들어 쓰기 보다는 그냥 사용할 프로젝트의 코드 베이스에 복사해서 패키지를 적절하게 변경한 다음에 사용하는 것이 좋을 듯 하다.

    설정

    가장 먼저 PropertyInjectionConfigurer를 XML에 등록한다.

        <bean class="property_injection.PropertyInjectionConfigurer">
            <property name="location" value="property_injection/test.properties"/>
        </bean>

    location 속성에 설정 값이 등록되어 있는 propertis 파일의 경로를 지정한다. 만약 properties 파일이 여러개라면 이렇게 할 수도 있다.

        <bean class="property_injection.PropertyInjectionConfigurer">
            <property name="locations">
                <list>
                    <value>classpath:property_injection/test.properties</value>
                    <value>classpath:property_injection/another.properties</value>
                </list>
            </property>
        </bean>

    물론 간단히 직접 Properties 값을 주는 것도 가능하다.

        <bean class="property_injection.PropertyInjectionConfigurer">
            <property name="properties">
                <value>
                    mail.server=mail.domain.net
                    mail.sender=admin@domain.net
                </value>
           </property>
        </bean>

    설정 방법은 PropertyPlaceholderConfigurer와 같으니 레퍼런스를 참조하도록 한다.

    필드에 @Property 사용

    일단 PropertyInjectionConfigurer를 등록했으면 Property 값을 지정하고 싶은 필드나 메소드에 @Property 애노테이션을 표기해야 한다. 직접 필드에 값을 지정하도록 할 수 있다.

        @Property(name="mail.server")
        private String mailServer;

    메소드에 @Property 사용

    메서드에 @Property를 달아 놓으면 초기화 단계에 해당 메소드를 호출한다. 이 메소드는 매개 변수가 하나 이상이어야 한다.
        
        @Property(name="mail.server")
        public void setMailServer(String mailServer) {
            this.mailServer = mailServer;
        }

    필수가 아닌 Property

    만약 지정한 Property 값을 찾지 못하면 예외를 발생시키기 때문에 빈 생성을 하지 못하게 된다. Property 값이 있어도 되고 없어도 되는 값이라면 required 속성를 false 값으로 바꾸어 예외 발생 없이 넘어가도록 할 수 있다.

      @Property(name="property.name" required=false)

    required 속성을 사용할 때에는 기본값을 필드에 지정하는 것이 좋을 것이다.

      @Property(name="mail.server" required=false)
      private String mailServer = "default.mailserver.com";


    출처 - http://gyumee.egloos.com/m/2369765








    Posted by linuxism
    ,