이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • Spring IoC Container는 단순 객체 저장소인가, 객체 그래프를 구성하는 실행 환경인가?
  • Bean은 클래스, 객체, 설정 정보 중 무엇에 가까운가?
  • Spring이 객체를 관리한다는 말은 실제로 무엇을 포함하는가?

개요

Spring Framework 공식 문서는 IoC Container를 Spring Core의 중심으로 둔다. ApplicationContext는 설정 메타데이터를 읽고, Bean을 생성하고, 설정하고, 의존성을 연결해 애플리케이션이 사용할 완성된 객체 그래프를 제공한다.

Bean은 Spring IoC Container가 관리하는 객체다. 중요한 점은 Bean이 “애너테이션이 붙은 클래스”가 아니라 컨테이너 안에 등록된 객체라는 것이다. 클래스는 설계도이고, BeanDefinition은 컨테이너가 읽는 정의이며, Bean은 그 정의를 바탕으로 만들어지고 관리되는 인스턴스다. @Component@Bean은 곧바로 객체 하나를 의미한다기보다 BeanDefinition을 만드는 입력에 가깝다.

원리

IoC는 Inversion of Control, 즉 제어의 역전이다. 일반적인 코드에서는 객체가 필요한 협력 객체를 직접 생성한다.

public class OrderService {
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}

이 코드는 간단하지만 OrderService가 구체 구현체 생성까지 책임진다. 변경하려면 서비스 코드를 고쳐야 하고, 테스트에서는 다른 정책을 끼워 넣기 어렵다.

Spring에서는 객체 생성과 연결을 컨테이너가 맡는다.

@Service
public class OrderService {
 
    private final DiscountPolicy discountPolicy;
 
    public OrderService(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
}

이제 OrderService는 어떤 구현체가 들어오는지 직접 결정하지 않는다. 컨테이너가 DiscountPolicy 타입의 Bean을 찾아 생성자에 주입한다. 이때 객체 생성의 주도권은 애플리케이션 코드에서 컨테이너로 이동한다.

ApplicationContext

ApplicationContext는 BeanFactory를 확장한 더 풍부한 컨테이너다. Bean 생성과 조회뿐 아니라 메시지, 이벤트, 리소스 로딩, 환경 정보 같은 애플리케이션 인프라도 제공한다.

실무에서 직접 new AnnotationConfigApplicationContext(...)를 쓰는 일은 많지 않다. Spring Boot 애플리케이션에서는 부팅 과정이 ApplicationContext를 만들고 자동 설정과 컴포넌트 스캔을 수행한다. 그러나 내부적으로는 여전히 컨테이너가 Bean 정의를 읽고 Bean을 조립한다는 흐름이 중심이다.

BeanDefinition

컨테이너는 Bean을 무작정 만드는 것이 아니라 BeanDefinition이라는 메타데이터를 기반으로 만든다. BeanDefinition에는 Bean 클래스 또는 factory method, 이름, Scope, 생성자 인자, 프로퍼티, 초기화/소멸 정보 같은 내용이 들어간다.

이 관점이 중요하다. @Component@Bean은 Bean을 바로 만드는 문법처럼 보이지만 실제로는 먼저 Bean 정의를 만드는 입력이다. 컨테이너는 이 정의를 모아 전체 객체 그래프를 계산하고, singleton Bean은 보통 컨테이너 초기화 시점에 만들며 lazy나 prototype 같은 설정이 있으면 다른 시점에 생성할 수 있다.

Bean 이름과 타입

Bean은 이름과 타입을 가진다. 대부분의 주입은 타입 기준으로 동작하지만, 같은 타입의 Bean이 둘 이상이면 이름이나 qualifier가 중요해진다.

@Service
public class OrderService {
    public OrderService(DiscountPolicy discountPolicy) {
    }
}

DiscountPolicy 구현체가 하나면 문제가 없다. 그러나 RateDiscountPolicy, FixedDiscountPolicy가 모두 Bean이면 Spring은 어떤 Bean을 넣어야 할지 결정할 수 없다. 이때 @Primary, @Qualifier, Bean 이름 같은 추가 기준이 필요하다.

실전 팁

  • Spring을 이해할 때 “객체를 대신 만들어 준다”보다 “객체 그래프를 구성한다”로 이해하는 편이 더 정확하다.
  • Bean 이름은 평소에는 잘 보이지 않지만, 후보가 여러 개인 주입, AOP bean() Pointcut, 테스트 대체 Bean에서 중요해진다.
  • 직접 ApplicationContext.getBean()을 많이 호출하는 코드는 컨테이너에 과하게 의존하고 있을 가능성이 높다. 일반 애플리케이션 코드는 필요한 협력 객체를 생성자 주입으로 받는 편이 객체 계약이 선명하다.
  • Bean으로 등록할 객체와 일반 객체를 구분한다. 상태가 요청마다 달라지는 값 객체나 DTO까지 무작정 Bean으로 만들 필요는 없다.
  • Spring이 관리하지 않는 객체는 DI, AOP, 생명주기 콜백의 자연스러운 대상이 아니다. 직접 new로 만든 객체가 왜 주입이나 프록시를 못 받는지 이 기준으로 판단한다.

위험 신호!

  • 서비스 내부에서 협력 객체를 직접 new로 만든다.
  • 같은 타입의 Bean이 여러 개인데 이름과 qualifier 설계가 없다.
  • 어디서 Bean이 등록되는지 추적하기 어렵다.
  • DTO, Entity, 값 객체까지 습관적으로 Bean으로 등록한다.
  • 비즈니스 코드에서 ApplicationContext를 직접 주입받아 Bean을 계속 꺼낸다.

확인 질문

확인 질문

  • IoC Container는 무엇을 관리하는가?
    • Bean 정의를 바탕으로 객체를 생성, 설정, 조립하고 의존성과 생명주기를 관리한다.
  • Bean은 클래스인가?
    • 아니다. Bean은 Spring Container가 관리하는 객체 인스턴스다.
  • 타입 기준 주입에서 후보가 여러 개면 무엇이 필요한가?
    • @Primary, @Qualifier, Bean 이름 같은 추가 선택 기준이 필요하다.

참고 문서