개발 이야기/Spring

[Spring] 스프링 컨테이너와 빈이란?

제이온 (J.ON) 2021. 6. 16.

안녕하세요? 제이온입니다.

 

오늘은 스프링 컨테이너와 빈에 대해서 알아 보겠습니다.

 

 

스프링 컨테이너란?

스프링 컨테이너는 자바 객체의 생명 주기를 관리하며, 생성된 자바 객체들에게 추가적인 기능을 제공하는 역할을 합니다. 여기서 말하는 자바 객체를 스프링에서는 빈(Bean)이라고 부릅니다. 그리고 저번 시간에 배웠던 IoC와 DI의 원리가 이 스프링 컨테이너에 적용됩니다.

 

개발자는 new 연산자, 인터페이스 호출, 팩토리 호출 방식으로 객체를 생성하고 소멸시킬 수 있는데, 스프링 컨테이너가 이 역할을 대신해 줍니다. 즉, 제어 흐름을 외부에서 관리하는 것이죠. 또한, 객체들 간의 의존 관계를 스프링 컨테이너가 런타임 과정에서 알아서 만들어 줍니다. DI는 이전 포스팅에서 말씀드렸듯이, 생성자, setter, @Autowired를 통해 적용합니다.

 

 

스프링 컨테이너의 종류

스프링 컨테이너는 BeanFactory와 ApplicationContext가 있습니다.

 

 

(1) BeanFactory

BeanFactory는 빈을 등록하고 생성하고 조회하고 돌려주는 등 빈을 관리하는 역할을 합니다. getBean() 메소드를 통해 빈을 인스턴스화할 수 있습니다.

 

먼저, 저번 시간에 만들었던 AppConfig에 다음과 같은 코드를 추가합니다.

 

 

@Configuration
public class AppConfig {

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(discountPolicy());
    }

    @Bean
    public FixDiscountPolicy discountPolicy() {
        return new FixDiscountPolicy();
    }
}

 

 

해당 코드에서 @Bean이 붙은 메소드의 명을 스프링 빈의 이름으로 사용하여 빈 등록을 합니다.

 

 

 

 

이런 식으로 스프링 컨테이너 안에 스프링 빈 저장소가 있고, 그 안에 빈이 들어있다고 생각하시면 됩니다.

 

 

public class Main {

    public static void main(String[] args) {
        final BeanFactory beanFactory = new AnnotationConfigApplicationContext(AppConfig.class);
        final OrderService orderService = beanFactory.getBean("orderService", OrderService.class);
        final Order order = orderService.createOrder(15, "샤프", 3000);
        System.out.println(order.getDiscountPrice());
    }
}

 

 

그리고 위와 같이 BeanFactory를 AnnotationConfigApplicationContext로 정의하되, AppConfig를 구성 정보로 지정합니다. 기존에는 개발자가 직접 AppConfig를 사용해서 필요한 객체를 직접 조회했지만, 이제부터는 스프링 컨테이너를 통해서 필요한 스프링 빈 객체를 찾을 수 있습니다.

 

getBean() 메소드에는 여러 가지 인자가 들어갈 수 있는데 이번 예제에서는 빈의 이름과 타입을 지정하여 빈 객체를 구현해 보겠습니다. 만약 OrderService.class를 적어주지 않는다면 orderService의 타입은 OrderService가 아니라 Object가 된다는 것을 유의하셔야 합니다.

 

 

.context.annotation.AnnotationConfigApplicationContext - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@4e718207
13:52:20.460 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalConfigurationAnnotationProcessor'
13:52:20.738 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerProcessor'
13:52:20.741 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerFactory'
13:52:20.743 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalAutowiredAnnotationProcessor'
13:52:20.745 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalCommonAnnotationProcessor'
13:52:20.758 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'appConfig'
13:52:20.767 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'orderService'
13:52:20.793 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'discountPolicy'
1000

 

 

그리고 실행하면 위와 같이 로그가 쫙 뜹니다. 중요한 것은 'Creating shared instance of singleton bean' 부분입니다. 뭔가 긴 빈 이름은 미리 정의된 초기 빈들이고 appConfig, orderService, discountPolicy가 우리가 등록한 빈이라는 사실을 알 수 있습니다. 또한, OrderService는 DiscountPolicy를 주입해 주어야 하는데 스프링 컨테이너가 알아서 주입을 해 줍니다.

 

 

(2) ApplicationContext

ApplicationContext도 BeanFactory처럼 빈을 관리할 수 있습니다. Main 코드에서 BeanFactory를 ApplicationContext로만 바꾸고 실행해 보겠습니다.

 

 

public class Main {

    public static void main(String[] args) {
        final ApplicationContext beanFactory = new AnnotationConfigApplicationContext(AppConfig.class);
        final OrderService orderService = beanFactory.getBean("orderService", OrderService.class);
        final Order order = orderService.createOrder(15, "샤프", 3000);
        System.out.println(order.getDiscountPrice());
    }
}

 

 

13:56:08.105 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@4e718207
13:56:08.136 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalConfigurationAnnotationProcessor'
13:56:08.391 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerProcessor'
13:56:08.396 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerFactory'
13:56:08.398 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalAutowiredAnnotationProcessor'
13:56:08.401 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalCommonAnnotationProcessor'
13:56:08.417 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'appConfig'
13:56:08.431 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'orderService'
13:56:08.458 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'discountPolicy'
1000

 

 

동일한 결과를 얻어온다는 사실을 알 수 있습니다.

 

 

(3) BeanFactory vs ApplicationContext

위 예제로만 보면 둘이 하는 역할이 같은데, 왜 두 개로 구분한 것인지 이해가 안 가실 수 있습니다. 이것은 ApplicationContext가 BeanFactory의 상속을 받았기 때문입니다.

 

 

public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
		MessageSource, ApplicationEventPublisher, ResourcePatternResolver {

	...
}

 

 

완전 정확히 BeanFactory 자체에 상속을 받은 것은 아니지만 빈을 관리하는 기능을 물려받은 것은 맞습니다. 그리고 ApplicationContext는 그 외에도 국제화가 지원되는 텍스트 메시지 관리, 이미지같은 파일 자원을 로드, 리너스로 등록된 빈에게 이벤트 발생 알림 등 부가적인 기능을 갖고 있습니다. 그래서 스프링 컨테이너하면 주로 이 ApplicationContext를 뜻합니다.

 

또한, 빈 관리라는 기능도 두 컨테이너가 동일한 기능을 하지는 않습니다. BeanFactory는 처음으로 getBean() 메소드가 호출된 시점에서야 해당 빈을 생성하고, ApplicationContext는 Context 초기화 시점에 모든 싱글톤 빈을 미리 로드한 후 애플리케이션 가동 후에는 빈을 지연 없이 받을 수 있습니다.

 

부가 기능과 빈을 지연 없이 얻을 수 있다는 장점으로 ApplicationContext을 실제 개발에서 주로 사용합니다.

 

 

싱글톤 컨테이너

스프링 컨테이너는 객체의 인스턴스를 싱글톤으로 관리하므로 싱글톤 컨테이너라고도 불립니다. 그래서 AppConfig 클래스를 읽어 들여서 만든 빈 목록에 있는 임의의 빈을 여러 번 불러도, 그 빈들의 주소는 같습니다. 실제로 orderService 빈 2개를 꺼내서 출력을 해 보면서 아래와 같습니다.

 

 

 

 

그런데 AppConfig 코드를 보면 한 가지 의문이 있습니다.

 

 

@Configuration
public class AppConfig {

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(discountPolicy());
    }

    @Bean
    public FixDiscountPolicy discountPolicy() {
        return new FixDiscountPolicy();
    }
}

 

 

orderService와 discountPolicy를 등록하는 건 이해가 가는데, orderService() 내의 코드를 보면 discountPolicy() 메소드를 호출하는 것을 알 수 있습니다. 즉, 자바 코드로만 보면 discountPolicy()를 2번 호출하여 싱글톤이 깨지는 것처럼 보입니다. 하지만 실제로는 싱글톤을 보장합니다. 왜 그럴까요?

 

 

바로 @Configuration 어노테이션이 있기 때문입니다. 일단 스프링 컨테이너의 AppConfig를 빈으로 꺼내오고 getClass() 메소드를 호출하여 출력해 보겠습니다.

 

 

public class Main {

    public static void main(String[] args) {
        final ApplicationContext beanFactory = new AnnotationConfigApplicationContext(AppConfig.class);
        final AppConfig bean = beanFactory.getBean("appConfig", AppConfig.class);
        System.out.println(bean.getClass());
    }
}

// 실행 결과
class blog.AppConfig$$EnhancerBySpringCGLIB$$fbbaa5ca

 

 

실행 결과가 뭔가 이상합니다. 원래는 'class blog.AppConfig'까지만 나와야하는데 Enhancer~ 이라는 낯선 명령어가 나오는 것을 알 수 있습니다. 이것은 우리가 만든 AppConfig가 아니라 CGLIB라는 바이트코드 조작 라이브러리를 이용해서 AppConfig 클래스를 상속받은 임의의 클래스를 만들고, 그 임의의 클래스를 스프링 빈으로 등록한 것입니다.

 

그리고 CGLIB 라이브러리를 이용한 클래스는 내부적으로 @Bean이 붙은 메소드마다 이미 스프링 빈이 존재하는지 확인하고 있다면 존재하는 빈을 반환하는 형식으로 역할을 합니다. 이로 인해 싱글톤을 보장하는 것이죠. 참고로, 특정 객체를 빈으로 등록하는 순간 그 객체의 자식 객체들도 연쇄적으로 모두 빈에 등록되므로 AppConfig의 자식 객체인 AppConfig (ver. CGLIB) 객체도 같이 등록됩니다.

 

만약, @Configuration을 적용하지 않고, @Bean만 적용한다면 싱글톤을 보장하지 못합니다.

 

 

정리

스프링 컨테이너는 IoC와 DI의 원리를 이용하였고, 싱글톤을 유지한다는 것을 배웠습니다. 다음 시간에는 컴포넌트 스캔과 의존 관계 자동 주입에 대해서 더 자세히 설명하겠습니다.

 

 

출처

 

[Spring] 스프링 컨테이너의 두 종류

스프링 컨테이너의 두 종류 컨테이너 안의 빈(Bean) 컨 테이너는 스프링 프레임워크의 핵심이다. 스프링 컨테이너는 제어 역행(IoC)을 사용해 애플리케이션을 구성하는 컴포넌트들을 관리한다. 여

flyburi.com

 

 

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., 스프링 핵심 원리를 이해하고, 성장하는 개발자가 되어보세요! 📣 확인해주

www.inflearn.com

 

추천 글