콘텐츠로 이동

스프링 컨테이너


스프링 컨테이너란?

스프링 컨테이너는 빈(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단계: 컨테이너 생성

ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

컨테이너가 생성되고 빈 저장소가 초기화된다. 아직 비어있다.

[스프링 컨테이너]
┌──────────────────────────────┐
│ 빈 이름  │  빈 객체          │
│──────────────────────────────│
│ (비어있음)                   │
└──────────────────────────────┘

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 GenericXmlApplicationContext("applicationContext.xml");

오래된 프로젝트에서 볼 수 있다. 신규 프로젝트에서는 사용하지 않는다.


빈 조회

기본 조회

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 참고).