Spring의 Java기반 컨테이너 구성 (1): @Bean과 @Configuration
기본 개념: @Bean 과 @Configuration
이 글은 스프링 공식 문서를 기반으로 정리했습니다.
Spring의 Java Configuartion 지원에서 핵심 구성요소는 @Configuartion이 붙은 클래스와 @Bean이 붙은 메서드다.
@Bean 메서드가 Spring IoC 컨테이너에 의해 과리된 새 객체를 인스턴스화, 설정, 초기화한하는 것을 나타내는데 쓰인다.
@Bean 어노테이션은 메서드가 Spring IoC 컨테이너에 의해 관리되는 새 객체를 인스턴스화, 설정, 초기화하는 것을 나타낸다.
XML 설정과 비교하자면 <bean/> 엘리먼트와 동일한 역할을 수행한다고 보면 된다.
@Bean 메서드는 사실 어떤 스프링 컴포넌트(@Component) 내에서도 사용될 수 있지만, 주로 @Configuration 클래스와 함께 쓰인다.
@Configuration이 붙은 클래스는 해당 클래스의 주 목적이 '빈 정의'라는 것을 나타내기 때문이다.
빈이 붙은 메서드는 어떤 스프링 컴포넌트와도 같이 사용될 수 있다.
더욱이 @Configuration 클래스를 사용하면 같은 클래스 내부에서 다른 @Bean 메서드를 호출함으로써 빈 간의 의존성을 정의할 수도 있다.
단순한 예시는 다음과 같다.
@Configuration
public class AppConfig {
@Bean
public MyServiceImpl myService() {
return new MyServiceImpl();
}
}다른 빈 메서드를 호출해서 빈 간의 의존성을 구성하는 예시는 다음과 같다.
@Configuration
public class AppConfig {
// 1. Repository 빈 정의
@Bean
public MyRepository myRepository() {
return new MyRepository();
}
// 2. Service 빈 정의 - 여기서 myRepository() 메서드를 직접 호출!
@Bean
public MyService myService() {
// 다른 @Bean 메서드를 호출하여 의존성을 주입합니다.
return new MyService(myRepository());
}
}@Bean Annotation 이용하기
@Bean은 메서드 레벨의 어노테이션이며, init-method, destroy-method, autowiring 등 XML의 <bean/>이 제공하는 일부 속성들을 지원한다.
빈 선언
메서드에 @Bean을 붙이면 빈이 선언된다. 이때 빈의 이름은 기본적으로 메서드 이름과 동일하게 결정된다.
@Bean이 붙은 메서드는 반환값으로 지정된 타입의 빈 정의를 ApplicationContext 내에 등록하는데 쓰인다.
또한 앞서 본 빈 정의와 다른 방식으로 빈을 정의할때 디폴트 메서드(default method, Interface 클래스에서 선언하는 디폴트 메서드 지칭)를 사용할 수도 있다. 이를 통해 디폴트 메서드에 빈 정의가 포함된 인터페이스를 구현함으로써 빈 설정들을 조합할 수 있게된다.
public interface BaseConfig {
@Bean
default TransferServiceImpl transferService() {
return new TransferServiceImpl();
}
}
@Configuration
public class AppConfig implements BaseConfig {
}이 선언에 의해 TransferServiceImpl 타입의 객체 인스턴스에 연결된 transferService란 이름의 빈을 ApplicationContext에서 사용할 수 있게한다.
반환 타입과 타입 예측 (Type Prediction)
다음 같이 아예 @Bean 메서드의 반환 타입을 인터페이스로 선언할 수 있다.
@Configuration
public class AppConfig {
//TransferService는 인터페이스
@Bean
public TransferService transferService() {
return new TransferServiceImpl();
}
}이 경우 스프링은 빈 정의(bean definition) 단계에서 해당 빈을 TransferService 타입으로 인식한다.
즉, 사전 타입 예측(advanced type prediction)은 메서드 시그니처에 명시된 반환 타입인 TransferService까지만 가능하며,
실제 구현체가 TransferServiceImpl인지는 빈 인스턴스가 생성된 이후에야 확정되는 것.
컴포넌트 스캔 및
@Bean등록 단계에서는 빈의 메타데이터(이름, 스코프, 반환 타입 등)만 수집하며 실제 인스턴스 생성(new)은 이후 단계에서 이루어진다.
대부분의 일반적인 상황에서는 문제가 발생하지 않는다. 하지만 스프링이 빈을 '인스턴스 생성 이전'에 타입 기준으로 판단해야 하는 경우, 이 제한된 타입 정보가 문제가 될 수 있다.
예를 들어, 다른 곳에서 다음과 같이 구현체 타입으로 직접 주입을 시도한다면
@Autowired
TransferServiceImpl transferService;스프링은 빈 인스턴스가 아직 생성되지 않은 시점에서는 해당 빈이 TransferServiceImpl 타입인지 확정할 수 없을 수 있다. 이 경우, 타입 기반 빈 탐색 과정에서 해당 빈이 후보에서 제외될 가능성이 있다.
그러면 안전한 방법으로는, 프로젝트 전체에서 인터페이스(TransferService)만 참조한다면 문제없다.
또는 구현체 타입으로 직접 참조될 가능성이 있다면, 반환 타입을 가능한 구체적으로(TransferServiceImpl) 선언하면 된다.
@Bean
public TransferServiceImpl transferService() {
return new TransferServiceImpl();
}@Configuration Annotation
빈 간 의존성 주입
빈이 다른 빈과 의존성을 가질때 다른 @Bean메서드를 직접 호출함으로써 이를 표현한다.
@Configuration
public class AppConfig {
@Bean
public BeanOne beanOne() {
return new BeanOne(beanTwo());
}
@Bean
public BeanTwo beanTwo() {
return new BeanTwo();
}
}beanOne은 생상저 주입에 의해 beanTwo를 주입받는다.
여기서 신기한 점은 beanTwo()를 호출할 때마다 매번 new BeanTwo()가 실행될 것 같지만, 스프링은 CGLIB라는 기술을 써서 싱글톤을 보장해준다. (이게 @Configuration의 핵심)
CGLIB (Code Generation Library)란?
CGLIB는 자바의 실시간 바이트코드 생성 라이브러리. 런타임에 자바 클래스의 상속을 받아 새로운 하위 클래스를 동적으로 만들어내는 도구이다. 스프링은 이 라이브러리를 사용하여 프록시 객체를 생성한다.