이 글에서 다루는 것
「Spring 소스 파헤치기」의 세 번째 글입니다. 2편에서 Spring Framework가 어떤 모듈로 나뉘어 있는지 큰 지도를 봤습니다. 이번에는 그 가운데 spring-beans 모듈로 들어가서, 컨테이너의 뼈대를 이루는 두 가지를 봅니다.
- BeanDefinition — 객체 하나를 어떻게 만들지 적어 둔 설계도
- BeanFactory — 그 설계도를 보관하고, 달라고 하면 객체를 만들어 주는 곳
Spring이 무엇이고 컨테이너와 빈이 무엇인지는 1편에서 소개했습니다. 필요한 말은 아래에서 다시 짧게 짚으니 이 글부터 읽어도 됩니다.
순서는 이렇습니다.
- 빈을 만들려면 왜 설계도(
BeanDefinition)가 먼저 필요한가 - 빈을 달라고 하는 창구
BeanFactory는 어떻게 생겼나 BeanFactory는 왜 여러 개의 작은 인터페이스로 나뉘어 있나- 그 인터페이스를 전부 구현하는 클래스
DefaultListableBeanFactory는 어떻게 쌓여 있나 - 설계도 한 장이 지나가는 길: 등록 → 병합 → 타입으로 찾기
- 처음 보면 꼭 헷갈리는
FactoryBean과&
기준으로 삼은 소스는 다음과 같습니다.
| 항목 | 내용 |
|---|---|
| 대상 | Spring Framework 7.1.0-SNAPSHOT |
| 커밋 | f447f3c310 (2026-10-01, main 브랜치) |
| 주로 보는 곳 | spring-beans/src/main/java/org/springframework/beans/factory/ |
줄 번호와 세부 구현은 버전에 따라 다를 수 있습니다. 코드 아래의 링크는 모두 이 커밋에 고정되어 있어서, 눌러 보면 이 글과 같은 줄이 나옵니다. 경로가 길어서 링크 글자는 beans/factory/…처럼 줄여 적었습니다.
코드 발췌에서 // ... 한 줄은 그 자리의 코드나 주석을 생략했다는 표시입니다. 그 밖의 줄은 원본 그대로입니다.
먼저 알아 둘 말 다섯 가지
빈(Bean)은 Spring이 대신 만들고 관리해 주는 객체입니다. 우리가 new로 직접 만들지 않고 Spring에게 맡긴 객체를 이렇게 부릅니다.
컨테이너는 빈을 만들고, 보관하고, 서로 연결해 주는 주체입니다. 객체를 만드는 일을 내 코드가 아니라 컨테이너가 쥐고 있다는 뜻에서 IoC(Inversion of Control, 제어의 역전) 컨테이너라고도 합니다. 이 글의 주인공 BeanFactory가 바로 이 컨테이너의 가장 기본이 되는 모습입니다.
의존성 주입(DI, Dependency Injection)은 어떤 빈이 일하는 데 필요한 다른 빈을 컨테이너가 찾아서 넣어 주는 것입니다. A가 B를 쓴다면, A가 B를 직접 만드는 대신 컨테이너가 B를 A에게 건네줍니다.
스코프(scope)는 빈을 몇 개 만들지에 대한 규칙입니다. 싱글톤(singleton)은 컨테이너에 하나만 만들어 두고 모두가 같이 쓰는 것이고, 프로토타입(prototype)은 달라고 할 때마다 새로 만드는 것입니다. 따로 정하지 않으면 싱글톤입니다(어디서 그렇게 정해지는지는 5절에 나옵니다).
인터페이스와 구현 클래스는 Java 문법이지만 이 글의 핵심이라 한 번 짚습니다. 인터페이스는 "이 객체에게 시킬 수 있는 일의 목록"입니다. 클래스 하나가 인터페이스를 여러 개 구현할 수 있고, 같은 객체라도 어떤 인터페이스 타입으로 건네받느냐에 따라 부를 수 있는 메서드가 달라집니다. 3절은 이 성질 하나로 설명됩니다.
1. 빈을 만들려면 설계도가 먼저 필요하다 — BeanDefinition
컨테이너가 빈을 만들려면 알아야 할 것이 많습니다. 어떤 클래스의 객체인지, 하나만 만들지 매번 새로 만들지, 시작하자마자 만들지 처음 쓰일 때 만들지, 생성자에 무엇을 넣을지. 이 정보는 객체를 만들기 전에 있어야 합니다.
그래서 Spring은 빈마다 이런 정보를 담은 객체를 먼저 만들어 둡니다. 이것이 BeanDefinition입니다. 빈이 "집"이라면 BeanDefinition은 "설계도"입니다.
빈을 선언하는 방법은 여러 가지지만 컨테이너 안에서는 전부 BeanDefinition이라는 한 가지 모양이 됩니다.
설계도를 따로 두면 좋은 점이 있습니다.
- 빈을 선언하는 방법(어노테이션, 자바 설정, XML)이 달라도 컨테이너는 한 가지 형식만 다루면 됩니다.
- 설계도만 다 모아 둔 상태에서, 객체를 만들기 전에 설계도를 살펴보거나 고칠 수 있습니다.
- 설계도 한 장으로 객체를 여러 번 만들 수 있습니다(프로토타입).
BeanDefinition은 인터페이스이고, 설계도에 적을 수 있는 항목이 메서드로 나열되어 있습니다. 몇 개만 봅니다.
public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement {
// ...
void setBeanClassName(@Nullable String beanClassName);
// ...
void setScope(@Nullable String scope);
// ...
void setLazyInit(boolean lazyInit);
// ...
void setPrimary(boolean primary);
// ...
}
소스: beans/factory/config/BeanDefinition.java 42–378행
@Nullable은 "이 값은 null이어도 된다"는 표시입니다. 앞으로도 계속 나오니 그렇게만 읽고 넘어가면 됩니다.
설계도에 담기는 정보를 뜻에 따라 묶으면 세 덩어리입니다. 필드 이름은 대부분의 설계도 클래스가 물려받는 AbstractBeanDefinition의 것입니다(169–225행).
| 묶음 | 필드 | 뜻 |
|---|---|---|
| 무엇을 만들까 | beanClass, factoryMethodName, constructorArgumentValues, propertyValues |
어떤 클래스를, 어떤 생성자 인자와 속성 값으로 만드는가 |
| 어떻게 살까 | scope, lazyInit, dependsOn, initMethodNames, destroyMethodNames |
몇 개를, 언제 만들고, 만든 직후와 없애기 직전에 무엇을 부르는가 |
| 주입 후보로서 | autowireCandidate, primary, fallback |
같은 타입의 빈이 여럿일 때 누구를 먼저 고르는가 |
설계도 클래스는 세 가지
BeanDefinition을 구현한 클래스는 크게 세 가지입니다. 셋 다 AbstractBeanDefinition을 상속합니다. @Configuration이나 @ComponentScan 같은 어노테이션이 이 설계도로 바뀌는 과정은 8편에서 봅니다.
| 클래스 | 언제 만나나 |
|---|---|
GenericBeanDefinition |
선언으로 적은 설계도에 쓰는 기본형. 컴포넌트 스캔(지정한 패키지의 클래스를 훑어 @Component 등이 붙은 것을 빈으로 등록하는 기능)으로 찾은 빈은 이것의 하위 클래스 ScannedGenericBeanDefinition이 됩니다 |
RootBeanDefinition |
@Bean 메서드로 선언한 빈의 설계도(하위 클래스 ConfigurationClassBeanDefinition). 그리고 5절에서 볼 병합 결과는 항상 이 타입입니다 |
ChildBeanDefinition |
부모 설계도가 고정된 옛 방식. 클래스 주석이 "Spring 2.5부터는 GenericBeanDefinition이 사실상 대신한다"고 직접 말합니다 |
지금은 "선언은 주로 Generic 계열, 컨테이너가 실제로 쓰는 것은 Root"라고만 기억해도 충분합니다.
헷갈리기 쉬운 곳: 설계도는 빈도 아니고 클래스도 아니다
BeanDefinition은 빈이 아닙니다. 설계도가 등록됐다고 객체가 만들어진 것은 아닙니다. 객체는 누군가 달라고 하거나, 컨테이너가 시작을 마무리하며 미리 만들 때 생깁니다.BeanDefinition은 클래스도 아닙니다. 같은 클래스로 설계도를 두 장 등록하면(이름과 설정이 다르게) 빈도 두 개가 됩니다.
2. 빈을 달라고 하는 창구 — BeanFactory
설계도를 보관하고 빈을 내주는 쪽이 BeanFactory입니다. 가장 기본이 되는 인터페이스이고, 핵심은 getBean입니다.
public interface BeanFactory {
// ...
Object getBean(String name) throws BeansException;
// ...
<T> T getBean(String name, Class<T> requiredType) throws BeansException;
// ...
<T> T getBean(Class<T> requiredType) throws BeansException;
// ...
boolean containsBean(String name);
// ...
}
소스: beans/factory/BeanFactory.java 123–444행
읽어 보면 전부 묻고 꺼내는 메서드입니다. 이름으로 달라(getBean(String)), 타입으로 달라(getBean(Class)), 그런 빈이 있나(containsBean). 생략한 부분도 싱글톤인가(isSingleton), 타입이 뭔가(getType) 같은 묻는 메서드뿐입니다. 빈을 등록하거나 컨테이너 설정을 바꾸는 메서드는 여기에 하나도 없습니다. 이 점이 3절의 출발점입니다.
설계도와 창구가 어떻게 맞물리는지 가장 작은 예로 보면 이렇습니다. 아래는 Spring 소스가 아니라 이 글을 위해 쓴 예시입니다(Greeter는 따로 만든 클래스이고, 나머지 문장은 어떤 메서드 안에 있다고 생각하면 됩니다).
public class Greeter {
public String hello() { return "hello"; }
}
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// 1. 설계도를 만들어 "greeter"라는 이름으로 등록한다. 아직 Greeter 객체는 없다
GenericBeanDefinition definition = new GenericBeanDefinition();
definition.setBeanClass(Greeter.class);
factory.registerBeanDefinition("greeter", definition);
// 2. 이름으로 빈을 달라고 한다. 이때 컨테이너가 설계도를 보고 객체를 만든다
Greeter greeter = factory.getBean("greeter", Greeter.class);
// 3. 스코프를 정하지 않았으니 싱글톤이다. 다시 달라고 해도 같은 객체가 온다
Greeter again = factory.getBean("greeter", Greeter.class); // greeter == again
설계도 등록과 객체 생성은 따로 일어납니다. 객체는 처음 달라고 할 때 만들어지고, 그 뒤로는 같은 객체가 나옵니다.
실제 애플리케이션에서는 보통 BeanFactory를 직접 만들지 않고 ApplicationContext를 씁니다(2편에 나온 그것입니다). ApplicationContext도 BeanFactory 계열 인터페이스를 상속하고, 그 구현인 GenericApplicationContext(어노테이션 설정에 쓰는 AnnotationConfigApplicationContext의 부모 클래스)는 안에 DefaultListableBeanFactory를 필드로 하나 들고 있습니다. 그러니 이 글에서 보는 것이 곧 ApplicationContext의 엔진입니다.
getBean을 불렀을 때 안에서 무슨 일이 일어나는지(객체 생성, 의존성 주입, 초기화)는 4편의 주제입니다. 이번 글은 그 앞 단계, 창구의 모양과 설계도까지만 봅니다.
3. 인터페이스를 왜 이렇게 잘게 나눴나
BeanFactory에는 꺼내는 메서드밖에 없었습니다. 그러면 설계도 등록이나 컨테이너 설정은 어디서 할까요? 다른 인터페이스에 있습니다. Spring은 컨테이너에게 시킬 수 있는 일을 아홉 개의 인터페이스로 나눠 두었습니다.
화살표는 "상속한다(구현한다)"는 뜻입니다. 회색은 꺼내고 읽는 쪽, 파랑은 설정하는 쪽, 노랑은 등록하는 쪽입니다. 오른쪽 노란 줄기가 BeanFactory와 이어져 있지 않은 것을 눈여겨보세요.
| 인터페이스 | 더해지는 능력 | 대표 메서드 |
|---|---|---|
BeanFactory |
이름이나 타입으로 빈 꺼내기 | getBean, containsBean, getType |
HierarchicalBeanFactory |
부모 팩토리 알기 | getParentBeanFactory |
ListableBeanFactory |
하나씩 묻지 않고 목록으로 훑기 | getBeanDefinitionNames, getBeanNamesForType, getBeansOfType |
AutowireCapableBeanFactory |
컨테이너가 만들지 않은 객체에 주입하고 초기화하기 | autowireBean, initializeBean |
SingletonBeanRegistry |
이미 만든 객체를 싱글톤으로 직접 올리기 | registerSingleton |
ConfigurableBeanFactory |
컨테이너 자체를 설정하기 | setParentBeanFactory, registerScope, addBeanPostProcessor |
ConfigurableListableBeanFactory |
등록된 설계도를 읽고 고치기, 시작 마무리 | getBeanDefinition, freezeConfiguration, preInstantiateSingletons |
AliasRegistry |
별칭(같은 빈을 가리키는 다른 이름) 관리 | registerAlias |
BeanDefinitionRegistry |
설계도를 새로 등록하고 지우기 | registerBeanDefinition, removeBeanDefinition |
"부모 팩토리"는 컨테이너를 부모-자식으로 겹쳐 쓸 수 있다는 뜻입니다. 자식 컨테이너에서 빈을 못 찾으면 부모에게 묻습니다.
나눈 이유: 받는 인터페이스가 곧 권한
이름만 보면 "역할별로 정리했구나" 싶지만, 더 정확하게는 누구에게 어디까지 보여 줄지를 정한 것입니다.
Spring에는 확장점이 여럿 있습니다. 정해진 인터페이스를 구현한 빈을 등록해 두면, 컨테이너가 정해진 때에 그 메서드를 불러 주는 자리입니다. 이때 컨테이너는 자기 자신을 인자로 넘겨주는데, 어떤 인터페이스 타입으로 넘겨주느냐가 확장점마다 다릅니다.
같은 컨테이너 객체지만, 건네받는 타입에 따라 할 수 있는 일이 달라집니다.
| 확장점 | 컨테이너가 불러 주는 메서드 | 그래서 할 수 있는 일 |
|---|---|---|
BeanFactoryAware |
setBeanFactory(BeanFactory beanFactory) |
빈 꺼내기 |
BeanFactoryPostProcessor |
postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) |
등록된 설계도를 읽고 고치기 |
BeanDefinitionRegistryPostProcessor |
postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) |
설계도를 새로 등록하고 지우기 |
평범한 빈은 BeanFactory만 받으니 다른 빈을 꺼내 쓸 수는 있어도 컨테이너를 바꿀 수는 없습니다. 뒤의 두 확장점은 "설계도는 다 읽었지만 빈은 아직 하나도 만들지 않은" 시점에 불립니다. 설계도를 고치거나 더할 수 있는 마지막 기회라서, 그 일에 필요한 인터페이스를 받습니다.
특히 설계도를 등록하는 능력은 BeanFactory 계열에 아예 없고 BeanDefinitionRegistry에만 있습니다. 이 인터페이스의 주석이 그 의도를 직접 적어 두었습니다.
This is the only interface in Spring's bean factory packages that encapsulates registration of bean definitions. The standard BeanFactory interfaces only cover access to a fully configured factory instance.
풀어 쓰면 "설계도 등록을 맡는 인터페이스는 이것 하나뿐이고, 표준 BeanFactory 인터페이스들은 설정이 끝난 팩토리를 쓰는 것만 다룬다"입니다(beans/factory/support/BeanDefinitionRegistry.java 29–31행). 이 인터페이스의 registerBeanDefinition은 5절에서 구현을 직접 봅니다.
헷갈리기 쉬운 곳
- 객체는 하나입니다. 인터페이스가 아홉 개라고 컨테이너가 아홉 개인 것이 아닙니다. 다음 절에서 볼 클래스 하나가 전부 구현합니다. 그래서
BeanFactory로 받은 것을 더 넓은 타입으로 캐스팅하면 나머지 메서드도 보이긴 합니다. 하지만ConfigurableBeanFactory의 주석은 이 인터페이스가 일반 애플리케이션 코드에서 쓰라고 만든 것이 아니라고 밝혀 두었습니다. 캐스팅해서 쓰는 것은 설계가 막아 둔 문을 여는 일입니다. getBean은 부모 팩토리까지 찾아보지만, 목록을 훑는 메서드는 지금 팩토리만 봅니다.ListableBeanFactory의 주석에 계층을 고려하지 않는다고 적혀 있습니다. "이름으로는 찾아지는데 목록에는 안 나온다"면 이 차이일 수 있습니다.
4. 이 모든 것을 구현하는 한 클래스 — DefaultListableBeanFactory
아홉 개의 인터페이스를 실제로 구현하는 클래스가 DefaultListableBeanFactory입니다. 2절의 예시에서 new로 만든 그 클래스입니다.
public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory
implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {
소스: beans/factory/support/DefaultListableBeanFactory.java 132–133행
implements 뒤의 앞 두 이름이 앞 그림의 맨 아래 두 갈래(파랑, 노랑)입니다. 이 둘을 구현하면 그 위의 인터페이스가 전부 따라옵니다. 세 번째 Serializable은 Java 표준 인터페이스라 여기서는 넘어갑니다.
이 많은 일을 클래스 하나에 다 적지는 않았습니다. 부모 클래스 다섯 개를 차례로 상속하면서, 층마다 한 가지 일과 그 일에 필요한 데이터만 갖도록 쌓았습니다.
화살표는 앞 그림과 같이 "상속한다"는 뜻입니다. 위로 갈수록 부모이고, 맨 아래가 우리가 쓰는 클래스입니다. 노랑은 만들어진 객체를 보관하는 층, 파랑은 설계도를 보관하는 층입니다.
| 클래스 | 갖고 있는 것 | 맡은 일 |
|---|---|---|
SimpleAliasRegistry |
aliasMap |
별칭을 진짜 이름으로 바꾸기 |
DefaultSingletonBeanRegistry |
singletonObjects 등 |
만들어진 싱글톤 객체 보관. 4편의 "3단계 캐시"가 이 층에 있습니다 |
FactoryBeanRegistrySupport |
factoryBeanObjectCache |
FactoryBean이 만든 객체 보관 (6절) |
AbstractBeanFactory |
mergedBeanDefinitions, scopes, beanPostProcessors |
getBean의 뼈대, 병합된 설계도 보관 (5절) |
AbstractAutowireCapableBeanFactory |
생성에 쓰는 설정 몇 가지 | 빈을 실제로 만들고 의존성을 주입 (createBean) |
DefaultListableBeanFactory |
beanDefinitionMap, beanDefinitionNames |
설계도 저장소, 타입으로 찾기 |
맨 위 SimpleAliasRegistry만 spring-core 모듈에 있고 나머지는 모두 spring-beans의 support 패키지에 있습니다.
AbstractBeanFactory는 설계도가 어디 있는지 모른다
이 사슬에서 설계 의도가 가장 잘 보이는 곳입니다. getBean의 뼈대는 AbstractBeanFactory에 있는데, 정작 이 클래스에는 설계도 저장소가 없습니다. 설계도가 필요하면 아래 두 메서드를 부르고, 그 내용은 비워 두었습니다(abstract).
protected abstract boolean containsBeanDefinition(String beanName);
// ...
protected abstract BeanDefinition getBeanDefinition(String beanName) throws BeansException;
소스: beans/factory/support/AbstractBeanFactory.java 1976–1997행
"설계도를 어디에 어떻게 저장할지는 자식 클래스가 정해라"는 뜻입니다. 큰 흐름은 부모가 잡고 세부는 자식이 채우는 이런 구조를 템플릿 메서드 패턴이라고 합니다. DefaultListableBeanFactory가 이 빈칸을 맵 하나로 채웁니다.
/** Map of bean definition objects, keyed by bean name. */
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);
// ...
/** List of bean definition names, in registration order. */
private volatile List<String> beanDefinitionNames = new ArrayList<>(256);
소스: beans/factory/support/DefaultListableBeanFactory.java 190–206행
컨테이너의 "설계도 보관함"은 결국 빈 이름 → BeanDefinition 맵입니다. 그런데 이름 목록을 리스트로 또 들고 있습니다. ConcurrentHashMap은 여러 스레드가 동시에 써도 안전하지만 넣은 순서를 기억하지 못하기 때문입니다. 등록 순서가 필요한 일은 리스트가 맡습니다(주석에도 "in registration order"라고 적혀 있습니다).
5. 설계도가 지나가는 길: 등록 → 병합 → 타입으로 찾기
이제 설계도 한 장이 컨테이너 안에서 어떤 길을 가는지 따라가 봅니다.
노랑은 보관하는 곳, 파랑은 하는 일입니다. 보관하는 곳이 두 군데라는 점이 이 절의 핵심입니다.
5-1. 등록 — registerBeanDefinition
모든 설계도는 이 메서드 하나를 거쳐 들어옵니다. 길지만 뼈대만 남기면 이렇습니다.
@Override
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
throws BeanDefinitionStoreException {
// ...
BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
if (existingDefinition != null) {
if (!isBeanDefinitionOverridable(beanName)) {
throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition);
}
// ...
this.beanDefinitionMap.put(beanName, beanDefinition);
}
else {
// ...
if (hasBeanCreationStarted()) {
// ...
}
else {
// Still in startup registration phase
this.beanDefinitionMap.put(beanName, beanDefinition);
this.beanDefinitionNames.add(beanName);
// ...
}
// ...
}
if (existingDefinition != null || containsSingleton(beanName)) {
resetBeanDefinition(beanName);
}
// ...
}
소스: beans/factory/support/DefaultListableBeanFactory.java 1251–1332행
순서대로 읽으면 다음과 같습니다.
- (생략한 앞부분) 이름이 비어 있지 않은지, 설계도 내용에 모순이 없는지 검사합니다.
- 같은 이름의 설계도가 이미 있는지 맵에서 찾아봅니다.
- 있으면 덮어써도 되는지 확인합니다. 안 되면
BeanDefinitionOverrideException을 던지고, 되면 맵의 값을 새 설계도로 바꿉니다. - 없으면 맵에 넣고 이름 리스트 끝에 붙입니다.
- 예전 설계도를 덮어썼거나 같은 이름의 싱글톤 객체가 이미 있으면
resetBeanDefinition을 부릅니다. 예전 설계도로 만든 것(병합본, 이미 만든 싱글톤 객체)을 치우는 메서드입니다.
hasBeanCreationStarted() 갈래는 "빈을 이미 만들기 시작한 뒤에 설계도가 추가되는 경우"입니다. 이때는 이름 리스트를 통째로 새로 복사해 바꿉니다. 다른 곳에서 그 리스트를 돌고 있을 수 있어서입니다. 지금은 그런 갈래가 있다는 것만 알아 두면 됩니다.
헷갈리기 쉬운 곳: 같은 이름으로 두 번 등록하면 에러가 날 것 같지만, 이 클래스의 기본값은 덮어쓰기 허용입니다(setAllowBeanDefinitionOverriding의 주석에 Default is "true"). 나중에 등록한 쪽이 조용히 이깁니다. 다만 이 값은 setAllowBeanDefinitionOverriding(false)로 금지할 수 있으니 "항상 덮어써진다"고 외우지는 마세요.
5-2. 병합 — 컨테이너가 실제로 쓰는 것은 복사본
설계도에는 부모를 지정할 수 있습니다(BeanDefinition의 setParentName). XML 설정에서 parent 속성으로 쓰는 기능인데, 공통 설정을 부모 설계도에 적어 두고 자식은 다른 부분만 적는 식입니다. 빈을 만들려면 부모와 자식을 합친 완성본이 필요하고, 이 과정을 병합(merge)이라고 합니다.
눈여겨볼 점은 부모가 없는 설계도도 이 과정을 똑같이 거친다는 것입니다.
if (bd.getParentName() == null) {
// Use copy of given root bean definition.
if (bd instanceof RootBeanDefinition rootBeanDef) {
mbd = rootBeanDef.cloneBeanDefinition();
}
else {
mbd = new RootBeanDefinition(bd);
}
}
else {
// Child bean definition: needs to be merged with parent.
// ...
// Deep copy with overridden values.
mbd = new RootBeanDefinition(pbd);
mbd.overrideFrom(bd);
}
// Set default singleton scope, if not configured before.
if (!StringUtils.hasLength(mbd.getScope())) {
mbd.setScope(SCOPE_SINGLETON);
}
소스: beans/factory/support/AbstractBeanFactory.java 1424–1464행
bd가 원본 설계도, mbd가 병합본(merged bean definition), pbd가 부모의 병합본입니다. bd instanceof RootBeanDefinition rootBeanDef는 타입 검사와 변수 선언을 한 번에 하는 Java 문법입니다. 세 가지를 읽을 수 있습니다.
- 부모가 없으면 원본을 복사합니다. 원본이 이미
RootBeanDefinition이어도 그대로 쓰지 않고 복제(cloneBeanDefinition)합니다. - 부모가 있으면 부모의 병합본을 복사한 뒤 자식의 값으로 덮습니다(
overrideFrom). - 어느 쪽이든 결과는 **항상
RootBeanDefinition**이고, 스코프가 비어 있으면 여기서 싱글톤으로 정해집니다. "따로 안 정하면 싱글톤"이라는 규칙이 코드로는 이 세 줄입니다.
이렇게 만든 병합본은 AbstractBeanFactory의 mergedBeanDefinitions 맵에 보관해 두고 다시 씁니다.
왜 굳이 복사할까요? 원본은 getBeanDefinition()으로 바깥에 내주는 객체이고, 3절의 BeanFactoryPostProcessor가 고치기도 하는 객체입니다. 반면 컨테이너는 빈을 만들면서 알아낸 것들(어떤 생성자를 쓰기로 했는지 등)을 설계도에 메모해 두고 다음에 다시 씁니다. 이 메모를 원본에 적으면 사용자가 보는 설계도가 지저분해지고 서로 꼬입니다. 사용자가 보는 원본과 컨테이너가 쓰는 병합본을 나눠 두면 그럴 일이 없습니다.
헷갈리기 쉬운 곳: 원본과 병합본은 다른 객체입니다. 빈이 이미 만들어진 뒤에 getBeanDefinition()으로 꺼낸 원본을 고쳐도, 컨테이너가 쓰는 병합본과 이미 만들어진 객체에는 반영되지 않습니다. 설계도를 고치는 일은 빈을 만들기 전, BeanFactoryPostProcessor 단계에서 끝내는 것이 원칙입니다. (병합본을 언제 버리고 다시 만드는지에 대한 세부 규칙이 따로 있지만 이 글에서는 다루지 않습니다.)
5-3. 타입으로 찾기 — getBeanNamesForType
이름으로 찾는 것은 맵에서 꺼내면 되니 빠릅니다. 타입으로 찾는 것은 사정이 다릅니다. 설계도 보관함은 이름이 열쇠라서, "이 타입인 빈이 누구냐"를 알려면 등록된 이름을 처음부터 끝까지 돌면서 하나하나 병합본을 꺼내 타입이 맞는지 확인해야 합니다(doGetBeanNamesForType, 619–663행). 의존성을 타입으로 주입할 후보를 찾을 때도 결국 이 조회를 거칩니다(후보가 여럿일 때 그 가운데 하나를 고르는 과정은 5편에서 봅니다).
매번 다 도는 것은 비싸니 결과를 기억(캐시)해 두면 좋겠지만, 설계도가 아직 추가되고 바뀌는 중에는 기억한 결과가 금방 틀려집니다. 그래서 DefaultListableBeanFactory는 freezeConfiguration()이 불린 뒤에만 타입 조회 결과를 캐시합니다(602–617행). freezeConfiguration()은 "이제 설계도는 더 바뀌지 않는다"고 선언하는 메서드이고, ApplicationContext가 시작을 마무리하며 싱글톤을 미리 만들기 바로 전에 부릅니다.
정리하면, 컨테이너의 시작 과정은 설계도를 모으고 고치는 단계와 빈을 만드는 단계로 나뉘고, 그 경계에서 설정을 "얼립니다". 3절에서 본 확장점들이 불리는 때가 앞 단계입니다.
6. FactoryBean과 & 접두사
마지막으로, 이름 때문에 처음에 꼭 헷갈리는 것을 하나 봅니다. BeanFactory와 FactoryBean은 글자 순서만 바뀐 것 같지만 전혀 다릅니다.
BeanFactory는 지금까지 본 컨테이너입니다.FactoryBean은 컨테이너 안에 들어가는 빈인데, 자기 자신이 쓰이는 것이 아니라 다른 객체를 만들어 내놓는 빈입니다. 빈으로 등록된 작은 공장입니다.
public interface FactoryBean<T> {
// ...
@Nullable T getObject() throws Exception;
// ...
@Nullable Class<?> getObjectType();
// ...
default boolean isSingleton() {
return true;
}
}
소스: beans/factory/FactoryBean.java 65–147행
"생성자를 부르고 값을 넣는다"만으로는 만들기 어려운 객체가 있을 때, 만드는 과정을 코드로 직접 쓰려고 사용합니다. 클래스 주석에 따르면 프레임워크 내부에서 많이 쓰고, 애플리케이션에서는 기반 코드에서나 흔합니다. 직접 만들 일은 드물어도, 프레임워크 안에서 많이 쓰이는 만큼 알아 둘 만합니다.
아래는 이 글을 위해 쓴 예시입니다. java.time.Clock을 만들어 내놓는 공장을 clock이라는 이름으로 등록합니다(factory는 2절 예시의 그것입니다).
public class ClockFactoryBean implements FactoryBean<Clock> {
@Override
public Clock getObject() { return Clock.systemUTC(); }
@Override
public Class<?> getObjectType() { return Clock.class; }
}
GenericBeanDefinition definition = new GenericBeanDefinition();
definition.setBeanClass(ClockFactoryBean.class);
factory.registerBeanDefinition("clock", definition);
Object a = factory.getBean("clock"); // Clock — 공장이 만든 객체
Object b = factory.getBean("&clock"); // ClockFactoryBean — 공장 자신
설계도에 적은 클래스는 ClockFactoryBean인데 getBean("clock")은 Clock을 돌려줍니다. 공장 자신이 필요하면 이름 앞에 &를 붙입니다. 이 글자는 BeanFactory 인터페이스에 상수로 정해져 있습니다(FACTORY_BEAN_PREFIX, 132행).
이름 앞의 & 하나가 "공장이 만든 것"과 "공장 자신"을 가릅니다.
이 갈림길은 AbstractBeanFactory의 getObjectForBeanInstance에 그대로 있습니다(1854–1895행). 이름이 &로 시작하면 빈 객체(공장) 자신을 돌려주고, 아니라면 평범한 빈은 그대로, FactoryBean이면 getObject()의 결과를 돌려줍니다.
헷갈리기 쉬운 곳
getObject()는 매번 불리지 않을 수 있습니다.isSingleton()의 기본값이true이고, 그러면 컨테이너는 (공장 자신이 싱글톤 빈일 때) 공장이 처음 만든 객체를 보관해 두었다가(4절의factoryBeanObjectCache) 계속 그것을 돌려줍니다. 달라고 할 때마다 새 객체를 내놓고 싶으면isSingleton()이false를 돌려주게 해야 합니다.- 평범한 빈에
&를 붙이면 예외가 납니다(BeanIsNotAFactoryException). 공장이 아닌 것에서 공장을 꺼낼 수는 없기 때문입니다. - 컨테이너가 관리하는 것은 공장이지, 공장이 만든 객체가 아닙니다.
FactoryBean의 클래스 주석이 굵은 글씨로 적어 둔 문장입니다. 공장이 만든 객체의 뒷정리는 컨테이너가 알아서 해 주지 않습니다.
정리
| 개념 | 한 줄 요약 |
|---|---|
BeanDefinition |
빈 하나의 설계도. 빈도 아니고 클래스도 아니다 |
BeanFactory |
빈을 꺼내는 창구. 꺼내고 묻는 메서드만 있다 |
| 인터페이스 아홉 개 | 능력을 나눈 것이자 권한을 나눈 것. 확장점이 받는 타입이 곧 그 확장점의 권한 |
BeanDefinitionRegistry |
설계도를 등록하는 유일한 인터페이스. BeanFactory 계열과 이어져 있지 않다 |
DefaultListableBeanFactory |
아홉 개를 전부 구현하는 클래스. 부모 다섯 층 위에 서 있고 층마다 한 가지 일을 맡는다 |
beanDefinitionMap |
설계도 보관함. 빈 이름 → BeanDefinition |
| 병합 | 원본을 복사해 RootBeanDefinition을 만드는 일. 컨테이너는 이 복사본을 쓴다 |
| 타입으로 찾기 | 이름을 전부 돌며 확인한다. 캐시는 설정을 얼린 뒤에만 |
FactoryBean |
다른 객체를 만들어 내놓는 빈. &이름은 공장 자신 |
이 글에서 다루지 않은 것
공부 노트에는 있지만 처음 읽는 사람에게는 너무 깊어서 뺀 것들입니다.
RootBeanDefinition이 갖고 있는 캐시 필드 하나하나와, 병합본을 언제 버리고 다시 만드는지에 대한 규칙- 빈 생성이 시작된 뒤의 등록이 리스트를 복사해 바꾸는 이유(동시성)
FactoryBean의 타입을 알아내는 여러 단계의 추론 순서- 별칭이 서로를 가리키며 도는 경우의 검사, 버전별로 바뀐 API 목록
다음에 볼 것
이번 글은 getBean을 부르기 전까지였습니다. 설계도가 등록되고, 병합되고, 찾을 수 있게 되는 데까지입니다.
4편: Bean은 어떻게 만들어지나 — 생성 흐름과 3단계 캐시에서는 getBean 안으로 들어갑니다. 병합된 설계도를 가지고 객체를 만들고, 의존성을 넣고, 초기화하는 순서와, 4절의 DefaultSingletonBeanRegistry가 싱글톤을 왜 세 겹으로 보관하는지를 봅니다.