본문 바로가기

Spring

[Spring] Spring Bean이란?

Spring을 공부하다보면 빈을 등록 및 사용한다는 표현을 자주 접하게 된다.

그러나 정작 Bean이라는 개념에 대해 자세히 알고 있지 못한 것 같아 이번 기회에 조사해보게 되었다.


🫘 Bean이란?

공식문서에 따르면 Spring IoC Container가 관리하는 순수 자바 객체(POJO)를 의미한다. 즉, 스프링 컨테이너에 등록된 인스턴스화된 객체를 Bean이라 부르는 것이다.

 

생성된 빈은 빈이름 - 인스턴스화된 객체의 Map 형태로 SingletonBeanRegistry에 저장된다. 이후 컨테이너에서 관리되는 객체에 대해서는 빈이름을 통해 항상 동일한 인스턴스를 조회할 수 있게 된다.
(구현체 - DefaultSingletonBeanRegistry)

 

+) POJO (Plain Old Java Object)

: Java로 생성하는 순수한 객체를 의미한다. 다른 인테페이스나 클래스를 상속받는 등 특정 기술에 종속되어 동작하는 것이 아닌, getter/setter로 구성된 가장 순수한 형태의 기본 클래스이다.

 

+) Registry

: key-value 형태로 데이터를 저장하는 방법을 의미한다. Spring 이외의 여러 분야에서도 통용되고 있다.


🚚 Spring IoC Container

Spring IoC Container는 스프링 빈의 생명 주기 및 의존성을 관리하는 역할을 하며, 보통 2가지로 구분된다.

 

1. BeanFactory

  • 스프링 컨테이너의 최상위 인터페이스로, 스프링 빈을 관리하고 조회하는 역할, 즉 IoC의 기본 기능에 초점을 맞춘 역할을 수행한다. 
  • 빈 객체의 로딩 요청이 올 때, 빈이 생성/로딩되는 Lazy Loading(지연 로딩) 방식으로 작동된다.

2. ApplicationContext

  • BeanFactory를 확장한 하위 인터페이스로, 빈을 관리하는 역할 이외에도 스프링이 제공하는 부가 서비스들을 추가적으로 사용할 수 있다.
  • 로딩 요청이 오지 않더라도 애플리케이션이 실행될 때 빈을 미리 생성/로딩 시켜두는 Pre-loading(즉시 로딩) 방식으로 작동한다. 덕분에 로딩 시간을 줄일 수 있어 애플리케이션 시작 시간을 단축할 수 있지만, 필요 없는 빈들도 로딩해두는 방식인 만큼 더 많은 메모리를 소모한다.

이러한 특징 때문에 메모리 소모량을 줄여야하는 특수 상황에는 BeanFactory를 사용하기도 하지만, 대부분의 경우에는 더 많은 기능을 제공해주는 ApplicationContext를 사용하는 것이 권장된다. 여기서 IoC 컨테이너라 칭하는 것들도 ApplicationContext라 이해하면 된다.


ApplicationContext가 부가적으로 제공하는 기능들은 상속받는 클래스들을 통해 확인할 수 있다.

아래와 같이 BeanFactory 외에도 다양한 기능들이 상속된 클래스를 통해 제공된다

ApplicationContext.class

  • ListableBeanFactory
    : (BeanFactory) 빈 목록을 조회하는 기능
  • HierarchicalBeanFactory
    : (BeanFactory) 부모 BeanFactory에 등록된 빈 객체들을 상속받아 사용 가능
  • EnviromentCapable
    : 애플리케이션 환경을 알아내는 기능 제공
    : 실행 환경에 따라 다양한 설정 및 환경 변수 사용 가능
  • MessageSource
    : 국제화 기능을 지원하는 메시지 관리 기능 (한국 → 한국어, 미국 → 영어 메시지 제공 등)
  • ApplicationEventPublisher
    : 이벤트를 발생시키거나, 해당 이벤트를 처리하는 리스너 등록 가능
  • ResourcePatternResolver
    : 다양한 리소스를 검색하고, 이를 이용해 필요한 작업 수행 가능

♻️ Bean LifeCycle (빈 생명 주기)

그렇다면 이 IoC 컨테이너는 구체적으로 어떻게 빈을 관리해주는걸까? 빈의 생명 주기를 확인하면 이를 알 수 있다.

 

[ 생명주기 ]

: Spring Container 생성 → Bean 생성 → 의존성 주입 → 초기화 콜백 → Bean 사용 → 소멸 전 콜백 → 스프링 종료

 

차례대로 하나씩 살펴보자


1. Spring Container 생성

빈의 생성을 담당하는 컨테이너가 생성된다.

 

2. Bean 생성

생성된 컨테이너가 빈을 생성한다.

먼저, 우리가 설정한 빈에 대한 정보(Configuration Metadata)를 통해 Bean Definition을 생성한다. 이렇게 생성된 definition 정보는 BeanDefinitionResitry에 저장된다.

BeanDefinitionRegistry.class

 

Bean Definition에 저장되는 정보들 중 일부는 다음과 같다.

  • Scope : singleton, prototype 등
  • 초기화 시점: (True) 빈 조회 시점에 초기화 / (False - default) 구동 시점에 초기화
  • AutowireCandidate : 다른 빈 객체에게 Autowired 되는지 여부
  • primary : 상위 타입이 같은 구현체들 사이에서 의존 주입 시 우선순위 갖는지 여부

이후, POJO 객체와  BeanDefition을 바탕으로 빈 인스턴스화를 진행한다. 필요시 빈 후처리기(BeanPostProcessor)가 추가적인 후처리 작업을 수행한다.

 

마지막으로, 완성된 빈이 컨테이너(SingletonBeanRegistry)에 저장된다.

 

3. 의존성 주입

@Autowired, 생성자 주입 등을 통해 의존성을 연결한다.

 

4. 초기화 콜백

여기서의 Callback (콜백)이란, 스프링 빈이 생성/소멸될 때 초기화 및 종료 작업을 수행할 수 있도록 하는 메소드를 호출하는 행위를 의미한다. 예를 들어 DB Connection에 대한 정보를 한 번의 초기화를 통해 계속 사용하고자 하는 경우 등을 생각할 수 있다.

 

Spring에서는 크게 3가지 방법을 통해 생명주기 콜백을 지원한다. (더 찾아보기)

  • 인터페이스 방식
    : 사실상 거의 사용되지 않음
  • 설정 정보에 초기화 메서드, 종료 메서드 지정
    : 외부 라이브러리를 빈으로 등록할 때 초기화/종료 메소드가 필요한 경우 등에 사용됨
  • Annotation 방식
    : @PostConstruct, @PreDestroy  - 가장 흔하게 사용됨

 

5. Bean 사용

저장된 빈들을 사용한다.

 

6. 소멸 전 콜백

마찬가지로 소멸 시 종료 작업을 수행할 수 있도록 하는 메소드를 호출하는 행위를 의미한다.

 

Singleton 빈의 경우 IoC 컨테이너가 종료될 때 빈도 함께 소멸되기 때문에, 안전한 소멸을 위한 사전 작업들이 필요하다. DBConnection을 close하는 등의 작업을 생각할 수 있다.

 

7. 스프링 종료

모든 과정이 끝나면, 스프링 애플리케이션이 종료된다.


🔭 Bean Scope

앞서 BeanDefinition에 Scope 정보가 저장된다고 언급히였다. 이는 빈이 존재할 수 있는 범위를 의미하며, 대부분의 경우에는 Singleton 스코프를 가지지만 경우에 따라 원하는 스코프를 지정해주는 것도 가능하다.

 

1. Singleton

객체의 인스턴스가 오직 1개만 생성되는 것을 보장하는 패턴이다. 스프링 컨테이너가 빈의 생성부터 소멸까지 관리하며, 컨테이너의 시작부터 종료까지 유지되는 가장 넓은 스코프를 가진다.

 

그러나 하나의 객체가 여러 곳에서 공유되기 때문에 상태를 가질 경우 복합적인 문제가 발생할 수 있어, Singleton 빈으로 등록되는 객체는 stateless하도록 설계하는 것이 좋다.

2. Prototype

요청이 올 때마다 객체가 생성된다. 따라서 스프링 컨테이너는 빈의 생성 및 의존 관계 주입까지만을 관리하며, 이후의 생명주기에는 관여하지 않는다. 이처럼 여러 객체가 생성되기 때문에 상태를 가질 수 있다.

 

그러나 자동주입이 필요한 경우 구현체가 여러개이면, 컨테이너가 어떤 객체를 주입해야할 지 몰라서 충돌이 발생하게 된다. 이러한 문제를 해결하기 위해서는 Annotation을 통해 우선순위를 지정해주어야 한다.

  • 만약 여러 객체 중 항상 하나의 객체만을 주입받고 싶다면, 해당 객체에 @Primary를 붙여주면 된다.
  • 상황에 따라 다른 구현체를 쓰고자 한다면, @Qualifier("~") 내부에 빈 이름을 넣어 지정해줌으로써 특정 구현체를 주입받을 수 있다.

3. 웹 스코프

웹 환경에서만 동작하는 스코프로, 위의 2개에 비해 중요도는 덜하다.

아래와 같은 스코프들이 웹 스코프에 해당된다.

  • request
    : HTTP 요청이 들어오고 나갈 때까지 유지
  • session
    : HTTP 세션이 생성되고 종료될 때까지 유지
  • application
    : servlet context와 동일한 생명 주기를 가짐
  • websocket
    : 웹 소켓과 동일한 생명주기를 가짐

📋Bean 등록 방법

이제 빈을 등록하는 방법에 대해 알아보겠다.

빈 등록이란, 어떤 객체를 Bean으로 관리할 것인지에 대한 정보를 알려주는 것이라 볼 수 있다.

 

먼저, Bean으로 등록할 간단한 객체를 만들어준다. 빈 정보 확인을 위한 임시 이름을 넣어 주었다.

public class TestBean {
    private String name = "TEST_NAME";

    public String getName() {
        return name;
    }
}

 

이제 빈을 등록해보자.


1. XML 설정 파일

먼저, resources 폴더 내부에 .xml 파일을 추가하여 빈 정보를 등록할 수 있다.

<beans> 태그 안에 <bean> 태그를 여러개 추가하여 원하는 객체들을 빈으로 등록한다. 이때, 태그 내부에 작성한 id값이 실제 빈 이름으로 저장된다.

<?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="testBean" class="com.spring.TestBean"/>
<!--    <bean id="testBean2" class="com.spring.TestBean2"/>-->
<!--    <bean id="testBean3" class="com.spring.TestBean3"/>-->
</beans>

 

잘 등록되었는지 확인하기 위해 간단한 테스트 코드를 만들었다.

이전에 언급한 컨테이너, 즉 ApplicationContext에 직접 접근하여 빈이름으로 객체를 가져오는 로직이다.

class ApplicationTests {
	@Test
	@DisplayName("XML로 빈 등록")
	void xmlBeanTest() {
		ApplicationContext context = new ClassPathXmlApplicationContext("classpath:application.xml");
		TestBean tb = (TestBean) context.getBean("testBean");
		System.out.println(tb.getName());
	}
}

 

등록한 이름대로 잘 나온다!


2. Java Configuration

두번째 방법은 @Configuration 파일 내부에 @Bean을 통해 빈을 등록하는 방법이다.

 

아래와 같이 @Configuration 어노테이션을 붙여주고, 내부에 빈으로 등록하고 싶은 객체를 반환하는 메소드를 작성해주면 된다. 이때, 각각의 메소드에는 @Bean 어노테이션을 붙여준다. 여기서 빈의 이름은 메소드명으로 저장된다.

@Configuration
public class BeanTestConfig {
    @Bean
    public TestBean testBean() {
        return new TestBean();
    }
}

 

이번에도 동일한 방식으로 테스트 코드를 작성해주었다.

@SpringBootTest
class ApplicationTests {
    @Test
    @DisplayName("Config로 빈 등록")
    void configBeanTest() {
        ApplicationContext context = new AnnotationConfigApplicationContext(BeanTestConfig.class);
        TestBean tb = (TestBean) context.getBean("testBean");
        System.out.println(tb.getName());
    }
}

 

이번에도 잘 조회된다.


3. Annotation

마지막 방법은 원하는 객체 위에 @Component 어노테이션을 달아주는 가장 간편한 방법이다.

 

객체에 해당 어노테이션이 달려있으면, 컴포넌트 스캔 과정에서 빈으로 인식되어 자동으로 컨테이너에 등록된다. 여기서는 클래스명의 맨 앞글자를 소문자로 변환시킨 것이 빈의 이름으로 등록된다.

 

@Component는 흔히 사용되는 @Controller, @Service, @Repository, @Configuration 등에도 들어있기 때문에 이들을 싱글톤 빈으로 관리 및 사용할 수 있다.

@Component
public class TestBean {
    private String name = "TEST_NAME";
    private int price;

    public String getName() {
        return name;
    }
}

 

동일한 테스트 코드를 통해 빈을 조회하였다.

@SpringBootTest
class ApplicationTests {

    @Autowired
    ApplicationContext context;

    @Test
    @DisplayName("Annotation으로 빈 등록")
    void annoBeanTest() {
        TestBean tb = (TestBean) context.getBean("testBean");
        System.out.println(tb.getName());
    }
}

 

마지막 방법까지 빈 등록이 잘 된 모습을 확인할 수 있었다.


ComponentScan

앞서 언급한 ComponentScan에 대해 더 자세히 알아보자.

 

ComponentScan은 @Component 어노테이션을 스캔하여 실제 빈으로 등록해주는 역할을 한다.

이때, @ComponentScan의 설정들을 기준으로 스캔을 수행하며, 기준은 크게 2가지 방법으로 설정해줄 수 있다.

 

1. basePackages

  • @ComponentScan(basePackages = "com.spring")
  • 지정된 패키지 내부에 대해 스캔을 수행

2. basePackageClases

  • @ComponentScan(basePackageClasses = Application.class)
  • 지정된 Class가 위치한 곳에서부터 하위 패키지까지 모두 스캔

해당 정보들은 기본적으로 @SpringBootApplication 내부에 들어있기 때문에, 따로 지정해줘야 하는 특수 케이스가 아니라면 크게 신경쓰지 않아도 스캔이 수행된다.

@SpringBootApplication


@Component vs. @Configuration + @Bean

사실 XML 방식은 거의 사용되지 않는다. 그렇다면 나머지 2가지 방식은 각각 어떤 상황에서 사용되는 것일까?

 

먼저, 어노테이션 방식의 경우, 개발자가 직접 제어 가능한 클래스를 빈으로 등록하고자 할 때 사용된다. 이처럼 싱글톤 빈을 등록하게 되면 해당 객체가 호출마다 생성되지 않기 때문에, 메모리적인 이점을 얻어갈 수 있다.

 

그러나 외부 라이브러리나 내장 클래스 등을 이용하는 경우에는 우리가 직접 코드를 수정할 수 없다. 즉, @Component를 객체 자체에 달기 어려워진다. 이 경우에는 Java Configuration 방식을 사용함으로써 이러한 제약의 구애 없이 빈 등록의 이점들을 얻어갈 수 있다.

 

또한, 빈 정보가 모두 Config 파일 안에 들어있어 한 눈에 확인하기 쉽다는 장점이 있기 때문에, 여러 빈들을 직접 생성/조립하고 싶은 경우에도 유용하다.

 

마지막으로, 1개 이상의 @Bean 메소드를 제공하는 클래스의 경우, @Configuration을 달아주어야만 싱글톤이 보장된다는 특징이 있다. 이에 대해 더 자세히 알아보자.


앞서 언급하였듯이 @Configuration 내부에는 @Comopnent가 들어있다.

이 이야기를 듣는다면, 어쩌면 왜 @Component + @Bean가 아닌 @Configuration + @Bean을 써야만 하는지에 대한 의문이 생길 수도 있다.

 

물론 @Component+@Bean이라는 조합으로도 빈 등록은 가능하지만, 이전에 말했듯이 빈의 싱글톤 보장이 되지 않는다는 문제가 발생한다.

 

A, B 객체를 빈으로 등록하는 간단한 예시를 통해 이유를 살펴보자.

아래의 예시에서는 따로 의존성을 설정해주지 않았기 때문에 A와 B 중 어느 것이 먼저 등록될 지 알 수 없다.

@Component
public class BeanTestConfig {
    @Bean
    public A a() {
        return new A(b());
    }

    @Bean
    public B b() {
        return new B();
    }
}

 

먼저, @Component를 사용한 경우이다.

여기서 A가 먼저 등록되는 경우를 생각해보자.

 

A의 생성자는 B 객체를 필요로 하기 때문에, A 내부의 b()가 호출되면서 객체 B도 빈으로 등록된다. 그러나 이후 B 등록을 위해 b() 메소드가 다시 호출되고, 이에 따라 객체 B의 빈이 추가적으로 하나 더 등록되는 문제가 생긴다.

 

@Configuration
public class BeanTestConfig {
    @Bean
    public A a() {
        return new A(b());
    }

    @Bean
    public B b() {
        return new B();
    }
}

 

다음으로 @Configuration을 살펴보자.

 

@Configuration 어노테이션을 사용하는 경우에는 @Component를 사용하는 경우와 달리, @Configuration이 있는 클래스를 객체로 생성할 때 CGLib 라이브러리를 사용해 프록시 패턴이 적용된다.

 

아래는 @Component와 @Configuration 어노테이션 내부 코드들이다.

@Component
@Configuration

 

@Component와 달리 여러 값들이 추가적으로 들어있는 모습을 확인할 수 있다.

 

여기서 proxyBeanMethods값이 true라면, @Bean 메소드 내부에서 @Bean메소드를 호출하는 경우 새로운 객체를 생성하지 않고, 이미 등록된 빈을 찾아서 반환해주도록 강제하게 된다.

 

즉, 위의 예시에서 a()가 먼저 호출되더라도, b()를 먼저 호출해 컨테이너에 새로운 빈을등록하고, A에서는 해당 빈을 받아와 사용하도록 함으로써 싱글톤을 보장해 주는 것이다.

 

+) @Configuration(proxyBeanMethods = false)

만약 원한다면 위와 같이 해당 설정을 조작하여 빈을 매번 생성하도록 만드는 것도 가능하다.

그러나 공식문서에서도 이는 @Configuration의 장점을 살리지 못하는 행위라며 지양하도록 권장한다.


 

참고자료

'Spring' 카테고리의 다른 글

[Spring] AOP (Aspect Oriented Programming)란? (+ 구현 예제)  (3) 2025.03.21