
지난 컨텐츠를 작성하다가
스프링 컨테이너 등의 용어가 생소해 공부 및 정리가 필요한 것 같았다.
이번 기회에 스프링 컨테이너가 뭔지, 어떻게 작동하는지에 대해 정리하고자 한다.
스프링 컨테이너는 객체(Bean) 를 생성하고,
생명주기를 관리하며, 의존성을 주입하는 역할을 하는 중앙 관리자이다.
스프링 컨테이너는 XML, 어노테이션 기반의 자바 설정 클래스로 만들 수 있다.
스프링부트(Spring Boot)를 사용하기 이전에는 xml을 통해 직접적으로 설정해 주어야 했지만,
스프링부트가 등장하면서 대부분 사용하지 않게 되었다.
스프링 컨테이너의 종류는 다음과 같다.

두 인터페이스가 대표적인 스프링 컨테이너이다.
빈 팩토리는 빈의 생성과 관계 설정 같은 제어를 담당하는 IoC 오브젝트이고,
빈 팩토리를 좀 더 확장한 것이 애플리케이션 컨텍스트이다.
즉, 애플리케이션 컨텍스트는 IoC 방식을 따라 만들어진 일종의 빈 팩토리로, 둘 다 동일한 개념이라 간주해도 된다.
주로 사용되는 스프링 컨테이너는 애플리케이션 컨텍스트이다.
스프링 컨테이너의 최상위 인터페이스
빈을 등록, 생성, 조회하는 등 관리 역할을 하며, getBean() 메서드 통해 인스턴스화 할 수 있다.
@Bean 어노테이션이 붙은 메서드의 이름을 스프링 빈의 이름으로 사용하여 빈 등록을 한다.
BeanFactory의 기능을 상속받아 제공한다.
| 구분 | BeanFactory | ApplicationContext |
|---|---|---|
| 역할 | 기본적인 Bean 관리 기능 | BeanFactory 기능 + 확장 기능 제공 |
| 기능 | Bean 생성, DI 관리 | 국제화, 이벤트 처리, AOP 지원, 환경 설정 |
| 사용 예 | 메모리 절약이 중요한 테스트 | 대부분의 스프링 애플리케이션 |
컨테이너는 개발자가 정의한 빈을 객체로 만들어 관리하고 개발자가 필요로 할 때 제공한다.
의존성 주입(DI)을 통해 애플리케이션의 컴포넌트를 관리할 수 있다.
또한, 서로 다른 빈을 연결하여 애플리케이션 빈을 연결하는 역할을 한다.
개발자는 모듈 간 의존 및 결합으로 인해 발생할 수 있는 문제에서 벗어날 수 있다.
UserService userService = new UserService();
이처럼 전통적인 자바에서는 개발자가 직접 객체를 만들고 관리한다.
일반적인 자바 프로그램에서 개발자는 main() 메서드로 프로그램의 시작을 제어하고,
필요한 객체를 생성하고, 메서드를 호출하며, 프로그램이 실행되는 전체 흐름을 관리한다.
그러나 대규모 애플리케이션에서는 이런 방식이 비효율적일 수도 있다.
시스템이 커지면 객체 간의 의존성이 복잡해지고, 코드의 결합도가 높아지기 때문에 유지보수와 확장이 어려워지기 때문이다.
하지만 스프링에서는 제어 흐름이 역전(Inverted) 된다.
즉, 객체의 생성과 주입을 개발자가 아닌 컨테이너가 담당한다.
개발자는 “무엇을 쓸지”만 선언하고,
“언제, 어떻게” 만들지는 스프링이 알아서 처리한다.
1. 객체 생성과 관리
2. 의존성 주입
3. 애플리케이션의 생명 주기 관리
4. AOP (관점 지향 프로그래밍)
5. 웹 요청 처리
의존성이란 “어떤 객체가 다른 객체를 필요로 하는 관계”이다.
DI는 이 관계를 스프링이 대신 연결해주는 방식이다.
이 방식은 제어의 역전(IoC)의 구체적인 구현 방법 중 하나이다.
이를 통해 객체 간의 결합도를 낮추고, 코드의 유연성과 재사용성을 높이며, 테스트와 유지보수를 훨씬 더 쉽게 할 수 있다.
| 주입 방법 | 설명 |
|---|---|
| 생성자 주입 | 권장 방식. 불변성 유지 가능 |
| 필드 주입 | 간단하지만 테스트 어려움 |
| Setter 주입 | 선택적 의존성에 사용 |
생성자 주입
생성자 주입은 객체가 생성될 때 필요한 의존성을 생성자의 매개변수로 전달받는 방식
이 방법은 객체가 생성될 때 반드시 필요한 의존성을 제공해야 하기 때문에,
불완전한 객체 생성을 방지할 수 있다.
필드 주입
인터페이스 주입은 주입할 의존성을 위한 인터페이스를 제공하고, 이를 구현하는 클래스가 의존성을 주입받는 방식
Setter 주입
객체가 생성된 후에 의존성을 주입하는 방식
의존성이 필수적이지 않고, 나중에 주입될 수 있을 때 유용하다.
그러나 필수적인 의존성인지 명확히 확인하기가 어렵기에, 테스트 시 객체가 불완전한 상태로 사용될 위험이 있다.
스프링에서는 주로 생성자 주입이 권장된다.
이는 필수적인 의존성을 명확히 하고, 객체가 생성될 때 필요한 모든 의존성을 즉시 주입받도록 보장한다.
스프링 컨테이너는 아래 단계로 Bean을 등록하고 관리한다.
@Configuration 클래스나
@ComponentScan 이 붙은 패키지를 스캔한다.
@SpringBootApplication
@ComponentScan(basePackages = "com.fintrack")
public class FintrackApplication {
public static void main(String[] args) {
SpringApplication.run(FintrackApplication.class, args);
}
}
@SpringBootApplication 내부에는 이미
@ComponentScan 이 포함되어 있어,
com.fintrack 패키지 이하의 @Component, @Service, @Repository, @Controller 클래스를 자동으로 탐색한다.
@Configuration스프링 컨테이너가 해당 어노테이션이 붙은 클래스를 설정 정보로 사용한다.
스프링은 스캔한 클래스 정보를 기반으로 BeanDefinition 객체를 생성한다.
이것은 “이 클래스로 Bean을 만들겠다”는 설명서 역할을 한다.
@Component → BeanDefinition 등록@Bean → BeanDefinition 등록스프링은 BeanDefinition 정보를 바탕으로
리플렉션(Reflection)을 사용해 객체를 실제로 생성한다.
Class<?> clazz = Class.forName("com.fintrack.service.UserService");
Object bean = clazz.getDeclaredConstructor().newInstance();
리플렉션
이전에 어노테이션 관련 포스팅을 작성할 때
실행 중에 클래스의 정보를 읽거나 조작할 수 있는 기술
쉽게 말하자면 코드가 자기 자신을 분석할 수 있는 능력
이라고 했다.
즉, 구체적인 Class Type을 알지 못하더라도 해당 Class의 method, type, variable들에 접근할 수 있도록 해주는 자바 API이며,
컴파일된 바이트 코드를 통해 Runtime에 동적으로 특정 Class의 정보를 추출할 수 있는 프로그래밍 기법이다.
생성된 Bean들 중 필요한 의존성을 자동으로 연결한다.
예를 들어,
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
여기서 스프링 컨테이너는 UserRepository Bean을 찾아서
자동으로 UserService에 주입한다.
Bean 생성 후, 스프링은 @PostConstruct 가 붙은 메서드를 자동 실행한다.
@PostConstruct
public void init() {
System.out.println("UserService 초기화 완료!");
}
컨테이너는 이제 Bean을 주입 완료한 상태로 보관하고,
요청이 들어올 때마다 필요한 Bean을 주입해준다.
애플리케이션이 종료되면,
스프링은 @PreDestroy 메서드를 호출하여 리소스를 정리한다.
@PreDestroy
public void close() {
System.out.println("UserService 종료!");
}
FinTrack 프로젝트에 등장하는 구성요소(AuthController, AuthService, UserRepository)를
직접 수동으로 스프링 컨테이너에 등록하고 불러오는 과정을 통해,
스프링이 내부적으로 Bean을 관리하는 원리를 눈으로 확인해보았다.
package com.fintrack.config;
import com.fintrack.auth.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration // 설정 클래스: Bean 등록 정보 정의
public class AppConfig {
@Bean
public AuthController authController() {
return new AuthController(authService());
}
@Bean
public AuthService authService() {
return new AuthServiceImpl(userRepository());
}
@Bean
public UserRepository userRepository() {
return new MemoryUserRepository();
}
}
@Bean : 각 메서드의 반환 객체를 스프링 컨테이너에 등록도메인 클래스 구현 후
컨테이너를 직접 생성하고 Bean을 가져와본다.
package com.fintrack.auth;
import com.fintrack.config.AppConfig;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class ContainerTest {
public static void main(String[] args) {
// 스프링 컨테이너 생성
ApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
// 컨테이너에서 Bean 가져오기
AuthController authController = context.getBean("authController", AuthController.class);
// Bean 사용 (회원가입 시뮬레이션)
authController.signup("example@naver.com", "fintrack1234");
}
}
실행결과
✅ 사용자 등록 완료: example@naver.com
| 단계 | 설명 |
|---|---|
| ① | AnnotationConfigApplicationContext 실행 시 AppConfig 로딩 |
| ② | @Bean 메서드 호출 → 반환 객체를 스프링 컨테이너에 등록 |
| ③ | getBean("authController") 호출로 Bean 꺼냄 |
| ④ | 내부적으로 AuthService와 UserRepository가 의존성 주입됨 |
| ⑤ | authController.signup() 실행 시 내부 Bean 간 연동 확인 |
| 단계 | 내용 |
|---|---|
| ① | 어노테이션 스캔 (@ComponentScan) |
| ② | BeanDefinition 생성 |
| ③ | Bean 인스턴스 생성 (Reflection 사용) |
| ④ | 의존성 주입 (DI) |
| ⑤ | 초기화 (@PostConstruct) |
| ⑥ | 서비스 사용 |
| ⑦ | 종료 및 소멸 (@PreDestroy) |
스프링 컨테이너는 단순히 Bean을 “저장”하는 역할을 넘어서
를 담당한다.
“어노테이션은 스프링에게 정보를 전달하고,
스프링 컨테이너는 그 정보를 실행 가능한 코드로 바꿔주는 핵심 엔진이다.”