Spring 스프링 컨테이너(Spring Container)란?

suover

spring

스프링 컨테이너(Spring Container)란?

스프링 컨테이너는 스프링 프레임워크에서 가장 핵심적인 부분으로, IoC(Inversion of Control) 개념을 기반으로 만들어진 의존성 관리 및 객체 생성, 설정, 생명주기 관리를 담당하는 중추적인 역할을 합니다. 쉽게 말해 스프링 컨테이너는 객체(빈, Bean)를 생성하고 관리하는 “공장(factory)”라고 볼 수 있습니다.

  • IoC(Inversion of Control): 제어의 역전이라는 말 그대로, 객체가 의존하는 객체(Bean)들을 직접 생성하고 관리하는 것이 아니라, 스프링 컨테이너가 대신 생성하고 연결(주입)해줍니다. 전통적인 방식에서 개발자가 new 키워드로 의존 객체를 직접 생성하던 부분이 역전되어, 스프링 컨테이너가 담당하게 되는 것이 핵심입니다.
  • DI(Dependency Injection): 의존관계 주입이라고 하며, IoC를 구현하는 방법 중 하나입니다. 스프링 컨테이너가 의존 객체를 대신 주입해주므로, 애플리케이션 코드가 의존 대상에 직접 접근하지 않아도 되며, 설정만으로도 주입 방식이 간편하게 변경될 수 있습니다.

스프링 컨테이너를 이용하면 결합도가 낮은(loose coupling) 애플리케이션 설계가 가능해집니다. 이는 유지보수성과 확장성에서 큰 이점을 제공합니다.

스프링 컨테이너의 종류

스프링 프레임워크에서 핵심적으로 제공하는 컨테이너는 크게 BeanFactoryApplicationContext로 나누어집니다.

  1. BeanFactory
    • 스프링의 가장 기본적인 컨테이너 인터페이스입니다.
    • 객체(빈)를 생성하고 의존관계를 설정하는 기능만 제공합니다.
    • 지연 로딩(lazy loading) 방식을 사용하여, 실제로 객체가 필요할 때에만 생성하는 특성이 있습니다.
    • 상대적으로 단순하고 경량이지만, 부가적인 기능(국제화, 환경변수 처리, AOP, 메시지 처리 등)은 지원하지 않습니다.
  2. ApplicationContext
    • BeanFactory를 확장한 가장 대표적이고 일반적으로 사용하는 스프링 컨테이너입니다.
    • BeanFactory가 제공하는 기능은 모두 포함하며, 추가 기능(국제화, 환경 변수, 애플리케이션 이벤트, AOP, 메시지 소스 처리, 프로파일 설정, 리소스 로딩 등)을 지원합니다.
    • 빈을 미리 생성(Eager loading)하여 런타임 시점 성능을 높일 수 있습니다.
    • 대표적인 구현체
      • ClassPathXmlApplicationContext: classpath에 있는 XML 파일에서 빈 정보를 로딩
      • FileSystemXmlApplicationContext: 파일 시스템 경로에 있는 XML 파일에서 빈 정보를 로딩
      • AnnotationConfigApplicationContext: 자바 애노테이션 기반 설정(@Configuration, @Bean 등)을 읽어 빈을 등록

실무에서는 일반적으로 ApplicationContext를 사용합니다. BeanFactory는 무척 간단한 테스트나 커스텀하게 최적화된 상황 이외엔 잘 쓰이지 않습니다.

스프링 컨테이너의 동작 과정

스프링 컨테이너의 전반적인 동작 흐름은 아래와 같습니다.

  1. 설정 정보 읽기
    • XML, 자바 클래스, 애노테이션 등을 통해 스프링 컨테이너가 빈 설정 정보를 읽어옵니다.
  2. 빈(Bean) 정의 등록
    • 스프링 컨테이너는 읽어온 설정 정보를 토대로 BeanDefinition(빈 정의) 객체를 생성하고 내부에 저장합니다.
    • BeanDefinition에는 빈의 클래스 정보, 스코프(싱글톤, 프로토타입 등), 의존관계 등에 대한 메타데이터가 들어 있습니다.
  3. 빈 생성 및 의존관계 주입
    • 스프링 컨테이너는 필요 시점에 빈을 생성하고, 의존관계를 자동 또는 수동으로 주입합니다.
    • 싱글톤 스코프의 빈은 일반적으로 컨테이너 초기화 시점에 모두 생성되고, 프로토타입 스코프의 빈은 요청 시점에 생성됩니다.
  4. 빈 초기화(Bean Initialization)
    • 빈이 생성된 후, 초기화 메서드(@PostConstruct 애노테이션 또는 init-method 지정 등)를 통해 빈이 내부적으로 필요한 작업을 수행합니다.
  5. 사용(Use)
    • 애플리케이션이 런타임 시점에서 빈을 사용합니다. 컨테이너는 의존성을 이미 주입해 두었으므로, 사용 시점에는 편리하게 활용할 수 있습니다.
  6. 빈 종료(Bean Destruction)
    • 컨테이너가 종료될 때, 소멸 메서드(@PreDestroy 애노테이션 또는 destroy-method 등)를 호출합니다.
    • 싱글톤 빈의 경우 주로 사용되며, 프로토타입 빈의 경우 컨테이너가 관리하지 않으므로 소멸 메서드가 호출되지 않습니다.

스프링 컨테이너 설정 방법

스프링 컨테이너에 빈 설정 정보를 주입하는 방법은 크게 XML 설정, 자바 설정, 애노테이션 설정 으로 나눌 수 있습니다. 실무에서는 주로 애노테이션 + 자바 설정 방식을 많이 활용합니다.

XML 기반 설정

과거 스프링 버전에서 가장 많이 사용되던 전통적 방식입니다. 빈의 의존관계를 명시적으로 관리하기 위해 XML 파일(applicationContext.xml 등)을 사용합니다.

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 파일 대신 자바 클래스를 이용하여 설정할 수 있습니다. 스프링 부트가 적극적으로 추천하는 방법이기도 하며, 애노테이션을 통한 빈 등록을 함께 사용하는 것이 일반적입니다.

Java
@Configuration
public class AppConfig {
    
    @Bean
    public HelloService helloService() {
        return new HelloService();
    }

    @Bean
    public HelloController helloController() {
        // 의존성 주입
        return new HelloController(helloService());
    }
}
Java

AnnotationConfigApplicationContext를 통해 위 설정 클래스를 읽어들여 스프링 컨테이너가 빈을 생성하고 주입합니다.

Java
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: 필요한 의존 객체를 자동으로 주입
Java
@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 계열 애노테이션이 붙은 클래스를 자동으로 스캔해서 빈으로 등록합니다.
Java
@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {
}
Java

빈(Bean) 라이프사이클 상세

스프링 컨테이너에서의 빈 라이프사이클은 다음과 같습니다.

  1. 스프링 컨테이너 초기화
    • 설정 파일(또는 클래스, 애노테이션 등)을 읽고, BeanDefinition 정보를 생성합니다.
  2. 빈 생성(instantiate)
    • 싱글톤 스코프의 빈은 컨테이너 시작 시점에 생성(주입)됩니다.
    • 프로토타입 스코프의 빈은 요청이 있을 때마다 생성됩니다.
  3. 의존관계 주입(Dependency Injection)
    • @Autowired, @Resource, 생성자 주입, setter 주입 등 등록된 설정 방법대로 의존성이 주입됩니다.
  4. 빈 초기화(Bean Initialization)
    • @PostConstruct 애노테이션이 붙은 메서드나, XML 혹은 자바 설정에서 지정된 init-method가 실행됩니다.
    • 이 시점에서 DB 연결 풀 생성, 캐시 초기화 같은 작업을 주로 수행합니다.
  5. 사용(Use)
    • 애플리케이션 로직에서 빈을 자유롭게 사용합니다.
  6. 소멸(Destruction)
    • 컨테이너 종료 시점(@PreDestroy 또는 destroy-method를 통해) 빈이 정리됩니다.
    • 싱글톤 스코프 빈만 컨테이너가 관리하고, 프로토타입 스코프 빈은 사용 후 수동으로 정리해야 합니다.

빈 스코프(Bean Scope)

스프링 컨테이너는 빈마다 스코프(scope)를 지정할 수 있습니다. 스코프는 빈의 생존 범위를 뜻합니다.

  1. Singleton: 컨테이너에 한 개의 빈 인스턴스만 존재 (가장 기본값)
  2. Prototype: 빈을 요청할 때마다 새 인스턴스를 생성
  3. Request: 웹 요청마다 별도의 빈 인스턴스 생성 (Spring MVC 환경 등)
  4. Session: 웹 세션마다 별도의 빈 인스턴스 생성
  5. Application: 서블릿 컨텍스트(애플리케이션) 범위에서 단 하나의 빈 인스턴스 생성

실무에서 가장 많이 사용하는 스코프는 Singleton이고, 가끔 Prototype 스코프가 필요할 때도 있습니다. 웹 애플리케이션 환경이라면 Request, Session, Application 스코프도 사용할 수 있습니다.

간단한 예시: 자바 설정 + 애노테이션

아래 예시는 자바 기반 설정(@Configuration)과 애노테이션(@Component, @Autowired)을 활용하여 스프링 컨테이너에 빈을 등록 및 주입하는 과정을 보여줍니다.

프로젝트 구조
Markdown
com
 └─ example
     ├─ AppConfig.java
     ├─ service
     │   └─ HelloService.java
     └─ controller
         └─ HelloController.java
Markdown

코드
Java
// 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

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

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

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!”이 출력됩니다.

스프링 컨테이너를 활용할 때 주의할 점

  1. 빈 싱글톤 주의
    • 대부분의 빈이 싱글톤 스코프로 관리되므로, 내부에 상태 값을 두는 것은 지양하는 것이 좋습니다.
    • 멀티쓰레드 환경에서 동시성 문제가 발생할 수 있기 때문입니다.
  2. 프로토타입 빈
    • 필요 시마다 매번 새 객체가 생성됩니다.
    • 컨테이너가 생성까지만 관여하고, 소멸 과정은 관여하지 않습니다.
  3. 순환 참조(Circular Reference)
    • A 빈이 B 빈을 참조하고, B 빈도 A 빈을 참조하는 순환구조가 생길 수 있습니다.
    • 스프링 5.3 부터 기본적으로 순환 참조를 막고 있으며(설정 변경 가능), 순환 참조를 피하는 설계를 권장합니다.
  4. 빈 생명주기 메서드
    • 초기화 메서드와 소멸 메서드를 잘 활용하면 리소스 초기화나 정리 로직을 깔끔하게 처리할 수 있습니다.
  5. 환경별 설정 분리
    • 개발/운영 환경별로 다른 설정을 사용해야 할 경우, 프로필(Profiles) 기능이나 설정 파일 분리를 통해 효율적으로 관리할 수 있습니다.

결론

스프링 컨테이너는 스프링 프레임워크의 핵심으로, 객체 생성 및 의존성 관리를 손쉽게 해 주며, 애플리케이션 개발자가 비즈니스 로직에만 집중할 수 있도록 돕습니다. IoCDI 개념을 이해하고, BeanFactoryApplicationContext의 차이, 각 빈의 스코프(scope)라이프사이클 등을 이해하면 스프링을 보다 유연하고 확장성 있게 사용할 수 있습니다.

오늘날 대부분의 스프링 프로젝트는 자바 기반 설정애노테이션을 많이 사용하지만, XML 기반 설정도 여전히 사용될 수 있으며, 프로젝트에 따라 적절히 혼합하여 쓰기도 합니다.

스프링 컨테이너를 제대로 이해하면 결합도를 낮춘 애플리케이션 아키텍처 설계가 가능하며, 다양한 부가 기능(AOP, 트랜잭션, 국제화, 테스트 지원 등)을 자유롭게 적용할 수 있다는 점에서 스프링 생태계 전체를 잘 이해하는 발판이 됩니다.

Leave a Comment