스프링 컨테이너
스프링 컨테이너란?
스프링 컨테이너는 빈(Bean)의 생성·관리·의존관계 주입을 담당하는 핵심 관리자다.
빈(Bean): 스프링이 관리하는 자바 객체.
우리가 직접 new MemoryMemberRepository() 같이 객체를 만드는 대신,
스프링 컨테이너가 객체를 만들고, DI를 처리하고, 싱글톤을 보장하고, 생명주기를 관리한다.
// 스프링 컨테이너 생성 — AppConfig 설정 정보를 읽어서 빈을 등록
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
// 등록된 빈 꺼내 쓰기
OrderService orderService = ac.getBean("orderService", OrderService.class);
ApplicationContext가 스프링 컨테이너의 핵심 인터페이스다.
BeanFactory vs ApplicationContext
스프링 컨테이너의 계층 구조를 먼저 이해해야 한다.
BeanFactory
스프링 컨테이너의 최상위 인터페이스. 빈을 등록하고, 조회하고, 의존관계를 주입하는 가장 기본적인 기능을 정의한다.
// BeanFactory의 핵심 메서드
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);
boolean isSingleton(String name);
// ...
}
BeanFactory의 특징: 지연 로딩(Lazy Loading)
- getBean()을 호출하는 순간에 빈을 생성한다
- 앱 시작 시점에는 빈을 만들지 않음
실무에서 BeanFactory를 직접 사용할 일은 거의 없다.
ApplicationContext
BeanFactory를 상속하면서 실무에 필요한 부가 기능을 추가한 인터페이스.
실제로는 항상 ApplicationContext를 사용한다.
ApplicationContext 상속 계층:
├── BeanFactory (빈 조회, 관리 — 핵심 기능)
├── EnvironmentCapable (환경변수 — local/dev/prod 프로파일 구분)
├── MessageSource (국제화 — messages_ko.properties, messages_en.properties)
├── ApplicationEventPublisher (이벤트 발행/구독 패턴)
└── ResourceLoader (파일/클래스패스/URL 리소스 로딩)
ApplicationContext의 특징: 즉시 로딩(Eager Loading, 기본)
- 앱 시작 시점에 모든 싱글톤 빈을 미리 생성해둠
- getBean() 호출 시 이미 만들어진 빈을 그냥 반환
- 덕분에 시작 시점에 설정 오류, 순환 참조 등을 바로 발견할 수 있음
| 기능 | BeanFactory | ApplicationContext |
|---|---|---|
| 빈 관리, 조회 | O | O |
| 환경변수 (프로파일) | X | O |
| 국제화 (다국어) | X | O |
| 이벤트 발행/구독 | X | O |
| 리소스 로딩 | X | O |
| 빈 로딩 방식 | 지연(Lazy) | 즉시(Eager) — 기본 |
ApplicationContext 구현체들
ApplicationContext는 인터페이스이므로 실제로는 구현체를 사용한다.
어떤 구현체를 쓰느냐에 따라 설정 방식이 달라진다.
AnnotationConfigApplicationContext (자바 설정 — 현재 주류)
// @Configuration 클래스를 읽어서 빈 등록
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
@Configuration과 @Bean으로 설정한 자바 코드를 읽어서 빈을 등록한다.
Spring Boot 포함 현재 대부분의 프로젝트에서 사용하는 방식이다.
GenericXmlApplicationContext (XML 설정 — 레거시)
// XML 파일을 읽어서 빈 등록
ApplicationContext ac = new GenericXmlApplicationContext("applicationContext.xml");
오래된 Spring 프로젝트(Spring 3.x 이전)에서 사용하던 방식. 신규 프로젝트에서는 거의 쓰지 않는다.
공통점
구현체가 달라도 사용하는 쪽 코드는 동일하다.
// AppConfig 방식이든 XML 방식이든 빈을 꺼내는 코드는 같다
OrderService orderService = ac.getBean("orderService", OrderService.class);
이것이 인터페이스(ApplicationContext)에 의존하는 장점이다.
스프링 컨테이너 생성 과정
스프링 컨테이너가 만들어지는 과정을 단계별로 살펴본다.
전제: AppConfig
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
1단계: 컨테이너 생성
컨테이너가 생성되고 빈 저장소가 초기화된다. 아직 비어있다.
[스프링 컨테이너]
┌──────────────────────────────┐
│ 빈 이름 │ 빈 객체 │
│──────────────────────────────│
│ (비어있음) │
└──────────────────────────────┘
2단계: 빈 등록
AppConfig의 @Bean 메서드를 모두 호출하고, 반환된 객체를 빈으로 등록한다.
[스프링 컨테이너]
┌────────────────────┬─────────────────────────────┐
│ 빈 이름 │ 빈 객체 │
├────────────────────┼─────────────────────────────┤
│ memberService │ MemberServiceImpl@x01 │
│ memberRepository │ MemoryMemberRepository@x02 │
│ orderService │ OrderServiceImpl@x03 │
│ discountPolicy │ RateDiscountPolicy@x04 │
└────────────────────┴─────────────────────────────┘
빈 이름은 기본적으로 메서드 이름이다.
직접 지정할 수도 있다: @Bean(name = "myMemberService")
주의: 빈 이름이 중복되면 예외가 발생하거나 기존 빈을 덮어쓴다. 반드시 다른 이름을 써야 한다.
3단계: 의존관계 주입 (DI)
설정 정보를 참고하여 빈들 사이의 의존관계를 연결한다.
[빈 객체 연결 결과]
MemberServiceImpl@x01
└── memberRepository ──→ MemoryMemberRepository@x02
OrderServiceImpl@x03
├── memberRepository ──→ MemoryMemberRepository@x02 (같은 인스턴스!)
└── discountPolicy ──→ RateDiscountPolicy@x04
생성자 주입의 경우 2단계(빈 생성)와 3단계(DI)가 동시에 일어난다. 빈을 생성할 때 생성자에 의존 객체를 바로 넘기기 때문이다.
빈 설정 방법 3가지
Java Config 방식 (현재 주류)
@Configuration
public class AppConfig {
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
}
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
타입 안전하고, IDE 자동완성이 지원되고, 리팩터링이 쉽다. 현재 표준.
Component Scan 방식 (Spring Boot 기본)
// main 클래스
@SpringBootApplication // 내부에 @ComponentScan이 포함되어 있음
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// 클래스에 @Component(@Service, @Repository 등)를 붙이면 자동으로 빈 등록됨
@Service
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
@Autowired
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
매번 AppConfig에 @Bean을 추가하지 않아도 된다. Spring Boot에서 가장 많이 사용하는 방식.
XML 방식 (레거시)
<!-- applicationContext.xml -->
<beans xmlns="http://www.springframework.org/schema/beans" ...>
<bean id="memberRepository" class="hello.core.member.MemoryMemberRepository"/>
<bean id="memberService" class="hello.core.member.MemberServiceImpl">
<constructor-arg name="memberRepository" ref="memberRepository"/>
</bean>
</beans>
오래된 프로젝트에서 볼 수 있다. 신규 프로젝트에서는 사용하지 않는다.
빈 조회
기본 조회
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
// 방법 1: 이름 + 타입으로 조회 (가장 명확, 권장)
MemberService memberService = ac.getBean("memberService", MemberService.class);
// 방법 2: 타입만으로 조회
// 단, 같은 타입이 2개 이상 등록되어 있으면 예외 발생
MemberService memberService = ac.getBean(MemberService.class);
// 방법 3: 이름만으로 조회 (Object 반환 — 타입 캐스팅 필요, 비권장)
Object memberService = ac.getBean("memberService");
예외 상황
// 존재하지 않는 빈 조회 → NoSuchBeanDefinitionException
ac.getBean("noBean", MemberService.class);
// → NoSuchBeanDefinitionException: No bean named 'noBean' available
// 같은 타입 빈이 2개 이상 → NoUniqueBeanDefinitionException
// MemoryMemberRepository, JdbcMemberRepository 둘 다 등록되어 있다면
ac.getBean(MemberRepository.class);
// → NoUniqueBeanDefinitionException: expected single matching bean but found 2
같은 타입이 여러 개인 경우
// 해결 1: 이름 지정
MemberRepository repo = ac.getBean("memoryMemberRepository", MemberRepository.class);
// 해결 2: 해당 타입의 모든 빈을 Map으로 조회
Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class);
beansOfType.forEach((name, bean) ->
System.out.println("name=" + name + " bean=" + bean));
// 출력:
// name=memoryMemberRepository bean=hello.core.member.MemoryMemberRepository@...
// name=jdbcMemberRepository bean=hello.core.member.JdbcMemberRepository@...
상속 관계 조회 주의
부모 타입으로 조회하면 자식 타입 빈도 모두 포함되어 조회된다.
// DiscountPolicy를 구현한 빈: FixDiscountPolicy, RateDiscountPolicy 두 개 등록
ac.getBean(DiscountPolicy.class);
// → NoUniqueBeanDefinitionException! (자식이 2개라서)
// 이름을 지정해야 함
ac.getBean("rateDiscountPolicy", DiscountPolicy.class); // OK
// Object로 조회하면 스프링 내부 빈 포함 모든 빈이 나옴
Map<String, Object> allBeans = ac.getBeansOfType(Object.class);
// 수십 개의 빈이 출력됨 — Spring 내부 빈 포함
빈 등록 방법 비교
| 방법 | 특징 | 언제 사용 |
|---|---|---|
@Bean (Java Config) |
명시적 등록, 코드로 직접 제어 | 외부 라이브러리 빈, 공통 기술 빈(DataSource, AOP 등) |
@Component + @ComponentScan |
자동 등록, 설정 코드 불필요 | 일반 비즈니스 빈 (서비스, 레포지토리, 컨트롤러) |
| XML | 레거시 | 오래된 프로젝트 유지보수 |
BeanDefinition (내부 동작 원리)
스프링이 다양한 설정 형식(Java Config, XML 등)을 지원할 수 있는 이유는 ==BeanDefinition==이라는 빈 메타데이터 추상화 덕분이다.
AppConfig.java 파싱
→ BeanDefinitionReader가 읽어서
→ BeanDefinition 생성 (빈 이름, 클래스, 스코프, 생성자 파라미터 등의 메타데이터)
→ 스프링 컨테이너가 BeanDefinition을 기반으로 빈 생성
applicationContext.xml 파싱
→ XmlBeanDefinitionReader가 읽어서
→ BeanDefinition 생성
→ 동일하게 처리
스프링 컨테이너 입장에서는 설정이 자바 코드인지 XML인지 상관없다.
BeanDefinition이라는 공통 형식으로 변환되기 때문이다.
직접 BeanDefinition을 만들어서 등록할 수도 있지만, 실무에서 그럴 일은 없다. "왜 다양한 설정 방식을 지원할 수 있는가"를 이해하는 용도로만 알아두면 된다.
내부 동작 원리
컨테이너 내부 자료구조
스프링 컨테이너는 내부적으로 두 개의 ConcurrentHashMap을 사용한다.
// DefaultListableBeanFactory 내부 (의사코드)
public class DefaultListableBeanFactory {
// BeanDefinition 저장소 — 빈의 "설계도"
// key: 빈 이름, value: BeanDefinition (클래스, 스코프, 의존관계 등 메타데이터)
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);
// 싱글톤 빈 저장소 — 완성된 빈 인스턴스
// key: 빈 이름, value: 실제 객체
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
}
컨테이너 시작 흐름:
1. beanDefinitionMap 채우기 (설계도 수집)
"memberService" → BeanDefinition{class=MemberServiceImpl, scope=singleton, ...}
"memberRepository" → BeanDefinition{class=MemoryMemberRepository, ...}
2. singletonObjects 채우기 (실제 객체 생성)
"memberService" → MemberServiceImpl 인스턴스
"memberRepository" → MemoryMemberRepository 인스턴스
getBean() 호출 시:
1. singletonObjects에서 이름으로 검색 → 있으면 즉시 반환 (O(1))
2. 없으면 beanDefinitionMap으로 새로 생성 (프로토타입 스코프 등)
왜
ConcurrentHashMap인가? 스프링 컨테이너는 멀티스레드 환경에서 동작한다. 여러 스레드가 동시에getBean()을 호출해도 스레드 안전하게 빈을 조회할 수 있도록ConcurrentHashMap을 사용한다.
ApplicationContext.refresh() — 컨테이너 초기화 8단계
new AnnotationConfigApplicationContext(AppConfig.class) 내부에서 refresh()가 호출된다.
이 메서드 하나가 스프링 컨테이너 전체 초기화를 담당한다.
① prepareRefresh()
환경변수(application.yml, 시스템 프로퍼티) 로딩, 유효성 검증
② obtainFreshBeanFactory()
DefaultListableBeanFactory 생성
beanDefinitionMap 초기화 (아직 비어있음)
③ invokeBeanFactoryPostProcessors()
← 핵심 단계
@Configuration 클래스 파싱 → @Bean 메서드를 BeanDefinition으로 변환
@ComponentScan 실행 → classpath 스캔 → @Component 빈들을 BeanDefinition으로 등록
이 단계 끝에 beanDefinitionMap이 모두 채워짐
④ registerBeanPostProcessors()
BeanPostProcessor 등록:
- AutowiredAnnotationBeanPostProcessor (@Autowired 처리)
- CommonAnnotationBeanPostProcessor (@PostConstruct/@PreDestroy 처리)
- AnnotationAwareAspectJAutoProxyCreator (AOP 프록시 생성)
⑤ initMessageSource()
국제화 설정 (messages.properties 로딩)
⑥ initApplicationEventMulticaster()
이벤트 발행자 초기화
⑦ finishBeanFactoryInitialization()
← 핵심 단계 (여기서 실제 new 발생)
beanDefinitionMap의 모든 싱글톤 빈을 순서대로 생성
→ 의존 그래프 분석 → 의존되는 빈부터 먼저 생성 → singletonObjects에 저장
⑧ finishRefresh()
ApplicationStartedEvent, ApplicationReadyEvent 발행
→ @EventListener(ApplicationReadyEvent.class) 메서드가 여기서 실행됨
의존성 순서 해결 — 왜 순서가 보장되는가
A → B → C 의존관계가 있을 때 (A가 B를 사용하고, B가 C를 사용) 스프링은 C → B → A 순으로 생성한다.
빈 생성 요청: A를 만들어야 함
1. A의 BeanDefinition을 읽음 → "A는 B가 필요하다"
2. B를 먼저 만들려고 하니 → "B는 C가 필요하다"
3. C를 먼저 만들려고 하니 → "C는 의존관계 없음" → C 생성 완료
4. B 생성 시 C를 주입 → B 생성 완료
5. A 생성 시 B를 주입 → A 생성 완료
이 과정이 자동으로 처리되기 때문에 개발자는 빈 생성 순서를 신경 쓸 필요가 없다.
순환 참조(A → B → A)는 이 방식으로 해결할 수 없다.
스프링은 생성자 주입의 경우 순환 참조를 시작 시점에 감지하고 BeanCreationException을 발생시킨다.
필드/Setter 주입은 3단계 캐시로 해결하려 하지만, 권장하지 않는다 (싱글톤.md 참고).