스프링 컨테이너(Spring Container)란?
스프링 컨테이너는 스프링 프레임워크에서 가장 핵심적인 부분으로, IoC(Inversion of Control) 개념을 기반으로 만들어진 의존성 관리 및 객체 생성, 설정, 생명주기 관리를 담당하는 중추적인 역할을 합니다. 쉽게 말해 스프링 컨테이너는 객체(빈, Bean)를 생성하고 관리하는 “공장(factory)”라고 볼 수 있습니다.
- IoC(Inversion of Control): 제어의 역전이라는 말 그대로, 객체가 의존하는 객체(Bean)들을 직접 생성하고 관리하는 것이 아니라, 스프링 컨테이너가 대신 생성하고 연결(주입)해줍니다. 전통적인 방식에서 개발자가 new 키워드로 의존 객체를 직접 생성하던 부분이 역전되어, 스프링 컨테이너가 담당하게 되는 것이 핵심입니다.
- DI(Dependency Injection): 의존관계 주입이라고 하며, IoC를 구현하는 방법 중 하나입니다. 스프링 컨테이너가 의존 객체를 대신 주입해주므로, 애플리케이션 코드가 의존 대상에 직접 접근하지 않아도 되며, 설정만으로도 주입 방식이 간편하게 변경될 수 있습니다.
스프링 컨테이너를 이용하면 결합도가 낮은(loose coupling) 애플리케이션 설계가 가능해집니다. 이는 유지보수성과 확장성에서 큰 이점을 제공합니다.
스프링 컨테이너의 종류
스프링 프레임워크에서 핵심적으로 제공하는 컨테이너는 크게 BeanFactory와 ApplicationContext로 나누어집니다.
- BeanFactory
- 스프링의 가장 기본적인 컨테이너 인터페이스입니다.
- 객체(빈)를 생성하고 의존관계를 설정하는 기능만 제공합니다.
- 지연 로딩(lazy loading) 방식을 사용하여, 실제로 객체가 필요할 때에만 생성하는 특성이 있습니다.
- 상대적으로 단순하고 경량이지만, 부가적인 기능(국제화, 환경변수 처리, AOP, 메시지 처리 등)은 지원하지 않습니다.
- ApplicationContext
- BeanFactory를 확장한 가장 대표적이고 일반적으로 사용하는 스프링 컨테이너입니다.
- BeanFactory가 제공하는 기능은 모두 포함하며, 추가 기능(국제화, 환경 변수, 애플리케이션 이벤트, AOP, 메시지 소스 처리, 프로파일 설정, 리소스 로딩 등)을 지원합니다.
- 빈을 미리 생성(Eager loading)하여 런타임 시점 성능을 높일 수 있습니다.
- 대표적인 구현체
- ClassPathXmlApplicationContext: classpath에 있는 XML 파일에서 빈 정보를 로딩
- FileSystemXmlApplicationContext: 파일 시스템 경로에 있는 XML 파일에서 빈 정보를 로딩
- AnnotationConfigApplicationContext: 자바 애노테이션 기반 설정(@Configuration, @Bean 등)을 읽어 빈을 등록
실무에서는 일반적으로 ApplicationContext를 사용합니다. BeanFactory는 무척 간단한 테스트나 커스텀하게 최적화된 상황 이외엔 잘 쓰이지 않습니다.
스프링 컨테이너의 동작 과정
스프링 컨테이너의 전반적인 동작 흐름은 아래와 같습니다.
- 설정 정보 읽기
- XML, 자바 클래스, 애노테이션 등을 통해 스프링 컨테이너가 빈 설정 정보를 읽어옵니다.
- 빈(Bean) 정의 등록
- 스프링 컨테이너는 읽어온 설정 정보를 토대로 BeanDefinition(빈 정의) 객체를 생성하고 내부에 저장합니다.
- BeanDefinition에는 빈의 클래스 정보, 스코프(싱글톤, 프로토타입 등), 의존관계 등에 대한 메타데이터가 들어 있습니다.
- 빈 생성 및 의존관계 주입
- 스프링 컨테이너는 필요 시점에 빈을 생성하고, 의존관계를 자동 또는 수동으로 주입합니다.
- 싱글톤 스코프의 빈은 일반적으로 컨테이너 초기화 시점에 모두 생성되고, 프로토타입 스코프의 빈은 요청 시점에 생성됩니다.
- 빈 초기화(Bean Initialization)
- 빈이 생성된 후, 초기화 메서드(@PostConstruct 애노테이션 또는 init-method 지정 등)를 통해 빈이 내부적으로 필요한 작업을 수행합니다.
- 사용(Use)
- 애플리케이션이 런타임 시점에서 빈을 사용합니다. 컨테이너는 의존성을 이미 주입해 두었으므로, 사용 시점에는 편리하게 활용할 수 있습니다.
- 빈 종료(Bean Destruction)
- 컨테이너가 종료될 때, 소멸 메서드(@PreDestroy 애노테이션 또는 destroy-method 등)를 호출합니다.
- 싱글톤 빈의 경우 주로 사용되며, 프로토타입 빈의 경우 컨테이너가 관리하지 않으므로 소멸 메서드가 호출되지 않습니다.
스프링 컨테이너 설정 방법
스프링 컨테이너에 빈 설정 정보를 주입하는 방법은 크게 XML 설정, 자바 설정, 애노테이션 설정 으로 나눌 수 있습니다. 실무에서는 주로 애노테이션 + 자바 설정 방식을 많이 활용합니다.
XML 기반 설정
과거 스프링 버전에서 가장 많이 사용되던 전통적 방식입니다. 빈의 의존관계를 명시적으로 관리하기 위해 XML 파일(applicationContext.xml 등)을 사용합니다.
<!-- applicationContext.xml -->
<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) 정의 -->
<bean id="helloService" class="com.example.service.HelloService" />
<bean id="helloController" class="com.example.controller.HelloController">
<!-- helloService 빈을 주입 -->
<property name="helloService" ref="helloService" />
</bean>
</beans>
XML- 장점: 설정이 명시적이고 직관적이어서, 외부에서 XML을 통해 전체 설정을 한눈에 파악하기 쉽습니다.
- 단점: 설정 파일과 실제 코드가 분리되어 있으므로 IDE 자동완성 및 리팩토링이 어렵고, 설정 파일이 복잡해질수록 관리가 어려워집니다.
자바(AnnotationConfig) 기반 설정
XML 파일 대신 자바 클래스를 이용하여 설정할 수 있습니다. 스프링 부트가 적극적으로 추천하는 방법이기도 하며, 애노테이션을 통한 빈 등록을 함께 사용하는 것이 일반적입니다.
@Configuration
public class AppConfig {
@Bean
public HelloService helloService() {
return new HelloService();
}
@Bean
public HelloController helloController() {
// 의존성 주입
return new HelloController(helloService());
}
}
JavaAnnotationConfigApplicationContext를 통해 위 설정 클래스를 읽어들여 스프링 컨테이너가 빈을 생성하고 주입합니다.
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
HelloController controller = context.getBean(HelloController.class);
controller.sayHello();
}
Java- 장점:
- IDE 자동완성과 컴파일 체크를 통해 안정적인 리팩토링이 가능
- 코드와 설정이 함께 있어, 유지보수가 용이
- 단점: 설정(메서드)과 구현 로직이 같은 클래스로 섞일 경우, 구분이 헷갈릴 수 있음
애노테이션 기반 자동 빈 등록
- @Component: 해당 클래스를 스프링 빈으로 등록하겠다는 표시
- @Controller, @Service, @Repository: @Component를 확장한 대표적 애노테이션들로, 계층형 구조에서 역할을 나타낼 때 사용
- @Autowired: 필요한 의존 객체를 자동으로 주입
@Component
public class HelloService {
public String greet(String name) {
return "Hello, " + name;
}
}
@Controller
public class HelloController {
@Autowired
private HelloService helloService;
public void sayHello() {
System.out.println(helloService.greet("Spring"));
}
}
Java- @ComponentScan을 통해 특정 패키지 이하의 클래스 중 @Component 계열 애노테이션이 붙은 클래스를 자동으로 스캔해서 빈으로 등록합니다.
@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {
}
Java빈(Bean) 라이프사이클 상세
스프링 컨테이너에서의 빈 라이프사이클은 다음과 같습니다.
- 스프링 컨테이너 초기화
- 설정 파일(또는 클래스, 애노테이션 등)을 읽고, BeanDefinition 정보를 생성합니다.
- 빈 생성(instantiate)
- 싱글톤 스코프의 빈은 컨테이너 시작 시점에 생성(주입)됩니다.
- 프로토타입 스코프의 빈은 요청이 있을 때마다 생성됩니다.
- 의존관계 주입(Dependency Injection)
- @Autowired, @Resource, 생성자 주입, setter 주입 등 등록된 설정 방법대로 의존성이 주입됩니다.
- 빈 초기화(Bean Initialization)
- @PostConstruct 애노테이션이 붙은 메서드나, XML 혹은 자바 설정에서 지정된 init-method가 실행됩니다.
- 이 시점에서 DB 연결 풀 생성, 캐시 초기화 같은 작업을 주로 수행합니다.
- 사용(Use)
- 애플리케이션 로직에서 빈을 자유롭게 사용합니다.
- 소멸(Destruction)
- 컨테이너 종료 시점(@PreDestroy 또는 destroy-method를 통해) 빈이 정리됩니다.
- 싱글톤 스코프 빈만 컨테이너가 관리하고, 프로토타입 스코프 빈은 사용 후 수동으로 정리해야 합니다.
빈 스코프(Bean Scope)
스프링 컨테이너는 빈마다 스코프(scope)를 지정할 수 있습니다. 스코프는 빈의 생존 범위를 뜻합니다.
- Singleton: 컨테이너에 한 개의 빈 인스턴스만 존재 (가장 기본값)
- Prototype: 빈을 요청할 때마다 새 인스턴스를 생성
- Request: 웹 요청마다 별도의 빈 인스턴스 생성 (Spring MVC 환경 등)
- Session: 웹 세션마다 별도의 빈 인스턴스 생성
- Application: 서블릿 컨텍스트(애플리케이션) 범위에서 단 하나의 빈 인스턴스 생성
실무에서 가장 많이 사용하는 스코프는 Singleton이고, 가끔 Prototype 스코프가 필요할 때도 있습니다. 웹 애플리케이션 환경이라면 Request, Session, Application 스코프도 사용할 수 있습니다.
간단한 예시: 자바 설정 + 애노테이션
아래 예시는 자바 기반 설정(@Configuration)과 애노테이션(@Component, @Autowired)을 활용하여 스프링 컨테이너에 빈을 등록 및 주입하는 과정을 보여줍니다.
프로젝트 구조
com
└─ example
├─ AppConfig.java
├─ service
│ └─ HelloService.java
└─ controller
└─ HelloController.java
Markdown코드
// com/example/service/HelloService.java
package com.example.service;
import org.springframework.stereotype.Service;
@Service
public class HelloService {
public String sayHello(String name) {
return "Hello, " + name + "!";
}
}
Java// com/example/controller/HelloController.java
package com.example.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import com.example.service.HelloService;
@Controller
public class HelloController {
private final HelloService helloService;
// 생성자 주입
@Autowired
public HelloController(HelloService helloService) {
this.helloService = helloService;
}
public void processHello() {
String greeting = helloService.sayHello("Spring");
System.out.println(greeting);
}
}
Java// com/example/AppConfig.java
package com.example;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {
// 필요한 경우 @Bean 정의를 추가로 넣을 수 있습니다.
}
Java// Main.java
package com.example;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import com.example.controller.HelloController;
public class Main {
public static void main(String[] args) {
ApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
HelloController controller = context.getBean(HelloController.class);
controller.processHello();
}
}
Java- 실행 시, 스프링 컨테이너가 AppConfig를 기반으로 @ComponentScan을 수행하여 @Controller와 @Service 등을 찾아 빈으로 등록합니다.
- 생성자 주입(@Autowired)을 통해 HelloService 빈이 HelloController에 자동으로 전달됩니다.
- controller.processHello()가 호출되면 “Hello, Spring!”이 출력됩니다.
스프링 컨테이너를 활용할 때 주의할 점
- 빈 싱글톤 주의
- 대부분의 빈이 싱글톤 스코프로 관리되므로, 내부에 상태 값을 두는 것은 지양하는 것이 좋습니다.
- 멀티쓰레드 환경에서 동시성 문제가 발생할 수 있기 때문입니다.
- 프로토타입 빈
- 필요 시마다 매번 새 객체가 생성됩니다.
- 컨테이너가 생성까지만 관여하고, 소멸 과정은 관여하지 않습니다.
- 순환 참조(Circular Reference)
- A 빈이 B 빈을 참조하고, B 빈도 A 빈을 참조하는 순환구조가 생길 수 있습니다.
- 스프링 5.3 부터 기본적으로 순환 참조를 막고 있으며(설정 변경 가능), 순환 참조를 피하는 설계를 권장합니다.
- 빈 생명주기 메서드
- 초기화 메서드와 소멸 메서드를 잘 활용하면 리소스 초기화나 정리 로직을 깔끔하게 처리할 수 있습니다.
- 환경별 설정 분리
- 개발/운영 환경별로 다른 설정을 사용해야 할 경우, 프로필(Profiles) 기능이나 설정 파일 분리를 통해 효율적으로 관리할 수 있습니다.
결론
스프링 컨테이너는 스프링 프레임워크의 핵심으로, 객체 생성 및 의존성 관리를 손쉽게 해 주며, 애플리케이션 개발자가 비즈니스 로직에만 집중할 수 있도록 돕습니다. IoC와 DI 개념을 이해하고, BeanFactory와 ApplicationContext의 차이, 각 빈의 스코프(scope)와 라이프사이클 등을 이해하면 스프링을 보다 유연하고 확장성 있게 사용할 수 있습니다.
오늘날 대부분의 스프링 프로젝트는 자바 기반 설정과 애노테이션을 많이 사용하지만, XML 기반 설정도 여전히 사용될 수 있으며, 프로젝트에 따라 적절히 혼합하여 쓰기도 합니다.
스프링 컨테이너를 제대로 이해하면 결합도를 낮춘 애플리케이션 아키텍처 설계가 가능하며, 다양한 부가 기능(AOP, 트랜잭션, 국제화, 테스트 지원 등)을 자유롭게 적용할 수 있다는 점에서 스프링 생태계 전체를 잘 이해하는 발판이 됩니다.