0. 학습 목적
- Spring에서 제어의 역전을 어떻게 구현했는지 이해한다.
- Spring에서 객체의 생성과 관리를 담당하는 컨테이너를 어떻게 생성하는지 이해한다.
- Spring에서 객체를 어떻게 해당 컨테이너에 등록하는지 이해한다.
- Spring에서 어떻게 객체를 주입하는지 이해한다.
1. Spring에서는 IoC를 어떻게 구현했는가?
Spring에서는 Spring IoC Container라는 객체 생성과 관리 전용 컨테이너를 만들어서 IoC를 구현하였다. Spring에서 IoC가 이루어지는 과정을 그려보면 다음과 같다.

과정을 말로 풀어보면,
- 프로그램 시작 시, Spring IoC Cotainer가 ApplicationContext 구현체로 생성된다.
- 컨테이너가 생성하고 관리할 객체를 찾는다.
- 1번에서 생성한 ApplicationContext가 뭔지에 따라, 어디서 찾을지가 나뉜다.
- pom.xml, 어노테이션 (
@Component,@Configuration,@Bean) - Spring IoC 컨테이너가 생성하고 관리하는 객체는
Bean이라고 불린다!
- 읽은 내용을 토대로, Spring IoC 컨테이너 내부에 Bean 객체를 생성 하고 관리한다.
- Business 객체 중 컨테이너에 등록된 Bean을 필요로 하는 객체가 있을 경우 컨테이너가 적절한 시점에 해당 의존성을 주입 (DI) 해준다.
이제 1번부터 4번 과정이 정확히 어떻게 이루어지는지를 세세하게 확인 해보자.
2. Spring IoC 컨테이너 명세와 구현
- 프로그램 시작 시, Spring IoC Cotainer가 ApplicationContext 구현체로 생성
Spring IoC 컨테이너 의 기능과 동작 방법은 BeanFactory, ApplicationContext 인터페이스에 명세되어 있다. BeanFactory 인터페이스가 Spring IoC 컨테이너의 초기 버전이자 루트 인터페이스이다. 해당 인터페이스를 상속받고 기능을 확장한 것이 바로 ApplicationContext 인터페이스이다.

그림처럼 ApplicationContext의 구현체는 5종류로 나뉘는데, 각각 IoC 컨테이너의 설정 파일과 Bean 객체를 어디서 읽어들일 것인가의 차이일 뿐이다.
현재 대다수의 Web Applicaton 구현 시에는 ApplicationContext를 사용하지만, 메모리가 극도로 부족한 특수한 상황에서는 BeanFactory를 사용하는 경우도 있다고 한다. 밑은 BeanFactory와 ApplicationContext의 차이이다.
| 기능 | BeanFactory | ApplicationContext |
|---|---|---|
| 기본 빈 관리 | O | O |
| 빈 적제 전략 | Lazy Loading( 지연 로딩 ) | Eager Loading (즉시 로딩) |
| 이벤트 처리 설계 | X | 가능(Publisher, Listener) |
| 어노테이션 지원 | X | O |
| AOP 통합 | 제한적 (수동 설정 필요) | O (어노테이션을 통해 자동 통합) |
BeanFactory가 지연 로딩이기 때문에 메모리 사용은 적지만, 이 특성 때문에 Bean 객체 등록 시 생길 수 있는 오류나 예외의 조기 파악이 힘들다.
3. IoC 컨테이너에 Bean 등록하기
IoC 컨테이너에 Bean을 등록하는 방법은 어디서 Bean 객체를 읽어들일 것이냐에 따라 나뉜다.
크게 XML 파일에서 읽어오는 방법 과 어노테이션으로 클래스 특정해서 Bean 등록하는 방법 으로 나뉜다.
(1) XML 기반 Bean 등록
- XML에 미리 등록할 Bean 객체에 대하여 명세한다.
- IoC 컨테이너 객체가 생성될 때, 해당 XML 파일을 읽어서 명세된 Bean들을 생성한다.
A. 예시
package net.javaguides.spring.ioc;
public class GreetingService {
private String message;
public void setMessage(String message) {
this.message = message;
}
public void getMessage() { System.out.println("Message: " + message);
}
}
만약 위와 같은 객체를 Bean으로 등록할 때, XML 방식을 쓴다고 가정해보자.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="greetingService" class="net.javaguides.spring.ioc.GreetingService">
<property name="message" value="Hello, Spring XML Configuration!"/>
</bean>
</beans>
그러면 위와 같이 XML 파일을 만들어서 해당 객체에 대한 명세를 해줘야 한다. 위의 파일 이름을 applicationContext.xml 이라고 해보자.
package net.javaguides.spring.ioc;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Application {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
GreetingService greetingService = (GreetingService) context.getBean("greetingService");
greetingService.getMessage();
}
}
이후 위와 같이 ApplicationContext 생성 시, 해당 XML 파일을 읽어들여서 생성한다. 이러면 XML에 적힌 객체들은 Bean 등록이 되고, 이후 의존성 주입에 사용할 수 있다.
(2) 어노테이션 기반 Bean 등록
- Bean으로 등록할 클래스들에 특정 어노테이션을 붙인다.
- 서비스 부팅 시, 루트 클래스에서
@ComponentScan으로 하위 클래스들 중 특정 어노테이션이 붙은 클래스 객체를 전부 Bean 등록한다.
Bean 등록 어노테이션은 그 대상이 클래스 인지, 매서드 인지에 따라 2개로 나뉜다.
@Component
: 클래스 대상 어노테이션, 해당 어노테이션이 붙은 클래스는 부팅 시 Bean으로 등록@Configuration+@Bean
: 매서드 대상 어노테이션, 클래스에 @Configuration, 매서드에 @Bean을 붙이면, @Bean이 붙은 매서드의 반환 객체를 Bean으로 등록한다.
A. 매서드 등록 방식은 왜 필요할까?
외부 라이브러리 객체 등 프레임워크가 온전히 스스로 객체를 만들 수 없는 경우를 위해 필요하다. 이 경우 매서드에 해당 객체를 생성하기 위한 환경 변수와 외부 설정을 미리 세팅한 뒤, 완성된 객체를 반환하면 IoC 컨테이너가 그 완성본만 등록하는 방식이다.
B. SpringBoot 환경에서 @ComponentScan은 어디 있을까?
MVC 환경과 달리, Spring Boot에서 @ComponentScan을 직접 써 본 경험은 없을 것이다. SpringBoot의 경우, @SpringBootApplication 이라는 어노테이션에 @ComponentScan이 포함되어 있다.

저 어노테이션을 들어가보면,

위와 같이 ComponentScan이 포함되어 있다.
나머지 주요 어노테이션의 뜻도 살펴보면 다음과 같다.
@SpringBootConfiguration
: @Configuration 과 같은 기능을 한다. 단지 SpringBoot 최상단의 Configuration이라는 뜻@EnableAutoConfiguration
: /resource/META-INF/spring.factories의 EnableAutoConfiguration에 정의된 모든 Configuration들을 자동 등록한다. (즉 의존하는 라이브러리 사용을 위해 무조건 등록해야 하는 Bean들을 자동 등록한다.) - (ex - JPA, 데이터소스 메시징 라이브러리 활용을 위한 자동 구성 설정들을 Bean에 등록)@CompnentScan
: base-package가 정의되어 있지 않으면, 그것 하위 모든 클래스들을 검사함. @SpringBootConfiguration은 최상단 클래스에 있으므로, 서버의 모든 클래스를 검사하게 된다.
C. 예시
@ComponentScan 방식은 쉽고 간편하므로, @Configuration + @Bean 방식이 어떻게 되는지만 예시를 들어보겠다.
package net.javaguides.spring.ioc;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public GreetingService greetingService() {
GreetingService greetingService = new GreetingService();
greetingService.setMessage("Hello, Spring Java Configuration!");
return greetingService;
}
}
4. 의존성 주입
이제 IoC 컨테이너에 Bean 등록까지 마쳤으니, 적절한 시점에 의존성을 주입하는 방법에 대해 알아보겠다.
의존성 주입 방법에는 3가지가 있다.
- 생성자 주입
- Setter 주입
- 필드 주입
여기서 생성자 주입이 가장 권장되는 방법이고, 필드 주입이 가장 비권장되는 방법이다. 이유를 들어 설명을 이어가겠다. (여기서는 설명의 간편성을 위해 Bean을 주입 대상자와 연결하는 어노테이션을 @AutoWired로 통일하겠다.)
(1) 생성자 주입
Bean 객체를 생성자를 통해 주입 받는 방식이다.
@Service
public class UserService {
private final UserRepository userRepository; // final 키워드 사용 가능
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
UserService 객체가 생성되는 시점에 IoC 컨테이너가 UserRepository 객체를 주입해준다.
생성자 주입 방식에서는 @Autowired 같은 바인딩 어노테이션을 생략해도 된다.
해당 방식이 가장 권장되는 방식이라고 했는데, 이유는 다음과 같다.
- 불변성 보장
- 순환 참조 사전 방지 가능
- Mocking 활용한 단위 테스트에 용이함
A. 불변성 보장
생성자 주입의 경우, 객체가 생성될 때 주입받을 Bean 객체도 한 번에 주입 시키는 방식이다.
따라서 멤버 변수의 private 화, setter 제거를 해주면 이후 Bean 객체 변경이 불가함으로 불변성을 보장할 수 있다. 이에 더해, final 키워드를 주입 멤버 객체에 적용해서 추후 setter 주입, 필드 주입 가능성을 제거해버리면, 불변성을 확실하게 보장할 수 있다.
a. 순환 참조 사전 방지
'객체가 생성될 때, Bean도 한 번에 주입된다.'* 는 특성 때문에, 순환 참조 사전 방지 또한 가능하다.
위에서 살펴보았듯이 ApplicationContext는 모든 Bean 객체를 부팅 시 한 번에 만들어서 적재한다. 따라서 Bean 객체를 만들 때, 임의의 객체들이 서로가 서로를 주입 받아야할 상황이 되면 둘은 동시에 생성될 수 없기 때문에 예외가 발생한다. (BeanCurrentlyCreationException) 순환 참조를 사전에 인식시켜 주기에 이로 인한 추후의 문제가 발생하지 않는다.
반면 Setter 주입과 필드 주입의 경우, 순환 참조를 알 수가 없다.
객체 생성 이후에 Bean 주입이 이루어지기 때문에, ApplicationContext 생성 후 Bean 등록 시, 서로가 서로를 주입받아야할 상황이 있는지 확인이 불가능하다. 또한 서비스 전개 시에 임의의 객체들이 서로가 서로를 주입받아 서비스를 제공해야하는 상황이 오더라도 IoC 컨테이너가 임시 객체를 만들어 동시에 따로따로 주입하기 때문에, 오류가 나지 않는다.
따라서 개발자가 순환 참조 여부를 오류로도 확인할 수가 없어 추후 예기치 못한 버그가 발생할 수 있다.
B. 단위 테스트 용이
생성자라는 객체를 받는 주입구가 있다. 따라서 가짜 객체를 주입하는 Mocking이 가능해서 단위 테스트에 용이하다.
(2) setter 주입
Bean 객체를 생성한 이후 setter로 Bean 객체를 주입하기 때문에, 불변성 보장과 순환참조방지는 불가능하다. 하지만 setter도 주입구이기 때문에, 단위 테스트는 가능하다.
(3) 필드 주입
불변성 보장도 안되며, 가짜 객체 주입구가 없어서 단위 테스트도 하기가 어렵다.
장점이 없는 방식이라 비권장된다.
5. 핵심 요약
- Spring IoC 컨테이너는 ApplicationContext로 구현된다.
- Bean 등록은 XML 파일 혹은 어노테이션 기반으로 진행된다.
- 어노테이션 기반은 클래스를 바로 등록하는 @Component 방식과 매서드 기반으로 매서드가 반환하는 객체를 등록하는 @Configuration + @Bean 방식으로 다시 나뉜다.
- 의존성 주입 방법에는 생성자 주입, setter 주입, 필드 주입으로 나뉜다.
부록
A. 모르는 단어 정리
순환 참조
: 서비스 실행을 위해서 서로가 서로를 참조해야 하는 상황
무한 참조 루프에 빠지기 쉽기에 순환 참조는 직렬화하여 고쳐야 한다.