https://sundaland.tistory.com/207
[ ▶ Java-based Container Configuration ]
[ ▷ Basic Concepts: @Bean and @Configuration ]
스프링의 자바 구성 지원에서 핵심 아프택트는 @Configuration 어노테이션이 달린 클래스와 @Bean 어노테이션이 달린 메서드이다.
@Bean 어노테이션은 메서드가 스프링 IoC 컨테이너에서 관리할 새 객체를 인스턴스화하고, 구성하고, 초기화한다는 것을 나타내는데 사용된다. 스프링의 <bean/> XML 구성에 익숙한 사람들에게 @Bean 어노테이션은 <Bean/> 엘리먼트와 동일한 역할을 한다. @Bean 어노테이션이 달린 메서드는 모든 스프링 @Componenet와 함께 사용할 수 있다. 그러나 @Configuration 빈과 함께 가장 많이 사용된다.
클래스에 @Configuration 어노테이션을 달면 해당 클래스의 주요 목적이 빈 정의 소스라는 것을 나타낸다. 또한 @Configuration 클래스는 동일한 클래스에서 @Bean 메서드를 호출하여 빈 간 종속성을 정의할 수 있다.
▼ 가능한 가장 간단한 @Configuration 클래스
@Configuration
public class AppConfig {
@Bean
public MyServiceImpl myService() {
return new MyServiceImpl();
}
}
AppConfig 클래스는 <beans/> XML과 동일하다.
@Configuration 클래스 내에서 @Bean 메서드들 간의 호출 여부
일반적인 시나리오에서, @Bean 메서드는 @Configuration 클래스로 선언되어야 하며, 이를 통해 전체 구성 클래스 처리가 적용되고 메서드 간 참조가 컨테이너의 라이프사이클 관리로 리디렉션된다. 이렇게 하면 동일한 @Bean 메서드가 일반 자바 메서드 호출을 통해 실수로 호출되는 것을 방지할 수 있으며, 추적하기 어려운 미묘한 버그를 줄이는데 도움이 된다.
@Configuration으로 어노테이션 처리되지 않은 클래스 내에서 @Bean 메서드가 선언되거나 @Configuration(proxyBeanMethods = false)로 선언된 경우, 이러한 메서드들은 라이트 모드로 처리된다. 이러한 시나리오에서는 @Bean 메서드가 특별한 런타임 처리가 없는 일반적인 팩토리 메서드 메커니즘으로 동작한다. (CGLIB 서브클래스를 생성하지 않는다). 따라서 이러한 메서드에 대한 커스텀 자바 호출은 컨테이너에 의해 인터셉트되지 않으며, 주어진 빈에 대한 기존의 싱글톤 (또는 스코프된) 인스턴스를 재사용하는 대신 매번 새로운 인스턴스를 생성하게 된다.
결과적으로 런타임 프록시가 없는 클래스에서의 @Bean 메서드는 빈 간의 의존성을 선언하는데 사용되지 않는다. 대신 이들은 주로 자신이 속한 컴포넌트의 필드와 팩토리 메서드에서 선언할 수 있는 아규먼트를 통해 주입된 협력자에 의존하도록 기대된다. 이러한 @Bean 메서드는 다른 @Bean 메서드를 호출할 필요가 없으며, 모든 호출은 팩토리 메서드의 아규먼트를 통해 표현될 수 있다. 여기서 긍정적인 부수 효과는 런타임 시 CGLIB 서브클래싱이 적용될 필요가 없기에 오버헤드와 메모리 사용량이 감소한다.
[ ▷ Instantiating the Spring Container by Using AnnotationConfigApplicationContext ]
스프링의 AnnotationConfigApplicationContext의 구현은 @Configuration 클래스뿐만 이나리 일반 @Component 클래스와 JSR-330 메타데이터로 어노테이션이 달린 클래스도 입력으로 허용할 수 있다.
@Configuration 클래스가 입력으로 제공되면 @Configuration 클래스 자체가 빈 정의로 등록되고 클래스 내에서 선언된 모든 @Bean 메서드도 빈 정의로 등록된다.
@Component 및 JSR-330 클래스가 제공되면 빈 정의로 등록되고 필요한 경우 @Autowired 또는 @Inject와 같은 DI 메타데이터가 해당 클래스 내에서 사용된다고 가정한다.
[ ▷ Simple Construction ]
ClassPathXmlApplicationContext를 인스턴스화할 때 Spring XML 파일이 입력으로 사용되는 것과 거의 같은 방식으로 AnnotationConfigApplicationContext를 인스턴스화할 떄 @Configuration 클래스를 입력으로 사용할 수 있다.
▼ XML 없이 사용하는 스프링 컨테이너
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
AnnotationConfigApplicationContext는 @Configuration 클래스에서만 작동하는데 국한되지 않는다.
아래의 예제는 @Component 또는 JSR-330 주석이 달린 클래스는 생성자에 입력으로 제공될 수 있다.
public static void main(String[] args) {
ApplicationContext ctx =
new AnnotationConfigApplicationContext(MyServiceImpl.class,
Dependency1.class, Dependency2.class);
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
윗 예제는 MyServiceImpl. Dependency1, Dependency2가 @Autowired와 같은 스프링 DI 어노테이션을 사용한다고 가정한다.
[ ▷ Building the Container Programmatically by Using register(Class<?>…) ]
아규먼트가 없는 생성자를 사용하여 AnnotationConfigApplicationContext를 인스턴스화한 다음 register() 메서드를 사용하여 구성할 수 있다. 이 접근 방식은 AnnotationConfigApplicationContext를 프로그래밍 방식으로 빌드할 떄 유용하다.
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(AppConfig.class, OtherConfig.class);
ctx.register(AdditionalConfig.class);
ctx.refresh();
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
[ ▷ Enabling Component Scanning with scan(String…) ]
@Configuration
@ComponentScan(basePackages = "com.acme")
public class AppConfig {
// ...
}
com.acme 패키지는 @Component 어노테이션이 달린 클래스를 찾기 위해 스캔되고, 해당 클래스는 컨테이너 내에서 스프링 빈 정의로 등록된다.
AnnotationConfigApplicationContext는 동일한 컴포넌트 스캔 기능을 허용하기 위해 scan(String...) 메서드를 노출한다.
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.scan("com.acme");
ctx.refresh();
MyService myService = ctx.getBean(MyService.class);
}
@Configuration 클래스는 @Component로 메타 어노테이션이 달려있으므로 컴포넌트 스캐닝 후보이다. 위 예제어서 AppConfig가 com.acme 패키지 (또는 그 아래의 패키지)내에서 선언되었다고 가정하면 scan()을 호출하는 동안 선택된다.
Refresh() 시 모든 @Bean 메서드가 처리되고 컨테이너 내에서 빈 정의로 등록된다.
[ ▷ Support for Web Applications with AnnotationConfigWebApplicationContext ]
AnnotationConfigApplicationContext의 WebApplicationContext 변형은 AnnotationConfigWebApplicationContext와 함께 사용할 수 있다. 이 구현은 Spring ContextLoaderListener 서블릿 리스너, 스프링 MVC DispatcherServlet 등을 구성할 때 사용할 수 있다.
▼ 일반적인 스프링 MVC 웹 애플리케이션 구성
<web-app>
<!-- Configure ContextLoaderListener to use AnnotationConfigWebApplicationContext
instead of the default XmlWebApplicationContext -->
<context-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</context-param>
<!-- Configuration locations must consist of one or more comma- or space-delimited
fully-qualified @Configuration classes. Fully-qualified packages may also be
specified for component-scanning -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.acme.AppConfig</param-value>
</context-param>
<!-- Bootstrap the root application context as usual using ContextLoaderListener -->
<listener>
<listener-class>org.springframework.web.context.
ContextLoaderListener</listener-class>
</listener>
<!-- Declare a Spring MVC DispatcherServlet as usual -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- Configure DispatcherServlet to use AnnotationConfigWebApplicationContext
instead of the default XmlWebApplicationContext -->
<init-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.
AnnotationConfigWebApplicationContext
</param-value>
</init-param>
<!-- Again, config locations must consist of one or more comma- or space-delimited
and fully-qualified @Configuration classes -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.acme.web.MvcConfig</param-value>
</init-param>
</servlet>
<!-- map all requests for /app/* to the dispatcher servlet -->
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>
</web-app>
프로그래밍적 사용 사례의 경우 GenericWebApplicationContext를 AnnotationConfigWebApplicationContext의 대안으로 사용할 수 있다.
[ ▷ Using the @Bean Annotation ]
@Bean은 메서드 레발 어노테이션이며 XML <bean/> 엘리먼트와 직접적으로 대응된다. 이 어노테이션은 <bean/>에서 제공하는 일부 속성을 지원한다.
- init-method
- destory-method
- autowiring
- name
@Bean 어노테이션은 @Configuration 어노테이션이 붙은 클래스나 @Component 어노테이션이 붙은 클래스를 사용할 수 있다.
[ ▷ 빈 선언 ]
빈을 선언하려면 @Bean 어노테이션으로 메서드에 어노테이션을 달 수 있다. 이 메서드를 사용하여 메서드의 리턴 값으로 지정된 타입의 ApplicationContext 내에 빈 정의를 등록한다. 기본적으로 빈 이름은 메서드 이름과 동일하다.
@Configuration
public class AppConfig {
@Bean
public TransferServiceImpl transferService() {
return new TransferServiceImpl();
}
}
TransferServiceImpl 타입의 객체 인스턴스에 바인딩된 transferService라는 이름의 빈을 ApplicationContext에서 사용할 수 있게 한다.
transferService -> com.acme.TransferServiceImpl
디폴트 메서드를 사용하여 빈을 정의할 수 도 있다. 이를 통해 디폴트 메서드에서 빈 정의가 있는 인터페이스를 구현하여 빈 구성을 구성할 수 있다.
public interface BaseConfig {
@Bean
default TransferServiceImpl transferService() {
return new TransferServiceImpl();
}
}
@Configuration
public class AppConfig implements BaseConfig {
}
▼ 인터페이스(또는 베이스 클래스) 리턴 유형으로 @Bean 메서드를 선언할 수 있다.
@Configuration
public class AppConfig {
@Bean
public TransferService transferService() {
return new TransferServiceImpl();
}
}
그러나, 이는 고급 타입 예측의 가시성을 지정된 인터페이스 타입 (TransferService)으로 제한한다. 전체 타입 (TransferServiceImpl)은 해당 싱글톤 빈이 인스턴스화된 후에야 컨테이너에 알려지게 딘다. 비지연 (non-lazy) 싱글톤 빈들은 선언된 순서에 따라 인스턴스화되기 때문에, 다른 컴포넌트가 선언되지 않은 타입을 기준으로 매칭을 시도할 때, 이 빈이 인스턴스화된 시점에 따라 다른 타입 매칭 결과가 나타날 수 있다.
선언된 서비스 인터페이스로 타입을 일관되게 참조하는 경우 @Bean 리턴 타입이 해당 디자인 결정에 안전하게 결합될 수 있다. 그러나 여러 인터페이스를 구현하는 컴포넌트나 구현 타입으로 잠재적으로 참조되는 컴포넌트의 경우 가능한 가장 구체적인 리턴 타입을 선언하는 것이 더 안전하다. (적어도 빈을 참조하는 주입 지점에서 요구하는 만큼 구체적이다.)
[ ▷ 빈 의존성 주입 ]
@Bean 어노테이션이 붙은 메서드는 해당 빈을 빌드하는데 필요한 종속성을 설명하는 임의의 수의 파라미터를 가질 수 있다. 예를 들어 TransferService에 AccountRepository가 필요한 경우 아래와 같이 메서드 파라미터로 해당 종속성을 구체화할 수 있다.
@Configuration
public class AppConfig {
@Bean
public TransferService transferService(AccountRepository accountRepository) {
return new TransferServiceImpl(accountRepository);
}
}
해당 메커니즘은 생성자 기반 종속성 주입과 거의 동일하다.
[ ▷ Receiving Lifecycle Callbacks ]
@Bean 어노테이션으로 정의된 모든 클래스는 일반 라이프사이클 콜백을 지원하고 JSR-250의 @PostConstruct 및 @PreDestory 어노테이션을 사용할 수 있다.
일반적인 스프링 라이프사이클 콜백도 완벽하기 지원한다. 빈이 InitializingBean, DisposableBean 또는 라이프사이클을 구현하는 경우 해당 메서드는 컨테이너에서 호출된다.
BeanFactoryAware, BeanNameAware, MessageSourceAware, ApplicationContextAware 등과 같은 표준 *Aware 인터페이스 세트도 완벽하게 지원한다.
@Bean 어노테이션은 빈 요소에 스프링 XML의 init-method 및 destory-method 속성과 매우 유사하게 임의의 init 및 destory 콜백 메서드를 지정하는 것을 지원한다.
public class BeanOne {
public void init() {
// initialization logic
}
}
public class BeanTwo {
public void cleanup() {
// destruction logic
}
}
@Configuration
public class AppConfig {
@Bean(initMethod = "init")
public BeanOne beanOne() {
return new BeanOne();
}
@Bean(destroyMethod = "cleanup")
public BeanTwo beanTwo() {
return new BeanTwo();
}
}
기본적으로 자바 구성으로 정의된 빈은 public close 또는 shutdown 메서드가 있고 자동으로 destory 콜백에 등록된다. public close 또는 shutdown 메서드가 있고 컨테이너가 종료될 떄 호출되지 않도록 하려면 빈 정의에 @Bean(destoryMethod = "")을 추가하여 디폴트(추론된) 모드를 비활성화할 수 있다.
JSDI로 획득한 리소스의 경우 기본적으로 이를 수행할 수 있다. 해당 리로스의 수명 주기는 애플리케이션 외부에서 관리되기 때문이다. 특히 Jakarta EE 애플리케이션 서버에[서 문제가 되는 것으로 알려져 있으므로 DataSource의 경우 항상 이를 수행해야 한다.
▼ DatatSource에 대한 자동 destruction 콜백을 방지하는 방법
@Bean(destroyMethod = "")
public DataSource dataSource() throws NamingException {
return (DataSource) jndiTemplate.lookup("MyDS");
}
또한 @Bean 메서드에서는 일반적으로 스프링의 JndiTemplate 또는 JndiLocatorDelegate 헬퍼를 사용하거나 직접 JNDI InitialContext를 사용하지만 JndiObjectFactoryBean 변형은 사용하지 않는 프로그램적 JNDI 조회를 사용한다.
# 이 경우 실제 대상 유형 대신 FactoryBean 유형으로 반환 유형을 선언해야 하므로 여기에 제공된 리소스를 참조하려는 다른 @Bean 메서드에서 교차 참조 호출에 사용하기 어렵다.
▼ 생성 중에 init() 메서드를 직접 호출하는 방법
@Configuration
public class AppConfig {
@Bean
public BeanOne beanOne() {
BeanOne beanOne = new BeanOne();
beanOne.init();
return beanOne;
}
// ...
}
자바에서 직접 작업하는 경우 객체를 원하는 대로 조족할 수 있으며 항상 컨테이너 라이프사이클에 의존할 필요가 없다.
[ ▷ Specifying Bean Scope ]
스프링에는 @Scope 어노테이션이 포함되어 있어 빈의 범위를 지정할 수 있다.
Using the @Scope Annotation
@Bean 어노테이션으로 정의된 빈이 특정 Scope를 가져야 한다고 지정할 수 있다. BeanScopes 색션에 지정된 표준 범위를 사용할 수 있따.
디폴트 스코프는 싱글톤이지만 아래 예제와 같이 @Scopㄷ 어노테이션으로 이를 재정의할 수 있다.
@Configuration
public class MyConfiguration {
@Bean
@Scope("prototype")
public Encryptor encryptor() {
// ...
}
}
@Scope and scoped-proxy
스프링은 스코프가 지정된 프록시를 통해 스코프가 지정된 종속성을 처리하는 편리한 방법을 제공한다. XML 구성을 사용할 때 이러한 프록시를 만드는 가장 쉬운 방법은 <aop:scoped-proxy/> 엘리먼트이다. @Scope 어노테이션으로 자바에서 빈을 구성하면 proxyMode 특성으로 동일한 지원을 제공한다. 기본 값은 ScopedProxyMode.DEFAULT이며, 일반적으로 컴포넌트 스캔 지시 레벨에서 다른 디폴트 값이 구성되지 않는 한 스코프가 지정된 프로시를 만들지 않아야 함을 나타낸다. ScopedProxyMode.TARGET_CLASS, ScopedProxyMode.INTERFACES 또는 ScopedProxyMode.NO를 지정할 수 있다.
// an HTTP Session-scoped bean exposed as a proxy
@Bean
@SessionScope
public UserPreferences userPreferences() {
return new UserPreferences();
}
@Bean
public Service userService() {
UserService service = new SimpleUserService();
// a reference to the proxied userPreferences bean
service.setUserPreferences(userPreferences());
return service;
}
[ ▷ Customizing Bean Naming ]
기본적으로 구성 클래스는 @Bean 메서드의 이름을 결과 빈의 이름으로 사용한다. 그러나 이 기능은 아래의 예제처럼 name 속성을 사용하여 재정의할 수 있다.
@Configuration
public class AppConfig {
@Bean("myThing")
public Thing thing() {
return new Thing();
}
}
[ ▷ Bean Aliasing ]
단일 빈에 여러 이름을 부여하는 것이 바람직한데 이를 빈 별칭이라고 한다. @Bean 어노테이션의 name 속성은 이 목적을 위해 문자열 배열을 허용한다.
@Configuration
public class AppConfig {
@Bean({"dataSource", "subsystemA-dataSource", "subsystemB-dataSource"})
public DataSource dataSource() {
// instantiate, configure and return DataSource bean...
}
}
[ ▷ Bean Description ]
때로는 빈에 대한 보다 자세한 텍스트 설명을 제공하는 것이 도움이 된다. 이는 모니터링 목적으로 빈을 노출될 떄 (아마도 JAX를 통해) 유용할 수 있다.
@Configuration
public class AppConfig {
@Bean
@Description("Provides a basic example of a bean")
public Thing thing() {
return new Thing();
}
}
[ ▶ Using the @Configuration annotation ]
@Configuration은 객체가 빈 정의 소스임을 나타내는 클래스 레벨 어노테이션이다. @Configuration 클래스는 @Bean 어노테이션이 달린 메서드를 통해 빈을 선언한다.
@Configuration 클래스에서 @Bean 메서드에 대한 호출을 빈 간 종속성을 정의하는 데에도 사용할 수 있다.
[ ▷ Injecting Inter-bean Dependencies ]
빈이 서로 종속성을 가질 때, 그 종속성을 표현하는 것은 아래 예제에 보이듯, 한 빈 메서드가 다른 빈 메서드를 호출하는 것이다.
@Configuration
public class AppConfig {
@Bean
public BeanOne beanOne() {
return new BeanOne(beanTwo());
}
@Bean
public BeanTwo beanTwo() {
return new BeanTwo();
}
}
beanOne은 생성자 주입을 통해 beanTwo에 대한 참조를 받는다.
이 빈 간 종속성 선언 방법은 @Bean 메서드가 @Configuration 클래스 내에서 선언될때만 작동한다. 일반 @Component 클래스를 사용하여 빈간 종속성을 선언할 수 없다.
[ ▷ Lookup Method Injection ]
앞서 언급했듯이 lookup method injection은 드믈게 사용해야하는 고급 기능이다. 싱글톤 스코프의 빈이 프로토타입 스코프의 빈이 프로토타입 스코프의 빈에 종속되어 있는 경우에 유용하다. 이러한 유형의 구성에 자바를 사용하면 이 패턴을 구현하는 자연스러운 수단이 제공된다.
▼ lookup method injection를 사용하는 방법
public abstract class CommandManager {
public Object process(Object commandState) {
// grab a new instance of the appropriate Command interface
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}
// okay... but where is the implementation of this method?
protected abstract Command createCommand();
}
자바 구성을 사용하면 abstract createCommand() 메서드가 새 (프로토타입) Command 클래스 객체를 찾는 방식으로 재정의되는 CommandManager의 하위 클래스를 만들 수 있다.
@Bean
@Scope("prototype")
public AsyncCommand asyncCommand() {
AsyncCommand command = new AsyncCommand();
// inject dependencies here as required
return command;
}
@Bean
public CommandManager commandManager() {
// return new anonymous implementation of CommandManager with createCommand()
// overridden to return a new prototype Command object
return new CommandManager() {
protected Command createCommand() {
return asyncCommand();
}
}
}
[ ▷ Further Information About How Java-based Configuration Works Internally ]
@Configuration
public class AppConfig {
@Bean
public ClientService clientService1() {
ClientServiceImpl clientService = new ClientServiceImpl();
clientService.setClientDao(clientDao());
return clientService;
}
@Bean
public ClientService clientService2() {
ClientServiceImpl clientService = new ClientServiceImpl();
clientService.setClientDao(clientDao());
return clientService;
}
@Bean
public ClientDao clientDao() {
return new ClientDaoImpl();
}
}
clientDao()는 clientservice1()에서 한 번, clientService2()에서 한 번 호출된다. 이 메서드는 ClientDaoImpl의 새 인스턴스를 생성하여 리턴하므로 일반적으로 두 개의 인스턴스 (각 서비스당 하나씩)가 있을 것으로 예상한다. 스프링에서 인스턴스화된 빈은 기본적으로 싱글톤 스코프를 갖는다. 모든 @Configuration 클래스는 시작 시 CGLIB로 서브 클래싱된다. 서브 클래스에서 자식 메서드는 부모 메서드를 호출하고 새 인스턴스를 만들기 전에 먼저 컨테이너에서 캐시된 (범위가 지정된) 빈을 확인한다.
CGLIB 클래스는 org.springframework.cglib 패키지로 다시 패키지딩되어 spring-core JAR에 직접 포함되므로 클래스 경로에 CGLIB를 추가할 필요가 없다.
CGLIB이 시작 지점에 동적으로 기능을 추가하기 때문에 몇 가지 제한 사항이 있다. 특히 구성 클래스는 final로 선언되어서는 안된다. 그러나 구성 클래스에서 생성자는 @Autowired를 사용하거나 디폴트가 아닌 생성자를 선언하여 디폴트 주입을 위한 단일 생성자를 포함하여 모든 생성자가 허용된다.
만약 CGLIB에 의해 부과되는 제한을 피하고 싶다면, @Bean 메서드를 non-@Configuration 클래스에서 선언하거나, 구성 클래스에 @Configuration(proxyBeanMethod = false)로 어노테이션을 지정하는 것을 고려해야한다. 이 경우 @Bean 메서드간의 교차 호출이 인터셉트되지 않으므로 생성자나 메서드 수준의 의존성 주입에만 의존해야 한다.
[ ▷ Composing Java-based Configurations ]
스프링의 자바 기능 구성 기능을 사용하면 어노테이션을 작성할 수 있어 구성의 복잡성을 줄일 수 있다.
[ ▷ Using the @Import Annotation ]
<import/> 엘리먼트가 스프링 XML 파일 내에서 구성의 모듈화를 돕기 위해 사용되는 것처럼, @Import 어노테이션을 사용하면 다른 구성 클래스에서 @Bean 정의를 로드할 수 있다.
@Configuration
public class ConfigA {
@Bean
public A a() {
return new A();
}
}
@Configuration
@Import(ConfigA.class)
public class ConfigB {
@Bean
public B b() {
return new B();
}
}
이제 컨텍스트를 인스턴스화할 때 ConfigA.class와 ConfigB.class를 모두 지정할 필요 없이 아래와 같이 ConfigB 만 명시적으로 제공하면 된다.
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class);
// now both beans A and B will be available...
A a = ctx.getBean(A.class);
B b = ctx.getBean(B.class);
}
이 접근 방식은 컨테이너 인스턴스화를 간소화한다. 컨테이너를 생성하는 동안 잠재적으로 많은 수의 @Configuration 클래스를 기억할 필요가 없고, 하나의 클래스만 처리하면 된다.
@Import 어노테이션은 일반 컴포넌트 클래스에 대한 참조를 지원한다. 이는 AnnotationConfigApplicationContext.register 메서드와 유사한 방식으로 작동하며, 컴포넌트 스캐닝을 피하고 몇 개의 구성 클래스를 사용하여 모든 컴포넌트를 명시적으로 정의하고자 할 떄 특히 유용하다.
[ ▷ @Import의 기본 개념 ]
구성 클래스에 대란 구성 클래스나 컴포넌트 클래스를 포함시킬 떄 사용된다. 이를 통해 여러 구성 클래스를 하나의 중앙 구성 클래스로 묶을 수 있으며, 이로 인해 구성으 모듈화가 가능해진다. 명시적으로 관리하고 싶은 컴포넌트만 등록하여 구성할 수 있다.
1. 일반 컴포넌트 클래스 정의
import org.springframework.stereotype.Component;
@Component
public class MyService {
public void performService() {
System.out.println("Service is being performed.");
}
}
@Component
public class MyRepository {
public void performRepositoryAction() {
System.out.println("Repository action performed.");
}
}
2. 설정 클래스 정의 및 @Import 사용
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import({MyService.class, MyRepository.class})
public class AppConfig {
// 다른 설정이 있을 경우 여기에 추가
}
3. 메인 메서드에서 스프링 컨텍스트 사용
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class MainApp {
public static void main(String[] args) {
// ApplicationContext 초기화 및 설정 클래스 로드
ApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
// MyService와 MyRepository 빈을 가져와서 사용
MyService myService = context.getBean(MyService.class);
myService.performService();
MyRepository myRepository = context.getBean(MyRepository.class);
myRepository.performRepositoryAction();
}
}
- MyService와 MyRepository : 각각 @Component 어노테이션이 붙은 일반 컴포넌트 클래스이다. 이 클래스들은 서비스 로직과 리포지토리 동작을 담당한다.
- AppConfig 클래스 : 스프링 구성 클래스이다. @Import 어노테이션을 사용하여 MyService와 MyRepository 컴포넌트 클래스를 명시적으로 등록한다. 이 방식으로 컴포넌트 스캐닝 없이도 필요한 컴포넌트만 설정에 포함할 수 있다.
- MainApp 클래스 : AnnotationConfigApplicationContext를 사용하여 AppConfig 설정 클래스를 로드하고, 등록된 빈을 가져와서 사용한다. MyService와 MyRepository 빈이 정상적으로 인스턴스화되고, 메서드를 호출하여 각각의 동작을 수행한다.
이 접근 방식은 특히 대규모 프로젝트에서 명시적인 구성 관리가 필요할 떄 유용하다. @Import를 사용하면 구성 클래스간의 의존성을 명확히 할 수 있고, 컴포넌트 스캐닝에 의존하지 않고도 필요한 컴포넌트를 명시적으루 구성할 수 있다. 이는 구성의 명시성과 예측 가능성을 높이며, 컴퍼논트 스캐닝에서 발생할 수 있는 잠재적은 문제를 회피하는데 도움을 준다.
[ ▷ Injecting Dependencies on Imported @Bean Definitions ]
대부분의 실제 시나리오에서 빈은 구성 클래스간에 서로 종속성이 있다. XML을 사용하는 경우 컴파일러가 관여하지 않기 때문에 문제가 되지 않지만 ref = "someBean"을 선언하고 컨테이너 초기화 중에 스프링이 이를 해결하도록 신뢰할 수 있다. @Configuration 클래스를 사용하는 경우 자바 컴파일러는 구성 모델에 제약 조건을 두므로 다른 빈에 대한 참조는 유효한 자바 구문이어야 한다.
@Bean 메서드는 빈 종속성을 설명하는 임의의 수의 파라미터를 가질 수 있다. 각각 다른 빈에 선언된 빈에 따라 달라지는 여러 @Configuration 클래스가 있는 다음과 같은 보다 현실적인 시나리오를 고려한다.
@Configuration
public class ServiceConfig {
@Bean
public TransferService transferService(AccountRepository accountRepository) {
return new TransferServiceImpl(accountRepository);
}
}
@Configuration
public class RepositoryConfig {
@Bean
public AccountRepository accountRepository(DataSource dataSource) {
return new JdbcAccountRepository(dataSource);
}
}
@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {
@Bean
public DataSource dataSource() {
// return new DataSource
}
}
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
// everything wires up across configuration classes...
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}
// SpringTransactionManager 참조
@Configuration 클래스는 궁극적으로 컨테이너의 또 다른 빈일 뿐 이다. 다른 빈과 마찬가지로 @Autowired 및 @Value 주입과 다른 기능을 활용할 수 있다.
▼ 구성 클래스내에 @Autowired를 사용한 의존성 주
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import javax.sql.DataSource;
@Configuration
public class ServiceConfig {
@Autowired
private AccountRepository accountRepository; // 자동 주입
@Bean
public TransferService transferService() {
return new TransferServiceImpl(accountRepository); // 필드에서 가져온 빈 사용
}
}
@Configuration
public class RepositoryConfig {
@Autowired
private DataSource dataSource; // 자동 주입
@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(dataSource); // 필드에서 가져온 빈 사용
}
}
@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {
@Bean
public DataSource dataSource() {
// DataSource 구현체 반환
// 예를 들어, HikariDataSource 등
}
}
public class MainApp {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
// 모든 설정 클래스가 적절하게 연결됨
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}
}
이런 방식으로 주입하는 종속성이 가장 단순한 종류인지 확인해야 한다. @Configuration 클래스는 컨텍스트 초기화 중에 매우 일찍 처리되며, 이런 방식으로 종속성을 강제로 주입하면 에상치 못한 조기 초기화가 발생할 수 있다. 가능한 앞의 예시와 같이 매개 변수 기반 주입을 사용해야 한다.
동일한 구성 클래스의 @PostConstruct 메서드내에서 로컬로 정의된 빈에 액세스하면 안된다. 비정적 @Bean 메서드는 의미적으로 완전히 초기화된 구성 클래스 인스턴스가 호출되어야 하기 때문에 순환 참조가 발생한다.
순환 참조가 허용되지 않으면 BeanCurrentlyCreationException이 발생할 수 있다.
또한 @Bean을 통한 BeanPostProcessor 및 BeanFactoryPostProcessor 정의에 특히 주의해 한다. 이러한 정의는 일반적으로 정적 @Bean 메서드로 선언해야하며, 포함된 구성 클래스의 인스턴스화를 트리거하지 않아야 한다. 그렇지 않으면 @Autowired와 @Value가 구성 클래스 자체에 동작하지 않을 수 있다.
왜냐하면 AutowirdAnnotationBeanPostProcessor 보다 일찍 빈 인스턴스로 생성할 수 있기 때문이다.
- 파라미터 기반 주입 사용 : @Autowired를 사용한 필드 주입 대신, 파라미터 기반 주입을 권장한다.
- @PostConstruct 주의 사항 : @PostConstruct 메서드 내에서 동일한 구성 클래스의 non-static @Bean 메서드에 접근하지 않아야 한다.
- BeanPostProcessor와 BeanFactoryPostProcessor 정의 : 이러한 클래스들은 일반적으로 정적 @Bean 메서드로 정의해야 하며, 그렇지 않으면 예상치 못한 초기화 문제가 발생할 수 있다.
1. 기본 구성 및 주입 예제
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Configuration
public class AppConfig {
private final TransferService transferService;
// 매개변수 기반 주입을 사용하여 TransferService를 초기화
public AppConfig(TransferService transferService) {
this.transferService = transferService;
}
@Bean
public TransferService transferService(AccountRepository accountRepository) {
return new TransferServiceImpl(accountRepository);
}
@Bean
public AccountRepository accountRepository(DataSource dataSource) {
return new JdbcAccountRepository(dataSource);
}
@Bean
public DataSource dataSource() {
// 데이터베이스 연결을 위한 DataSource 설정
// 예를 들어, HikariDataSource 또는 다른 구현체를 반환
}
@PostConstruct
public void init() {
// 이 메서드 내에서 동일한 구성 클래스의 비정적 @Bean 메서드에 접근하지 마세요
// 예: transferService(); (비정적 메서드 접근 금지)
System.out.println("AppConfig initialized");
}
}
2. BeanPostPorcessor 및 BeanFactoryPostProcessor 정의 예제
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Component;
@Configuration
public class ProcessorConfig {
// 정적 @Bean 메서드로 BeanPostProcessor 정의
@Bean
public static BeanPostProcessor customBeanPostProcessor() {
return new CustomBeanPostProcessor();
}
// 정적 @Bean 메서드로 BeanFactoryPostProcessor 정의
@Bean
public static BeanFactoryPostProcessor customBeanFactoryPostProcessor() {
return new CustomBeanFactoryPostProcessor();
}
}
@Component
class CustomBeanPostProcessor implements BeanPostProcessor {
// BeanPostProcessor 구현
}
@Component
class CustomBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
// BeanFactoryPostProcessor 구현
}
- 파라미터 기반 주입 : AppConfig 클래스에서는 TransferService 생성자 주입 방식으로 받아 초기화한다. 이렇게 하면 스피링 컨텍스트 초기화 중에 발생할 수 있는 조기 초기화 문제를 피할 수 있다.
- @PostConstruct 주의 사항 : @PostConstruct 메서드에서 비정적 @Bean 메서드를 호출하지 않도록 주의해야 한다. 순환 참조가 발생할 수 있으며 BeanCurrentlyInCreationException이 발생할 수 있다.
- 정적 @Bean 메서드를 통한 BeanPostProcessor와 BeanFactoryPostProcessor 정의 : Bean ProcessorConfig 클래스에서는 BeanFactoryPostProcessor를 정적 메서드로 정의하여 스프링이 해당 클래스 인스턴스화를 조기에 트리거하지 않도록 한다. 이렇게 하면 @Autowired와 @Value가 예상대로 동작한다.
이 코드는 스프링에서 의존성 주입을 처리하는 올바른 방법을 보여준다. 특히 스프링 컨텍스트 초기화 중 발생할 수 있는 조기 초기화 문제를 피하기 위해 파라미터 기반 주입과 정적 @Bean 메서드랄 사용하여 BeanPostProcessor 및 BeanFactoryPostProcessor를 정의하는 방법을 강조한다. 이러한 패턴을 따르면 예상치 못한 초기화 문제를 방지할 수 있다.
▼ 한 빈이 다른 빈에 자동으로 연결되는 방법
@Configuration
public class ServiceConfig {
@Autowired
private AccountRepository accountRepository;
@Bean
public TransferService transferService() {
return new TransferServiceImpl(accountRepository);
}
}
@Configuration
public class RepositoryConfig {
private final DataSource dataSource;
public RepositoryConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(dataSource);
}
}
@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {
@Bean
public DataSource dataSource() {
// return new DataSource
}
}
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
// everything wires up across configuration classes...
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}
대상 빈이 생성자를 하나만 정의하는 경우 @Autowired를 지정할 필요가 없다.
[ ▷ Fully-qualify imported bean for ease of navigation ]
이전 시나리오에서 @Autowired를 사용하면 잘 작동하고 원하는 모듈성을 제공하지만 자동 와이어링된 빈 정의가 정확히 어디에 선언되는지 확인하는 것은 여전히 다소 모호하다.
Eclipse용 Spring Tools는 모든 것이 어떻게 와이어링되는지 보여주는 그래프를 렌더링할 수 있는 도구를 제공 (스프링 부트 프로젝트에서만 지원)하며, 이것만으로도 충분할 수 있다. 또한 자바 IDE는 AccountRepository 타입의 모든 선언과 사용을 쉽게 찾고 해당 타입을 리턴하는 @Bean 메서드의 위치를 빠르게 보여줄 수 있다.
이러한 모호성이 허용되지 않고 IDE 내에서 한 @Configuration 클래스에서 다른 @Configuration 클래스로 이동하려는 경우 구성 클래스 자체를 자동 와이어링하는 것을 고려해야한다.
@Configuration
public class ServiceConfig {
@Autowired
private RepositoryConfig repositoryConfig;
@Bean
public TransferService transferService() {
// navigate 'through' the config class to the @Bean method!
return new TransferServiceImpl(repositoryConfig.accountRepository());
}
}
이전 상황에서 AccountRepository가 정의된 위치는 완전히 명시적이다. 그러나 ServiceConfig는 이제 RepositoryConfig에 밀접하게 결합되었다. 이것이 트레이드오프이다. 이러한 밀집 결합은 인터페이스 기반 또는 추상 클래스 기반 @Configuration 클래스를 사용하야 다소 완화할 수 있다.
@Configuration
public class ServiceConfig {
@Autowired
private RepositoryConfig repositoryConfig;
@Bean
public TransferService transferService() {
return new TransferServiceImpl(repositoryConfig.accountRepository());
}
}
@Configuration
public interface RepositoryConfig {
@Bean
AccountRepository accountRepository();
}
@Configuration
public class DefaultRepositoryConfig implements RepositoryConfig {
@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(...);
}
}
@Configuration
@Import({ServiceConfig.class, DefaultRepositoryConfig.class}) // import the concrete config!
public class SystemTestConfig {
@Bean
public DataSource dataSource() {
// return DataSource
}
}
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}
이제 ServiceConfig는 구체적인 DefaultRepositoryConfig와 관련하여 느슨하게 결합되었으며, 내장된 IDE 툴링은 여전히 유용하다. RepositoryConfig 구현의 타입 계층을 쉽게 얻을 수 있다. 이런 식으로 @Configuration 클래스와 해당 의존성을 탐색하는 것은 인터페이스 기반 코드를 탐색하는 일반적인 프로세스와 다르지 않다.
특정 빈의 시작시 생성 순서에 영향을 주고 싶다면, 일부 빈을 @Lazy (시작 시가 아닌 첫 번째 액세스 시 생성)로 선언하거나, 다른 특정 빈에 @DependsOn (현재 빈의 직접적인 의존성이 의미하는 것 이상으로, 다른 특정 빈이 현재 빈보다 먼저 생성되도록 함)으로 선언하는 것을 고려해야한다.
[ ▷ Conditionally Include @Configuration Classes or @Bean Methods ]
임의의 시스템 상태에 따라 전체 @Configuration 클래스 또는 개별 @Bean 메서드를 조건부로 활성화하거나 비활성화하는 것이 종종 유용하다. 이에 대한 일반적인 예 중 하나는 @Profile을 사용하여 스프링 환경에서 특정 프로필이 활성화된 경우에만 빈을 활성화하는 것이다. (빈 정의 프로필)
@Profile은 실제로 @Configuration이라는 훨씬 더 유연한 어노테이션을 사용하여 구현된다. @Configuration은 @Bean이 등록되기 전에 참조해야 하는 특정 org.springframework.context.Condition 구현을 나타낸다.
Condition 인터페이스의 구현은 true 또는 false를 반환하는 matches() 메서드를 제공한다.
▼ @Profile에 서용된 실제 Condition 구현
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// Read the @Profile annotation attributes
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
for (Object value : attrs.get("value")) {
if (context.getEnvironment().acceptsProfiles(((String[]) value))) {
return true;
}
}
return false;
}
return true;
}
@Conditional 어노테이션을 사용해 특정 조건에 따라 @Configuration 클래스나 @Bean 메서드를 포함하거나 제외하는 방법을 보여주는 샘플 코드를 제작한다. 이 코드는 @Profile 어노테이션을 사용해 특정 프로파일이 활성화된 경우에만 빈을 등록하고, Condition 인터페이스를 구현하여 @Conditional을 사용하는 방법이다.
1. HikariCP 의존성 추가
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.0.1</version>
</dependency>
2. @Profile 어노테이션을 사용한 샘플 코드
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import com.zaxxer.hikari.HikariDataSource;
@Configuration
public class AppConfig {
@Bean
@Profile("dev")
public DataSource devDataSource() {
// 개발 환경용 데이터소스 설정
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
dataSource.setDriverClass(com.mysql.cj.jdbc.Driver.class);
dataSource.setUrl("jdbc:mysql://localhost:3306/sbdt_db?characterEncoding=UTF-8");
dataSource.setUsername("root");
dataSource.setPassword("1234");
return dataSource;
}
@Bean
@Profile("prod")
public DataSource prodDataSource() {
// 프로덕션 환경용 데이터소스 설정
return new HikariDataSource(); // 다른 데이터베이스 설정
}
}
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class MainApp {
public static void main(String[] args) {
// 프로파일 설정: "dev" 또는 "prod"
System.setProperty("spring.profiles.active", "dev");
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
DataSource dataSource = ctx.getBean(DataSource.class);
System.out.println("Using DataSource: " + dataSource.getClass().getName());
}
}
3. @Conditional 어노테이션을 사용한 사용자 정의 Condition 구현
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class CustomCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 예를 들어 특정 시스템 속성이 존재하는 경우에만 true 반환
String expectedProperty = "my.custom.property";
String propertyValue = context.getEnvironment().getProperty(expectedProperty);
return propertyValue != null && propertyValue.equals("enabled");
}
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
@Conditional(CustomCondition.class)
public MyService myService() {
return new MyServiceImpl();
}
}
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class MainApp {
public static void main(String[] args) {
// 조건에 맞는 시스템 속성 설정
System.setProperty("my.custom.property", "enabled");
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
if (ctx.containsBean("myService")) {
MyService myService = ctx.getBean(MyService.class);
System.out.println("MyService Bean is available: " + myService.getClass().getName());
} else {
System.out.println("MyService Bean is not available");
}
}
}
4. 주요 클래스 정의
public interface MyService {
void performService();
}
public class MyServiceImpl implements MyService {
@Override
public void performService() {
System.out.println("Service is being performed.");
}
}
1. @Profile 사용
- AppConfig 클래스는 @Profile 어노테이션을 사용하여 특정 프로파일이 활성화된 경우에만 빈을 등록한다.
- MainApp에서는 spring.profiles.active 속성을 설정하여 'dev' 또는 'prod' 프로파일을 활성화할 수 있다.
2. @Conditional 사용
- CustomCondition 클래스는 Condition 인터페이스를 구현하여, 특정 시스템 속성이 설정된 경우에만 빈을 등록하도록 한다.
- AppConfig 클래스의 myService 빈은 @Conditional을 사용하여 CustomCondition이 참일 경우에만 등록된다.
- MainApp에서 시스템 속성을 설정하고, 해당 조건에 맞는 빈이 등록되었는지 확인한다.
이 샘플 코드는 @Profile과 @Conditional 어노테이션을 사용하여 Spring에서 조건부로 @Configuration 클래스나 @Bean 메서드를 포함하거나 제외하는 방법을 보여준다. 이러한 기능을 사용하면 특정 환경이나 조건에 맞게 어플래케이션 구성을 유연하게 제어할 수 있다.
'스프링 프레임워크' 카테고리의 다른 글
스프링 프레임워크에서 의존성 주입 (0) | 2024.08.14 |
---|---|
The IoC Container [4] (0) | 2024.08.14 |
The IoC Container (3) (0) | 2024.08.09 |
The IoC Container (2) (0) | 2024.08.09 |
메이븐 (Maven) Build System (0) | 2024.08.08 |