Container Extension Points

2024. 11. 14. 17:26Spring Framework/Spring IoC

일반적으로 애플리케이션 개발자는 ApplicationContext 구현 클래스를 서브클래싱할 필요가 없습니다. 대신, Spring IoC 컨테이너는 특수 통합 인터페이스의 구현체를 플러그인 방식으로 추가하여 확장할 수 있습니다. 다음 몇 개의 섹션에서는 이러한 통합 인터페이스를 설명합니다.

 

Customizing Beans by Using a BeanPostProcessorBeanPostProcessor 인터페이스는 콜백 메서드를 정의하여 객체 생성 논리, 의존성 해결 논리 등을 커스터마이징하거나 컨테이너의 기본 동작을 대체할 수 있도록 합니다. Spring 컨테이너가 빈을 인스턴스화하고, 구성 및 초기화 작업을 완료한 후에 사용자 정의 로직을 구현하고자 한다면, 하나 이상의 BeanPostProcessor 구현을 추가하여 이를 수행할 수 있습니다.여러 BeanPostProcessor 인스턴스를 구성할 수 있으며, 이러한 BeanPostProcessor 인스턴스의 실행 순서는 order 속성을 설정하여 제어할 수 있습니다. 단, 이 속성은 BeanPostProcessor가 Ordered 인터페이스를 구현한 경우에만 설정할 수 있습니다. 직접 BeanPostProcessor를 작성하는 경우, Ordered 인터페이스를 함께 구현하는 것도 고려해야 합니다. 자세한 내용은 BeanPostProcessor 및 Ordered 인터페이스의 javadoc을 참조하십시오. 또한, BeanPostProcessor 인스턴스의 프로그래밍 방식 등록에 대한 참고 사항도 확인하십시오.

BeanPostProcessor 인스턴스는 빈(또는 객체) 인스턴스에 대해 작동합니다. 즉, Spring IoC 컨테이너가 빈 인스턴스를 생성한 후, BeanPostProcessor 인스턴스들이 해당 작업을 수행합니다.
BeanPostProcessor 인스턴스는 컨테이너별로 스코프가 지정됩니다. 이는 컨테이너 계층 구조를 사용하는 경우에만 관련이 있습니다. 특정 컨테이너에 BeanPostProcessor를 정의하면 해당 컨테이너 내의 빈에 대해서만 후처리를 수행합니다. 다시 말해, 하나의 컨테이너에 정의된 빈은 동일한 계층에 속하더라도 다른 컨테이너에 정의된 BeanPostProcessor에 의해 후처리되지 않습니다.
실제 빈 정의(즉, 빈을 정의하는 설계도)를 변경하려면 대신 BeanFactoryPostProcessor를 사용해야 합니다. 자세한 내용은 "BeanFactoryPostProcessor로 구성 메타데이터 커스터마이징" 항목을 참조하십시오.

 

org.springframework.beans.factory.config.BeanPostProcessor 인터페이스는 정확히 두 개의 콜백 메서드로 구성됩니다. 이 클래스가 컨테이너에 Post Processor로 등록되면, 컨테이너가 생성하는 각 빈 인스턴스에 대해 초기화 메서드가 호출되기 전(예: InitializingBean.afterPropertiesSet() 또는 선언된 초기화 메서드 호출 전)과 초기화 콜백이 모두 완료된 후에 콜백을 받습니다. Post Processor는 해당 빈 인스턴스에 대해 아무 작업도 하지 않을 수 있으며, 빈을 무시할 수도 있습니다. 일반적으로 Post Processor는 콜백 인터페이스를 확인하거나 빈을 프록시로 감쌀 수 있습니다. 일부 Spring AOP 인프라 클래스는 프록시 래핑 로직을 제공하기 위해 빈 후처리기로 구현되어 있습니다.

ApplicationContext는 구성 메타데이터에 정의된 BeanPostProcessor 인터페이스를 구현하는 빈을 자동으로 감지합니다. ApplicationContext는 이러한 빈을 Post Processor로 등록하여, 빈 생성 시 호출될 수 있도록 합니다. Bean Post Processor는 다른 빈과 마찬가지로 컨테이너에 배포될 수 있습니다.

또한, 구성 클래스에서 @Bean 팩토리 메서드를 사용해 BeanPostProcessor를 선언할 때는, 팩토리 메서드의 리턴 타입을 구현 클래스 자체 또는 최소한 org.springframework.beans.factory.config.BeanPostProcessor 인터페이스로 명시하여 해당 빈의 후처리기 특성을 명확히 해야 합니다. 그렇지 않으면, ApplicationContext가 완전히 생성되기 전에는 타입을 자동 감지할 수 없습니다. BeanPostProcessor는 컨텍스트의 다른 빈 초기화에 적용되기 위해 조기에 인스턴스화되어야 하므로 이러한 조기 타입 감지가 중요합니다.

BeanPostProcessor 인스턴스를 프로그래밍 방식으로 등록하기
BeanPostProcessor를 등록하는 권장 방법은 앞서 설명한 것처럼 ApplicationContext의 자동 감지를 통한 방식이지만, ConfigurableBeanFactoryaddBeanPostProcessor 메서드를 사용하여 프로그래밍 방식으로 등록할 수도 있습니다. 이 방법은 등록 전에 조건부 논리를 평가해야 하거나, 계층 구조에서 컨텍스트 간에
BeanPostProcessor를 복사해야 하는 경우에 유용할 수 있습니다. 그러나 프로그래밍 방식으로 추가된 BeanPostProcessor 인스턴스는 `Ordered` 인터페이스를 따르지 않는다는 점에 유의해야 합니다. 이 경우 실행 순서는 등록 순서에 의해 결정됩니다. 또한, 프로그래밍 방식으로 등록된 BeanPostProcessor 인스턴스는 자동 감지를 통해 등록된 것보다 항상 먼저 처리되며, 명시적인 순서 설정과 상관없이 우선 처리됩니다.

 

BeanPostProcessor 인스턴스와 AOP auto proxy 생성
BeanPostProcessor 인터페이스를 구현하는 클래스는 특별히 취급되며, 컨테이너에 의해 다르게 처리됩니다. 모든 BeanPostProcessor 인스턴스와 그들이 직접 참조하는 빈은 ApplicationContext의 특별한 시작 단계의 일부로서 시작 시에 인스턴스화됩니다. 이후 모든 BeanPostProcessor 인스턴스는 정렬되어 등록되고, 이후 컨테이너 내의 다른 빈에 적용됩니다. AOP auto proxy는 BeanPostProcessor로 구현되어 있기 때문에, BeanPostProcessor 인스턴스와 그들이 직접 참조하는 빈은 auto proxy 생성의 타겟이 아니며, 따라서 이들에는 Aspect가 적용되지 않습니다.
이러한 빈에 대해 다음과 같은 정보성 로그 메시지가 표시될 수 있습니다: Bean someBean is not eligible for getting processed by all BeanPostProcessor interfaces (for example: not eligible for auto-proxying).
BeanPostProcessor에 @Autowired 또는 @Resource를 사용해 빈을 연결할 경우(특히, @Resource가 자동 주입으로 대체될 수 있는 경우), Spring이 타입 일치하는 의존성을 찾는 동안 예기치 않은 빈에 접근할 수 있으며, 이로 인해 해당 빈이 auto proxy 생성이나 기타 Bean Post Processor 대상이 되지 않을 수 있습니다. 예를 들어, 필드나 setter 이름이 빈의 선언된 이름과 직접적으로 일치하지 않거나 name 속성을 사용하지 않은 @Resource로 의존성을 주입할 때, Spring은 타입으로 일치하는 다른 빈에 접근하여 이를 매칭하려고 시도할 수 있습니다.

 

다음 예제는 ApplicationContext에서 BeanPostProcessor 인스턴스를 작성하고, 등록하고, 사용하는 방법을 보여줍니다.

 

Example: Hello World, BeanPostProcessor-style

다음 예제는 기본적인 사용법을 보여줍니다. 이 예제는 각 빈이 컨테이너에 의해 생성될 때 toString() 메서드를 호출하고 그 결과 문자열을 시스템 콘솔에 출력하는 사용자 정의 BeanPostProcessor 구현을 보여줍니다.

아래는 사용자 정의 BeanPostProcessor 구현 클래스 정의입니다:

package scripting;

import org.springframework.beans.factory.config.BeanPostProcessor;

public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {

	// simply return the instantiated bean as-is
	public Object postProcessBeforeInitialization(Object bean, String beanName) {
		return bean; // we could potentially return any object reference here...
	}

	public Object postProcessAfterInitialization(Object bean, String beanName) {
		System.out.println("Bean '" + beanName + "' created : " + bean.toString());
		return bean;
	}
}

 

InstantiationTracingBeanPostProcessor 이것도 빈이기 때문에 다른 빈처럼 의존성 주입이 가능합니다. 

 

Example: The AutowiredAnnotationBeanPostProcessor

콜백 인터페이스나 애노테이션을 사용자 정의 BeanPostProcessor 구현과 함께 사용하는 것은 Spring IoC 컨테이너를 확장하는 일반적인 방법입니다. 예로는 Spring 배포에 포함된 AutowiredAnnotationBeanPostProcessor가 있습니다. 이는 BeanPostProcessor의 구현체로, 애노테이션이 적용된 필드, 세터 메서드, 임의의 구성 메서드에 자동으로 의존성을 주입합니다.

 

Customizing Configuration Metadata with a BeanFactoryPostProcessor

다음으로 살펴볼 확장 포인트는 org.springframework.beans.factory.config.BeanFactoryPostProcessor입니다. 이 인터페이스의 의미는 BeanPostProcessor와 유사하지만 중요한 차이점이 하나 있습니다. BeanFactoryPostProcessor는 빈 구성 메타데이터에 작동합니다. 즉, Spring IoC 컨테이너는 BeanFactoryPostProcessor가 빈 구성 메타데이터를 읽고, 필요하다면 BeanFactoryPostProcessor 인스턴스를 제외한 모든 빈이 인스턴스화되기 전에 해당 메타데이터를 수정할 수 있도록 허용합니다.

 

여러 BeanFactoryPostProcessor 인스턴스를 구성할 수 있으며, order 속성을 설정하여 이들의 실행 순서를 제어할 수 있습니다. 그러나 이 속성은 BeanFactoryPostProcessor가 Ordered 인터페이스를 구현한 경우에만 설정할 수 있습니다. 직접 BeanFactoryPostProcessor를 작성하는 경우, Ordered 인터페이스를 함께 구현하는 것을 고려해야 합니다. 자세한 내용은 BeanFactoryPostProcessor와 Ordered 인터페이스의 javadoc을 참조하십시오.

실제 빈 인스턴스(즉, 구성 메타데이터에서 생성된 객체)를 변경하려면, BeanPostProcessor를 사용해야 합니다 (이전 섹션 "BeanPostProcessor를 사용한 빈 사용자 정의" 참조). 기술적으로 BeanFactoryPostProcessor 내에서 빈 인스턴스를 다룰 수는 있지만(예: BeanFactory.getBean() 사용), 이렇게 하면 빈의 조기 인스턴스화를 유발하여 표준 컨테이너 라이프사이클을 위반하게 됩니다. 이는 Bean Post Processor 과정을 우회하는 등의 부정적인 부작용을 초래할 수 있습니다. 또한, BeanFactoryPostProcessor 인스턴스는 컨테이너별로 스코프가 지정됩니다. 이는 컨테이너 계층 구조를 사용하는 경우에만 관련이 있습니다. 특정 컨테이너에 BeanFactoryPostProcessor를 정의하면, 해당 컨테이너 내의 빈 정의에만 적용됩니다. 동일한 계층에 속하더라도, 한 컨테이너에 정의된 빈 정의는 다른 컨테이너에 정의된 BeanFactoryPostProcessor에 의해 후처리되지 않습니다.

 

BeanFactoryPostProcessor는 ApplicationContext에 선언될 때 자동으로 실행되며, 컨테이너를 정의하는 구성 메타데이터에 변경 사항을 적용합니다. Spring에는 PropertyOverrideConfigurerPropertySourcesPlaceholderConfigurer와 같은 여러 사전 정의된 BeanFactoryPostProcessor가 포함되어 있습니다. 또한, 사용자 정의 BeanFactoryPostProcessor를 사용하여 예를 들어, 사용자 정의 속성 편집기를 등록할 수도 있습니다.

ApplicationContext는 BeanFactoryPostProcessor 인터페이스를 구현하는 빈을 자동으로 감지하여 적절한 시점에 BeanFactoryPostProcessor로 사용합니다. 이러한 빈은 다른 빈과 마찬가지로 ApplicationContext에 배포할 수 있습니다.

BeanPostProcessor와 마찬가지로, 일반적으로 BeanFactoryPostProcessor에 대해 Lazy 초기화를 설정하지 않는 것이 좋습니다. 다른 빈이 Bean(Factory)PostProcessor를 참조하지 않으면, 해당 PostProcessor는 전혀 인스턴스화되지 않습니다. 따라서 지연 초기화를 설정해도 무시되며, <beans /> 엘리먼트 선언에서 default-lazy-init 속성을 true로 설정하더라도 Bean(Factory)PostProcessor는 즉시 초기화됩니다.

 

Example: The Class Name Substitution PropertySourcesPlaceholderConfigurer

 

jdbc.driverClassName=org.hsqldb.jdbcDriver
jdbc.url=jdbc:hsqldb:hsql://production:9002
jdbc.username=sa
jdbc.password=root
import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.beans.factory.annotation.Value;
import javax.sql.DataSource;

@Configuration
@PropertySource("classpath:com/something/jdbc.properties")
public class AppConfig {

    // PropertySourcesPlaceholderConfigurer를 빈으로 등록하여 플레이스홀더를 해석
    @Bean
    public static PropertySourcesPlaceholderConfigurer propertyPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }

    // 환경 설정에서 외부 파일의 속성 값을 로드하여 DataSource 빈 설정
    @Bean(destroyMethod = "close")
    public DataSource dataSource(
            @Value("${jdbc.driverClassName}") String driverClassName,
            @Value("${jdbc.url}") String url,
            @Value("${jdbc.username}") String username,
            @Value("${jdbc.password}") String password) {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName(driverClassName);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }
}

 

코드 설명:

  1. @PropertySource: 이 어노테이션은 classpath:com/something/jdbc.properties 파일에서 속성 값을 가져옵니다. 이 파일에 jdbc.driverClassName, jdbc.url, jdbc.username, jdbc.password와 같은 설정이 포함되어 있어, DataSource 빈에서 사용할 값을 정의합니다.
  2. PropertySourcesPlaceholderConfigurer 빈 정의: propertyPlaceholderConfigurer() 메서드에서 PropertySourcesPlaceholderConfigurer 빈을 생성하여 등록합니다. 이 빈은 Spring이 플레이스홀더 형식("${property-name}")을 해석하도록 하여, 외부 Properties 파일에 정의된 값을 코드 내에서 사용할 수 있도록 합니다.
  3. @Value 애노테이션을 통한 속성 값 주입: @Value를 사용하여 DataSource 빈 내 driverClassName, url, username, password와 같은 속성 필드에 외부 파일의 값을 주입합니다. 예를 들어, ${jdbc.username}은 jdbc.properties 파일의 jdbc.username 속성 값을 가져와 sa로 치환합니다.
  4. 실행 시 치환: PropertySourcesPlaceholderConfigurer는 DataSource 빈의 속성 값에 대해 Properties 파일의 플레이스홀더 값을 치환합니다. 만약 파일에 정의된 속성이 없다면, 기본적으로 Spring Environment나 Java 시스템 속성(System Properties)을 참조하여 값을 확인합니다.
  5. 추가 설정 방법: @PropertySource에서 여러 Properties 파일을 쉼표로 구분하여 지정할 수 있습니다.

어플리케이션에서 필요한 속성들을 제공하기 위해 하나의 PropertySourcesPlaceholderConfigurer 요소만 정의하는 것이 좋습니다. 여러 개의 속성 플레이스홀더를 사용하려면, 각 플레이스홀더는 서로 구별되는 플레이스홀더 구문(${…​} 형식)으로 구성해야 합니다.

속성 소스를 모듈화해야 할 경우, 다수의 속성 플레이스홀더를 생성하지 않는 것이 좋습니다. 대신, 필요한 속성들을 모아 활용할 수 있도록 커스텀 PropertySourcesPlaceholderConfigurer 빈을 만들어 사용하는 것이 바람직합니다.

 

Example: The PropertyOverrideConfigurer

PropertyOverrideConfigurer는 또 다른 BeanFactoryPostProcessor로, PropertySourcesPlaceholderConfigurer와 유사하지만, 후자의 경우와 달리 원래 정의에는 빈 속성에 대해 디폴트 값이 있거나 값이 없어도 됩니다. 만약 덮어쓰는 속성 파일에 특정 빈 속성에 대한 항목이 없다면, 디폴트 컨텍스트 정의가 사용됩니다.

빈 정의는 자신이 덮어쓰여졌다는 것을 인식하지 못하기 때문에, XML 정의 파일에서는 덮어쓰기 설정자가 사용되고 있다는 것이 즉시 명확하게 보이지 않습니다. 동일한 빈 속성에 대해 다른 값을 정의하는 여러 PropertyOverrideConfigurer 인스턴스가 있는 경우, 덮어쓰기 메커니즘에 의해 마지막에 정의된 값이 우선합니다.

속성 파일 구성 라인은 다음 형식을 따릅니다.

beanName.property=value

 

다음 예제는 이 형식에 대한 예시를 보여줍니다:

dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql:mydb

 

이 예제 파일은 driverClassName과 url 속성을 가진 dataSource라는 빈을 포함하는 컨테이너 정의와 함께 사용할 수 있습니다.

복합 속성 이름도 지원되며, 덮어쓰려는 마지막 속성을 제외한 경로의 모든 구성 요소가 이미 null이 아닌 경우(생성자에 의해 초기화된 것으로 추정) 가능합니다. 다음 예제에서는 tom 빈의 fred 속성의 bob 속성의 sammy 속성을 스칼라 값 123으로 설정합니다.

tom.fred.bob.sammy=123

 

Customizing Instantiation Logic with a FactoryBean

org.springframework.beans.factory.FactoryBean 인터페이스는 그 자체가 팩토리인 객체를 위한 인터페이스입니다.

FactoryBean 인터페이스는 Spring IoC 컨테이너의 인스턴스화 로직에 유연성을 제공합니다. 복잡한 초기화 코드가 많은 XML 대신 Java로 표현되는 것이 더 나은 경우, 사용자 정의 FactoryBean을 생성하고 해당 클래스 내부에 복잡한 초기화 로직을 작성한 다음, 이 사용자 정의 FactoryBean을 컨테이너에 연결할 수 있습니다.

FactoryBean<T> 인터페이스는 다음 세 가지 메서드를 제공합니다:

  • T getObject(): 이 팩토리가 생성하는 객체의 인스턴스를 반환합니다. 이 팩토리가 싱글톤을 반환하는지 프로토타입을 반환하는지에 따라 인스턴스를 공유할 수 있습니다.
  • boolean isSingleton(): 이 FactoryBean이 싱글톤을 반환하면 true를 반환하고, 그렇지 않으면 false를 반환합니다. 이 메서드의 기본 구현은 true를 반환합니다.
  • Class<?> getObjectType(): getObject() 메서드가 반환하는 객체의 타입을 반환합니다. 미리 타입을 알 수 없는 경우 null을 반환합니다.

Spring 프레임워크 내부에서는 FactoryBean 개념과 인터페이스가 여러 곳에서 사용됩니다. Spring 자체에 50개 이상의 FactoryBean 인터페이스 구현이 포함되어 있습니다.

컨테이너에서 실제 FactoryBean 인스턴스를 요청할 필요가 있을 때는, ApplicationContext의 getBean() 메서드를 호출할 때 빈의 ID 앞에 앰퍼샌드 심볼(`&`)을 붙여야 합니다. 예를 들어, ID가 myBean인 FactoryBean이 있는 경우, 컨테이너에서 getBean("myBean")을 호출하면 FactoryBean이 생성한 객체를 반환하고, getBean("&myBean")을 호출하면 FactoryBean 인스턴스 자체를 리턴합니다.