[SpringBoot] 스프링 컨테이너의 작동 원리와 Bean 주입 과정

TNFUDS·2025년 11월 14일

FinTrack 프로젝트

목록 보기
5/14
post-thumbnail

지난 컨텐츠를 작성하다가
스프링 컨테이너 등의 용어가 생소해 공부 및 정리가 필요한 것 같았다.
이번 기회에 스프링 컨테이너가 뭔지, 어떻게 작동하는지에 대해 정리하고자 한다.

1. 스프링 컨테이너란?

스프링 컨테이너는 객체(Bean) 를 생성하고,
생명주기를 관리하며, 의존성을 주입하는 역할을 하는 중앙 관리자이다.

스프링 컨테이너는 XML, 어노테이션 기반의 자바 설정 클래스로 만들 수 있다.
스프링부트(Spring Boot)를 사용하기 이전에는 xml을 통해 직접적으로 설정해 주어야 했지만,
스프링부트가 등장하면서 대부분 사용하지 않게 되었다.

스프링 컨테이너의 종류는 다음과 같다.

두 인터페이스가 대표적인 스프링 컨테이너이다.

  • ApplicationContext
  • BeanFactory

빈 팩토리는 빈의 생성과 관계 설정 같은 제어를 담당하는 IoC 오브젝트이고,
빈 팩토리를 좀 더 확장한 것이 애플리케이션 컨텍스트이다.

즉, 애플리케이션 컨텍스트는 IoC 방식을 따라 만들어진 일종의 빈 팩토리로, 둘 다 동일한 개념이라 간주해도 된다.
주로 사용되는 스프링 컨테이너는 애플리케이션 컨텍스트이다.

BeanFactory

스프링 컨테이너의 최상위 인터페이스
빈을 등록, 생성, 조회하는 등 관리 역할을 하며, getBean() 메서드 통해 인스턴스화 할 수 있다.
@Bean 어노테이션이 붙은 메서드의 이름을 스프링 빈의 이름으로 사용하여 빈 등록을 한다.

ApplicationContext

BeanFactory의 기능을 상속받아 제공한다.

  • MessageSource (메시지 다국화 인터페이스)
  • ApplicationEventPublisher(이벤트 관련 기능 제공 인터페이스)
  • ResourceLoader(파일, 클래스패스, 외부 등 리소스 조회)
  • EnvironmentCapable(변수 나누어 처리하고, 애플리케이션 구동 시 필요 정보 관리 인터페이스)

차이점

구분BeanFactoryApplicationContext
역할기본적인 Bean 관리 기능BeanFactory 기능 + 확장 기능 제공
기능Bean 생성, DI 관리국제화, 이벤트 처리, AOP 지원, 환경 설정
사용 예메모리 절약이 중요한 테스트대부분의 스프링 애플리케이션

컨테이너의 기능

컨테이너는 개발자가 정의한 빈을 객체로 만들어 관리하고 개발자가 필요로 할 때 제공한다.
의존성 주입(DI)을 통해 애플리케이션의 컴포넌트를 관리할 수 있다.
또한, 서로 다른 빈을 연결하여 애플리케이션 빈을 연결하는 역할을 한다.

개발자는 모듈 간 의존 및 결합으로 인해 발생할 수 있는 문제에서 벗어날 수 있다.


2. IoC (Inversion of Control, 제어의 역전)

UserService userService = new UserService();
이처럼 전통적인 자바에서는 개발자가 직접 객체를 만들고 관리한다.

일반적인 자바 프로그램에서 개발자는 main() 메서드로 프로그램의 시작을 제어하고,
필요한 객체를 생성하고, 메서드를 호출하며, 프로그램이 실행되는 전체 흐름을 관리한다.

그러나 대규모 애플리케이션에서는 이런 방식이 비효율적일 수도 있다.
시스템이 커지면 객체 간의 의존성이 복잡해지고, 코드의 결합도가 높아지기 때문에 유지보수와 확장이 어려워지기 때문이다.

하지만 스프링에서는 제어 흐름이 역전(Inverted) 된다.
즉, 객체의 생성과 주입을 개발자가 아닌 컨테이너가 담당한다.

개발자는 “무엇을 쓸지”만 선언하고,
“언제, 어떻게” 만들지는 스프링이 알아서 처리한다.

IoC 구현된 기능

  • 1. 객체 생성과 관리

    • 개발자는 애플리케이션에서 사용할 객체를 직접 생성하지 않고, 스프링 컨테이너가 대신 생성하고 관리하며 객체의 생명 주기를 프레임워크가 제어한다.
  • 2. 의존성 주입

    • 객체가 필요로 하는 의존성을 프레임워크가 주입한다.
    • 개발자는 의존성을 스스로 관리하지 않아도 되며, 스프링이 자동으로 필요한 객체를 주입한다.
  • 3. 애플리케이션의 생명 주기 관리

    • 스프링은 객체의 생성, 초기화, 소멸 등을 포함한 전체 생명 주기를 관리한다.
    • 언제 객체가 생성되고 소멸하는지 제어한다.
  • 4. AOP (관점 지향 프로그래밍)

    • 스프링은 로깅이나 트랜잭션 관리 같은 횡단 관심사를 자동으로 처리하기 때문에 개발자는 핵심 비즈니스 로직에만 집중할 수 있다.
  • 5. 웹 요청 처리

    • 스프링의 MVC 패턴에서는 개발자가 웹 요청을 처리하는 방식을 직접 제어하지 않는다.
    • 스프링이 요청을 컨트롤러에 전달하고, 이후의 흐름을 관리한다.

3. DI (Dependency Injection, 의존성 주입)

의존성이란 “어떤 객체가 다른 객체를 필요로 하는 관계”이다.
DI는 이 관계를 스프링이 대신 연결해주는 방식이다.

이 방식은 제어의 역전(IoC)의 구체적인 구현 방법 중 하나이다.
이를 통해 객체 간의 결합도를 낮추고, 코드의 유연성과 재사용성을 높이며, 테스트와 유지보수를 훨씬 더 쉽게 할 수 있다.

의존성 주입 방법 3가지

주입 방법설명
생성자 주입권장 방식. 불변성 유지 가능
필드 주입간단하지만 테스트 어려움
Setter 주입선택적 의존성에 사용

생성자 주입
생성자 주입은 객체가 생성될 때 필요한 의존성을 생성자의 매개변수로 전달받는 방식
이 방법은 객체가 생성될 때 반드시 필요한 의존성을 제공해야 하기 때문에,
불완전한 객체 생성을 방지할 수 있다.

필드 주입
인터페이스 주입은 주입할 의존성을 위한 인터페이스를 제공하고, 이를 구현하는 클래스가 의존성을 주입받는 방식

Setter 주입
객체가 생성된 후에 의존성을 주입하는 방식
의존성이 필수적이지 않고, 나중에 주입될 수 있을 때 유용하다.
그러나 필수적인 의존성인지 명확히 확인하기가 어렵기에, 테스트 시 객체가 불완전한 상태로 사용될 위험이 있다.

스프링에서는 주로 생성자 주입이 권장된다.
이는 필수적인 의존성을 명확히 하고, 객체가 생성될 때 필요한 모든 의존성을 즉시 주입받도록 보장한다.


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

스프링 컨테이너는 아래 단계로 Bean을 등록하고 관리한다.

① 설정 파일 or 어노테이션 스캔

@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 생성

스프링은 스캔한 클래스 정보를 기반으로 BeanDefinition 객체를 생성한다.
이것은 “이 클래스로 Bean을 만들겠다”는 설명서 역할을 한다.

  • @Component → BeanDefinition 등록
  • @Bean → BeanDefinition 등록

③ Bean 인스턴스 생성

스프링은 BeanDefinition 정보를 바탕으로
리플렉션(Reflection)을 사용해 객체를 실제로 생성한다.

Class<?> clazz = Class.forName("com.fintrack.service.UserService");
Object bean = clazz.getDeclaredConstructor().newInstance();

리플렉션
이전에 어노테이션 관련 포스팅을 작성할 때

실행 중에 클래스의 정보를 읽거나 조작할 수 있는 기술
쉽게 말하자면 코드가 자기 자신을 분석할 수 있는 능력

이라고 했다.

즉, 구체적인 Class Type을 알지 못하더라도 해당 Class의 method, type, variable들에 접근할 수 있도록 해주는 자바 API이며,
컴파일된 바이트 코드를 통해 Runtime에 동적으로 특정 Class의 정보를 추출할 수 있는 프로그래밍 기법이다.


④ 의존성 주입 (DI; Dependency Injection)

생성된 Bean들 중 필요한 의존성을 자동으로 연결한다.

예를 들어,

@Service
public class UserService {
    private final UserRepository userRepository;

    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

여기서 스프링 컨테이너는 UserRepository Bean을 찾아서
자동으로 UserService에 주입한다.


⑤ 초기화 (Initialization)

Bean 생성 후, 스프링은 @PostConstruct 가 붙은 메서드를 자동 실행한다.

@PostConstruct
public void init() {
    System.out.println("UserService 초기화 완료!");
}

⑥ 사용 (Injection 완료 후 서비스 동작)

컨테이너는 이제 Bean을 주입 완료한 상태로 보관하고,
요청이 들어올 때마다 필요한 Bean을 주입해준다.


⑦ 종료 및 소멸 (Destroy)

애플리케이션이 종료되면,
스프링은 @PreDestroy 메서드를 호출하여 리소스를 정리한다.

@PreDestroy
public void close() {
    System.out.println("UserService 종료!");
}

실제 FinTrack 프로젝트로 확인하기

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 꺼냄
내부적으로 AuthServiceUserRepository가 의존성 주입됨
authController.signup() 실행 시 내부 Bean 간 연동 확인

5. 정리: 스프링 컨테이너의 동작 순서

단계내용
어노테이션 스캔 (@ComponentScan)
BeanDefinition 생성
Bean 인스턴스 생성 (Reflection 사용)
의존성 주입 (DI)
초기화 (@PostConstruct)
서비스 사용
종료 및 소멸 (@PreDestroy)

마무리

스프링 컨테이너는 단순히 Bean을 “저장”하는 역할을 넘어서

  • Bean의 생성 시점 제어,
  • 의존 관계 관리,
  • 생명주기 전반의 자동 관리

를 담당한다.

“어노테이션은 스프링에게 정보를 전달하고,
스프링 컨테이너는 그 정보를 실행 가능한 코드로 바꿔주는 핵심 엔진이다.”

profile
내 세상을 넓혀가는 중

0개의 댓글