
해당 포스트는 Spring.io의 공식 문서를 포함한 레퍼런스와 코드를 통해 Spring Framework의 구조 / 기술에 대해 확인해보고자 하는 포스트입니다.
스프링 프레임워크의 핵심 기술 중에서도 가장 중요한 IoC 컨테이너, IoC 컨테이너가 수행하는 DI, 그리고 컨테이너에 의해 관리되는 객체인 빈(Bean)에 대한 내용입니다.
IoC(Invasion of Control, 제어의 역전)은 흔히들 '개발자가 코드 흐름을 제어하는 것이 아닌, 프레임워크가 코드 흐름을 제어하는 것' 이라고 표현을 합니다. 개발자가 작성한 코드를 프레임워크가 사용한다는 것인데요.
스프링 프레임워크에서는 이러한 IoC를 수행하는 컨테이너를 IoC 컨테이너라고 부르며 IoC의 방식으로 DI(Dependency Injection, 의존성 주입)을 사용하고 있습니다.
IoC 컨테이너는 어플리케이션에 필요한 객체인 빈(Bean)들을 생성하고 DI를 통해 의존성을 주입한 뒤 생명 주기를 관리합니다.
스프링 프레임워크 핵심 기술 중에서도 핵심이며 DI(의존성 주입)을 통해 IoC를 실현하는 컨테이너입니다.
컨테이너가 어플리케이션 구동에 필요한 객체인 빈(Bean)을 생성하고 의존성을 주입하며 생명 주기를 관리합니다.
IoC 컨테이너의 종류로는 크게 BeanFactory와 ApplicationContext가 있습니다. BeanFactory 인터페이스가 기본적인 IoC 컨테이너이며 이의 하위 타입 인터페이스가 ApplicationContext입니다.

ApplicationContext와 BeanFactory의 차이점
WebApplicationContext 와 같은 애플리케이션 계층 특정 컨텍스트를 제공
Spring Framework 공식 문서에 따르면 ApplicationContext 는 BeanFactory의 완벽한 상위 집합 (The ApplicationContext is a complete superset of the BeanFactory) 입니다.
어플리케이션을 구성하는 객체이며 IoC 컨테이너에 의해 생성 및 의존성 주입되고 생명 주기가 관리됩니다. Bean 간의 종속성은 컨테이너에서 사용하는 구성 메타데이터(configuration metadata)에 반영됩니다.
의존성은 객체 간의 의존관계, 종속성을 의미합니다. 또한 DI(의존성 주입)는 객체 생성 시점에서 생성자 파라미터나 팩토리 메서드 생성자 파라미터를 사용하거나 객체 생성 후 팩토리 메서드 설정 옵션 등으로 필요한 종속성(함께 작동하는 다른 객체 등)을 정의하는 프로세스입니다.
공식 문서에서의 순서와는 달리 다음과 같은 흐름으로 내용을 다루고자 합니다.
어플리케이션을 구성하는 객체인 빈 -> 빈 객체 간 종속성을 다루는 DI -> DI를 통해 빈을 관리하는 IoC 컨테이너
빈(Bean)은 어플리케이션을 구성하는 객체이며 IoC 컨테이너에 의해 관리됩니다.
이러한 빈들은 컨테이너에 제공하는 구성 메타데이터(configuration metadata)로 생성됩니다.
컨테이너 자체 내에서 이러한 빈들의 정의는 BeanDefinition 객체로 표시됩니다.
BeanDefinition은 다음과 같은 요소로 구성됩니다.
모든 빈에는 하나 이상의 식별자가 있으며 이러한 식별자는 컨테이너 내에서 고유해야 합니다.
기본적으로 식별자는 id나 name 속성을 사용하여 지정할 수 있는데 명시적으로 식별자를 지정하지 않을 경우 컨테이너는 해당 빈에 대해 고유한 이름을 생성합니다.
(빈 명명 규칙에 따르면 빈 이름은 소문자로 시작하여 카멜 케이스로 지어집니다. 예를 들어 빈 클래스 이름이 TestController일 경우 testController가 됩니다)
빈 정의는 하나 이상의 객체를 생성하기 위한 정의이며 컨테이너는 요청될 때 빈 정의를 확인하고 해당 빈 정의에 의해 캡슐화된 구성 메타데이터를 사용하여 실제 객체를 생성(또는 획득) 합니다.
이 때 객체를 생성하는 방식을 지정할 수 있는데 기본적으로는 컨테이너 자체가 반사적으로 생성자를 호출하여 빈을 생성하는 생성자 접근 방식이 일반적입니다.
빈 정의 시 생성된 객체의 생명 주기의 범위를 제어할 수 있습니다. 스프링 프레임워크는 총 6개의 범위를 지원하며 그 중 4개는 웹 인식 ApplicationContext에서만 사용이 가능합니다. 또한 사용자 지정 범위를 만들 수 있습니다.


Scope 인터페이스를 구현하여 사용하며 메서드에 대한 내용은 다음과 같음.public abstract class CustomScope implements Scope {
// 기본 범위에서 객체를 반환
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
return null;
}
// 기본 범위에서 객체를 제거
@Override
public Object remove(String name) {
return null;
}
// 범위가 소멸되거나 범위의 지정된 객체가 소멸될 때 범위가 호출해야 하는 콜백
@Override
public void registerDestructionCallback(String name, Runnable callback) {
}
// 범위에 대한 대화 식별자를 가져옴
@Override
public String getConversationId() {
return null;
}
// 주어진 키에 대한 컨텍스트 객체를 확인 (있는 경우)
@Override
public Object resolveContextualObject(String key) {
return null;
}
}
빈 수명 주기의 컨테이너 관리와 상호 작용하기 위한 InitializingBean 과 DisposableBean 인터페이스를 구현할 수 있습니다. 각각 컨테이너는 빈의 초기화 / 파괴 시 빈이 특정 조치를 수행하도록 afterPropertiesSet() 과 destory() 메서드를 지정합니다.
다만 이 경우 어플리케이션이 스프링 프레임워크에 불필요하게 종속되기 때문에 공식 문서에서는 이보다는 JSR 표준인 @PostConstruct, @PreDestroy 어노테이션을 사용하는 것을 권장하고 있습니다.
이밖에도 스프링 프레임워크가 기본적으로 제공하지 않는 사용자 지정 기능이나 기타 수명 주기 동작이 필요할 경우에는 후술할 BeanPostProcessor 를 통해 직접 구현할 수 있습니다.
초기화 및 소멸 콜백 외에도 스프링 프레임워크 관리 객체는 Lifecycle 인터페이스를 구현하여 해당 객체가 컨테이너 자체 수명 주기에 따라 시작 및 종료 프로세스에 참여할 수 있습니다.
의존성 주입 원칙을 사용하면 코드가 더 깔끔해지고 객체에 종속성이 제공될 때 분리가 더 효과적입니다.
객체는 종속성을 조회하지 않으며 종속성의 위치나 클래스를 알지 못합니다.
이 덕분에 종속성이 인터페이스나 추상 기본 클래스에 있을 때 클래스를 테스트하기가 쉬워지며 이를 통해 Stub 또는 모의 구현을 단위 테스트에 사용할 수 있다고 합니다.
DI는 생성자 기반 종속성 주입과 Setter 기반 종속성 주입의 두 가지 주요 변형이 있습니다.
각각 종속성을 나타내는 여러 인수를 사용하여 생성자를 호출하는 컨테이너에 의해 수행하는 방식입니다.
public class TestMovieController {
// TestMovieService와의 의존성이 있음
private final TestMovieService service;
// TestMovieController의 생성자이며 스프링 컨테이너가 TestMovieService를 주입할 수 있음
public TestMovieController(TestMovieService service) {
this.service = service;
}
}
인수가 없는 생성자(@NoArgsConstructor) 또는 인수가 없는 정적 팩토리 메서드를 호출하여 빈을 인스턴스화한 후 빈에서 세터 메서드를 호출하는 컨테이너에 의해 수행됩니다.
public class TestMovieController {
// TestMovieService와의 의존성이 있음
private final TestMovieService service;
// service의 세터 메서드이며 스프링 컨테이너가 TestMovieService를 주입할 수 있음
public void setService(TestMovieService service) {
this.service = service;
}
}
생성자 기반, Setter 기반 모두 혼합 사용이 가능하며 공식 문서에는 다음과 같은 견해를 밝히고 있습니다.
가장 중요한 것은 특정 클래스에 가장 적합한 DI 스타일을 사용하는 것이라고 합니다.
DI 스타일마다 트레이드 오프가 있으니 공식 문서의 견해를 참고하며 사용하는 것이 좋을 것 같습니다.
getBean() 메서드가 동작해야 빈을 로딩(지연 로딩)하는 BeanFactory와 달리 ApplicationContext 는 초기화 시 모든 빈을 생성하여 로딩(즉시 로딩)합니다.
다만 이러한 동작으로 인해 오류가 발생하는 경우(앞에서 이야기한 순환 종속성 문제 등) 초기화 전략을 지연 초기화로 설정할 수 있습니다. default-lazy-init=true
XML 기반 구성 메타데이터에서 서로 협업하는 빈 간의 관계를 Autowiring을 통해 자동으로 연결할 수 있습니다.
다만 한계와 단점이 명확하여 현재는 거의 사용이 되지 않는 것으로 보입니다.
대부분의 경우 어플리케이션을 구성하는 빈은 싱글톤이지만 간혹 협업 관계가 있는 두 빈의 생명 주기가 다른 경우 (빈 A가 싱글톤인데 빈 B가 프로토타입인 경우) 문제가 생길 수 있습니다.
이럴 때 메서드 주입을 통해 빈이 다른 빈을 요청할 수 있도록 컨테이너를 호출한 후 필요한 빈을 인식할 수 있습니다.
인터페이스 ApplicationContext 는 IoC 컨테이너이며 빈의 인스턴스화와 구성 및 조립을 담당합니다. 이 때 구성 메타데이터(configuration metadata)를 읽어 인스턴스화, 구성 및 조합을 할 객체에 대한 지침을 얻습니다. 구성 메타데이터는 XML, Java 어노테이션 또는 java 코드로 표시됩니다.

ApplicationContext 는 여러 구현체가 있는데 대표적으로 다음과 같습니다.
ClassPathXmlApplicationContextFileSystemXmlApplicationContextAnnotationConfigApplicationContextWebApplicationContext위에서 소개한 대로 IoC 컨테이너는 구성 메타데이터를 통해 어떤 객체를 인스턴스화 하고 구성, 조합하는지에 대해 확인합니다. 구성 메타데이터를 사용하는 방법은 크게 다음과 같습니다.
<beans/> 요소 내의 <bean/> 으로 작성@Autowired, @Inject, @Named)@Configuration, @Bean, @Import, @DependsOn 등을 사용컨테이너를 특정 구현체로 인스턴스화한 후 사용할 수 있습니다. 예를 들어 ClassPathXmlApplicationContext 로 인스턴스화할 경우 생성자의 파라미터에 위치 경로를 기입하여 해당 위치에 있는 컨테이너의 구성 메타데이터를 가져올 수 있습니다. 이 때 사용되는 위치 경로는 스프링 프레임워크의 Resource 추상화와 관련되어 있습니다.
ClassPathXmlApplicationContext (XML 기반 구성) 생성자 일부
AnnotationConfigApplicationContext (어노테이션, Java 기반 구성) 생성자 일부
// services.xml, daos.xml의 구성 메타데이터 확인, 빈 생성
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml")
// 정의된 빈 객체를 검색
TestService service = context.getBean("testService", TestService.class);
// 정의된 빈 객체를 사용
List<String> userList = service.getUserList();
getBean() 메서드를 통해 구성 메타데이터에 정의된 빈 객체들을 검색할 수 있으나 공식 문서에 따르면 실질적으로 개발자가 해당 메서드를 어플리케이션 코드에서 사용하는 일은 없어야 한다고 합니다. (어플리케이션 코드가 스프링 API에 의존하지 않도록 해야 하므로)
XML 기반 구성 대신 관련 클래스, 메서드 또는 필드 선언에 대한 주석을 사용하여 구성 요소 클래스 자체로 구성을 진행할 수 있습니다. 이 때 @Autowired 를 비롯한 어노테이션을 통해 구성 메타데이터를 구성합니다.
어노테이션 기반으로 구성 메타데이터를 구성할 경우 후술할 PostProcessor들이 암시적으로 사용됩니다.
ConfigurationClassPostProcessorAutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessorPersistenceAnnotationBeanPostProcessorEventListenerMethodProcessor어노테이션 기반 구성과 관련된 어노테이션은 다음과 같습니다.
@Autowired@Order@Primary@Qualifier@Resource@Autowired와 유사@Value@PostConstruct, @PreDestroy@Inject, @Named 등어노테이션 기반 구성의 주의할 점은 XML 기반 구성이 이뤄지기 전에 구성이 된다는 것, 그리고 XML 파일(ApplicationContext)에서 <context:annotation-config/> 를 입력한 경우, 해당 XML 파일에서 정의된 빈들만 탐색하게 된다는 것입니다.
XML 기반 구성이 아닌 클래스 경로를 스캔하여 후보 구성 요소(Candidate Components)를 감지하는 옵션이 클래스 경로 스캔입니다. 여기서 후보 구성 요소란 필터 기준과 일치하고 해당 빈 정의가 컨테이너에 등록된 클래스입니다.
클래스 경로 스캔을 사용하기 위해선 @Component와 같은 어노테이션이나 AspectJ 표현식, 또는 사용자 정의 필터 기준을 사용하여야 하며 이를 통해 컨테이너에 등록된 빈 정의가 있는 클래스를 선택할 수 있습니다.
@Component는 스프링 프레임워크가 관리하는 컴포넌트에 대한 일반적인 스테레오 타입이며 메타 어노테이션입니다.
@Component 를 활용해 보다 구체적인 사용 사례에 대해 전문화한 어노테이션들의 대표적인 사례가 @Controller, @Service, @Repository 입니다.
스프링은 이러한 스테레오 타입 클래스들을 자동으로 감지하여 해당 클래스들에 대한 BeanDefinition 인스턴스를 ApplicationContext에 등록합니다. 이렇게 BeanDefinition이 등록된 클래스들을 자동으로 감지하고 빈으로 등록하려면 @Configuration 가 부착된 클래스에 @ComponentScan을 부착해야 합니다.
@Repository
public class TestRepository {
...
}
@Configuration
@ComponentScan(basePackages = "org.example)
public class AppConfig {
}
또는, @Component 어노테이션이 부착된 클래스 내부에 @Bean을 부착시킨 메서드에서 빈을 반환하는 것으로 빈 정의 메타데이터를 컨테이너에 제공할 수도 있습니다.
@Component
public class FactoryComponent {
@Bean
public TestBean testBean() {
return new TestBean();
}
}
Java 기반 컨테이너 구성은 java 코드를 사용하여 컨테이너를 구성하는 것으로 핵심적인 요소는 @Bean 과 @Configuration 입니다. @Bean과 @Configuration에 대해 간단하게 설명하자면 다음과 같습니다.
@Bean<bean/> 와 같은 기능@Configuration@Bean이 부착된 메서드를 호출하여 빈 간 종속성을 정의할 수 있음공식 문서에 따르면 @Bean이 @Configuration 이 선언된 클래스 내에 있는 메소드에 선언될 경우에는 'full' 모드, 아닌 경우(이를테면 @Component) 'lite' 모드로 동작한다고 하는데, lite 모드에서의 @Bean 은 빈 간 종속성을 선언할 수 없고 다른 @Bean 메서드를 호출해서는 안된다고 합니다. 이러한 연유로 공식 문서에서는 가급적 @Configuration 내에 @Bean을 선언하는 것을 권장하고 있습니다.
@Configuration
public class AppConfig
@Bean
public BeanOne beanOne() {
return new BeanOne(beanTwo());
}
@Bean
public BeanTwo beanTwo() {
return new BeanTwo();
}
개발자들이 ApplicationContext 의 구현 클래스를 확장하고 싶다면 별도의 하위 클래스를 만들 필요 없이 특수 통합 인터페이스의 구현체를 플러그인하여 확장할 수 있습니다. 대표적인 특수 통합 인터페이스는 다음과 같습니다.
BeanPostProcessorBeanFactoryPostProcessorFactoryBean어플리케이션 환경과 관련된 프로필이나 속성을 추상화하는 Environment 인터페이스를 제공합니다.
@Profile 어노테이션을 통해 컨테이너 내 빈이 동작하기 위해 활성화되어야 할 프로필을 지정할 수 있습니다.
속성은 각종 속성 파일, JVM 속성, 시스템 환경 변수 등 다양한 소스에서 발생할 수 있는 속성을 의미하며 PropertySource, @PropertySource 등을 사용하여 검색을 할 수 있습니다.
@EnableLoadTimeWeaving, @Configuration 을 통해 클래스가 JVM에 로드될 때 동적으로 변형시키기 위해 사용합니다.
IoC 컨테이너는 어플리케이션을 구성하는 객체인 빈을 생성하고 DI를 통해 의존성을 주입하고 관리하는 역할을 합니다. BeanFactory와 ApplicationContext가 있으며 ApplicationContext가 보다 많은 기능을 가지고 있어 대부분 ApplicationContext를 활용한 구현체들을 사용하게 됩니다.
컨테이너는 빈 정의(BeanDefinition)과 해당 빈 정의에 의해 캡슐화된 구성 메타데이터를 활용하여 빈을 인스턴스화합니다.
이를 위해서 빈들은 컨테이너 내 구성 메타데이터(configuration metadata)에 등록이 되어야 하는데 이런 구성 메타데이터를 만드는 방법은 크게 XML 기반 구성, 어노테이션 기반 구성, Java 기반 구성이 있으며 각자 다른 방식으로 빈들을 구성 메타데이터에 등록합니다.
DI는 스프링 프레임워크에서 IoC를 실현하기 위한 방법이며 크게 생성자 기반 방식과 Setter 기반 방식이 있으며 공식 문서에서는 생성자 기반 방식을 권장하고 있습니다.
빈의 생명 주기는 총 6가지가 있으며 기본값은 싱글톤(singleton) 입니다.
만약 서로 종속성을 가진 두 빈의 생명 주기가 다를 경우 지연 초기화를 이용하거나 메서드 주입 방식을 통해 종속성을 주입하여야 합니다.
https://docs.spring.io/spring-framework/reference/core/beans.html
https://www.baeldung.com/spring-beanfactory-vs-applicationcontext