Spring IoC 컨테이너, 컴포넌트 스캔, 빈 생명주기

요약 (다음 질문의 정답을 안다면 이 포스트를 읽지 않아도 된다.)

  1. IoC 컨테이너와 ApplicationContext는 완전히 같은 개념인가?
  2. IoC 컨테이너를 구성하는 방법은 어떤 것이 있는가?
  3. BeanFactory와 ApplicationContext의 차이를 알고 있는가?

IoC 컨테이너

스프링의 IoC 컨테이너는 객체를 인스턴스화하고 구성 및 조합하고 수명주기를 관리하는 역할을 한다.

스프링이 제공하는 IoC 컨테이너

스프링의 IoC 컨테이너는 두 가지 유형의 컨테이너를 제공한다.

  1. BeanFactory 기반 컨테이너
  2. ApplicationContext 기반 컨테이너

우리가 가장 흔히 아는 ApplicationContext는 IoC 컨테이너 중 하나이다.

BeanFactory는 IoC 컨테이너의 가장 기본적인 버전이다.
ApplicationContext는 BeanFactory의 기능을 확장한 버전이다.

BeanFactory

BeanFactory는 IoC 컨테이너의 가장 기본적인 버전이다.
BeanFactory는 메타데이터를 기반으로 빈 객체를 생성하고 구성한다.

이때 BeanFactory는 XML 기반으로 메타데이터를 사용한다.
BeanFactory는 Lazy Loadaing으로 빈 객체를 등록한다.

코드로 이해하기

User라는 POJO 클래스가 있다.

1
2
3
4
5
6
7
public class User {
public static boolean IS_BEAN_INITIALIZED = false;

public void setIsBeanInitialized() {
IS_BEAN_INITIALIZED = true;
}
}

이 클래스를 XML을 통해 Bean 등록해보자.

1
2
3
4
5
6
7
<?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="user" class="nextstep.helloworld.core.User" init-method="setIsBeanInitialized"></bean>
</beans>

config.xml에 user라는 이름으로 빈 등록을 해줬다.
init-method를 통해 빈 객체가 생성될 때 User#setIsBeanInitialized를 실행하도록 했다.

만약 User가 빈 객체로 생성되면 IS_BEAN_INITIALIZED가 true가 될 것 이다.

테스트 코드로 확인해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ContainerTest {

@Test
@DisplayName("XML 기반으로 BeanFactory 레이지 로딩 확인하기")
void beanFactoryLazyLoading() {

// given
Resource res = new ClassPathResource("config.xml");
BeanFactory factory = new XmlBeanFactory(res);

// when
boolean isCreated = User.IS_BEAN_INITIALIZED;
User user = (User) factory.getBean("user");
boolean isCreatedAfterGetBean = User.IS_BEAN_INITIALIZED;

// then
assertThat(isCreated).isFalse();
assertThat(isCreatedAfterGetBean).isTrue();
}
}

여기서 Lazy Loading을 확인할 수 있다.
Lazy Loading은 BeanFactory에서 빈 객체를 가져올 때 해당 객체를 생성한다는 의미다.

그래서 User.IS_BEAN_INITIALIZED가 빈 객체를 가져올 때 true가 됨을 확인 할 수 있다.

ApplicationContext

ApplicationContext는 BeanFactory의 하위 인터페이스다.
따라서 BeanFactory의 모든 기능을 제공한다.

다만 ApplicationContext는 웹 어플리케이션, AOP에 필요한 더 많은 기능을 제공한다.

ApplicationContext의 선언부를 통해 어떤 기능을 추가로 제공하는 지 보자.

1
2
3
public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory, MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
//...생략
}
  1. EnvironmentCapable 인터페이스 : 프로파일과 프로퍼티를 다루는 Environment를 접근할 수 있게 한다.
  2. ListableBeanFactory 인터페이스 : BeanFactory의 기능을 지원하기 위한 인터페이스. 빈 객체를 생성 관리한다.
  3. HierarachicalBeanFactory 인터페이스 : BeanFactory 구현체 사이의 계층 구조를 확인할 수 있게 한다.
  4. MessageSource 인터페이스 : 국제화(i18n)을 제공하는 인터페이스
  5. ApplicationEventPublisher 인터페이스 : 이벤트를 발생시키는 기능을 제공한다.
  6. ResourcePatternResolver 인터페이스 : 리소스를 읽어오는 기능을 제공한다.

ApplicationContext의 메타데이터 설정 방법들

BeanFactory와 달리 ApplicationContext의 중요한 기능 중 하나는 메타데이터를 다양한 방식으로 할 수 있다.

BeanFactory는 XML 외부 파일로 컨테이너를 설정해줘야 했다.
ApplicationContext는 어노테이션 기반 설정도 지원한다!

아까 사용했던 User POJO 클래스를 다시 사용하자.

1
2
3
4
5
6
7
public class User {
public static boolean IS_BEAN_INITIALIZED = false;

public void setIsBeanInitialized() {
IS_BEAN_INITIALIZED = true;
}
}

그리고 @Configuration 어노테이션을 붙인 설정 파일로 Bean 등록해보자.

1
2
3
4
5
6
7
8
9
10
@Configuration
public class ConfigurationUserBean {

@Bean
public User user() {
User user = new User();
user.setIsBeanInitialized();
return user;
}
}

@Bean 어노테이션이 붙은 메서드가 실행되면 반환되는 객체를 빈 객체로 컨테이너에 등록한다.
그리고 XML의 init-method 대신 메서드 안에 setIsBeanInitialized 메서드를 호출했다.

테스트로 확인해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
@DisplayName("어노테이션 기반(설정 파일)로 빈 객체 생성 확인하기")
void applicationContextBeanCreate() {

// given
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(ConfigurationUserBean.class);

// when
boolean isCreated = User.IS_BEAN_INITIALIZED;
User user = (User) applicationContext.getBean("user");
boolean isCreatedAfterGetBean = User.IS_BEAN_INITIALIZED;

// then
assertThat(isCreated).isTrue();
assertThat(isCreatedAfterGetBean).isTrue();
}

설정 클래스를 전달해서 ApplicationContext를 생성했다.
ApplicationContext는 eager-loading이라 컨테이너가 생성됐을 때 빈 객체를 생성한다!

그래서 빈 객체를 호출하기 전에도 이미 빈 객체가 생성되어 있다는 의미다!
(결국 모든 빈객체를 eager-loading하기 때문에 ApplicationContext는 비교적 무거운 IoC container이다.)

ComponentScan

ComponentScan은 특정 패키지에서 @Component가 붙은 클래스를 빈 객체 등록하는 방법이다.
일일히 빈 객체를 메서드로 작성하지 않아도 된다는 장점이 있다.

이전에 @Bean 어노테이션으로 등록했던 방식 대신 ComponentScan을 활용해보자.

먼저 빈등록하고 싶은 클래스에 @Component 어노테이션을 달아주자.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class User implements InitializingBean {
public static boolean IS_BEAN_INITIALIZED = false;

public void setIsBeanInitialized() {
IS_BEAN_INITIALIZED = true;
}

@Override
public void afterPropertiesSet() throws Exception {
setIsBeanInitialized();
}
}

InitializeBean 인터페이스로 해당 클래스가 빈 객체로 만들어지고 나서 setIsBeanInitialized()를 실행하도록 했다.

이제 설정 파일에 @ComponentScan 어노테이션을 붙여주자!

1
2
3
4
5
6
@Configuration
@ComponentScan(basePackages = {"nextstep.helloworld.core.componentscan.user"})
public class ConfigurationUserBean {

}

@ComponentScan은 특정 패키지를 기준으로 해당 패키지와 하위 패키지의 @Component 어노테이션이 붙은 클래스를 빈등록한다. (위 예시는 User클래스가 있는 패키지를 기준으로 잡았다.)

@Bean을 붙여서 빈객체를 생성해줬던 메서드를 제거할 수 있게됐다!

그렇다면 스프링 부트에서는 어떻게 ComponentScan을 하는 걸까??

스프링 부트를 사용하면 @Configuration을 붙인 설정파일 없이도 @Component를 붙인 클래스를 빈등록 해준다.
왜 그렇게 되는 건지 확인해보자.

1
2
3
4
5
6
@SpringBootApplication
public class HelloApplication {
public static void main(String[] args) {
SpringApplication.run(HelloApplication.class, args);
}
}

스프링 부트 프로젝트를 만들면 먼저 @SpringBootApplication이 붙은 클래스가 프로젝트 패키지에 생성된다.
@SpringBootApplication에 들어가보면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
//생략
}

즉 @SpringBootApplication 어노테이션에는 @ComponentScan 어노테이션이 포함되어 있다.
basePackages가 따로 설정하지 않았으니 해당 클래스가 속한 패키지를 기준으로 컴포넌트 스캔을 진행하게 된다.
그리고 @Filter 어노테이션으로 특정 클래스들을 컴포넌트 스캔에서 제외할 수 있다.

Bean 생명주기

Bean 객체의 생명 주기를 이해하기 위해서는 Bean을 생성하고 관리하는 스프링 컨테이너의 생명주기를 이해해야 한다.

스프링 컨테이너의 생명주기

  • 컨테이너 초기화
  • 컨테이너 종료
1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
@DisplayName("컨테이너 생명주기")
void applicationContextLifeCycle() {

// 초기화
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ConfigurationUserBean.class);

// 사용
User user = (User) applicationContext.getBean("user");

// 종료
applicationContext.close();
}

컨테이너 초기화

1
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ConfigurationUserBean.class);

AppliactionContext 객체를 생성하면 스프링 컨테이너를 초기화한다.
이 과정에서 메타데이터를 읽어서 알맞은 빈 객체를 생성하고 각 빈을 연결(주입)하게 된다.

컨테이너 종료

1
applicationContext.close();

컨테이너를 close 메서드를 통해 종료하면 컨테이너가 관리하던 빈 객체들도 모두 소멸된다.
(이때 프로퍼티 스코프인 객체들은 소멸되지 않는다. )

빈 객체의 생명 주기

스프링 컨테이너의 생명주기를 보면 짐작할 수 있듯이 일반적인 빈 객체는 컨테이너의 통제에 따라 생명 주기를 진행한다.

  • 객체 생성 -> ApplicationContext가 생성되면서 빈 객체 생성
  • 의존 설정 -> ApplicationContext가 메타데이터를 기반으로 의존 주입
  • 초기화 -> 의존관계가 모두 설정하고 나면 빈 객체의 초기화에 해당하는 메서드를 수행
  • 소멸 -> ApplicationContext를 종료하면 빈 객체 소멸에 해당하는 메서드를 수행

초기화와 소멸

의존관계까지 모두 주입되고 나면 빈 객체가 등록될 때 어떤 행위를 하도록 하고싶거나,
특정 빈 객체가 소멸 될 때 어떤 행위를 하기 원할 때가 있다.

이런 의도를 빈 생명 주기 중 초기화와 소멸에서 수행할 수 있다.

초기화

빈 객체가 생성되고 나서 어떤 메서드를 실행시키고 싶은 경우 초기화 단계에서 수행한다.
이를 설정하기 위해서는 여러가지 방법이 있다.

  1. @PostConstruct
  2. InitializingBean 인터페이스
  3. init-method

코드로 이해해보자.
User 클래스가 빈 객체로 등록될 때 특정 메서드를 실행하게 만들자.

먼저 @PostConstruct

1
2
3
4
5
6
7
8
9
@Component
public class User {
public static boolean IS_BEAN_INITIALIZED = false;

@PostConstruct
public void setIsBeanInitialized() {
IS_BEAN_INITIALIZED = true;
}
}

이렇게 빈 객체가 등록되고 나서 수행됐으면 하는 메서드에 @PostConstruct를 붙여주면 된다.

InitializingBean 인터페이스를 구현하는 방법도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class User implements InitializingBean {
public static boolean IS_BEAN_INITIALIZED = false;

public void setIsBeanInitialized() {
IS_BEAN_INITIALIZED = true;
}

@Override
public void afterPropertiesSet() throws Exception {
setIsBeanInitialized();
}
}

afterPropertiesSet() 메서드 안에 빈 객체가 등록될 때 수행할 행동을 적어놓으면 된다.

마지막으로 xml init-method를 사용하는 방법이다.

1
<bean id="user" class="nextstep.helloworld.core.User" init-method="setIsBeanInitialized">

Bean 태그 안에 init-method 속성에 빈 객체 생성 될 때 수행될 메서드 이름을 적어주면 된다.

소멸

빈 객체가 사라질 때 특정 행동을 하라고 설정할 수 있다.
소멸을 구현하는 데에도 여러 방법이 있다.

  1. @PreDestroy
  2. DisposableBean 인터페이스
  3. destroy-method

코드로 이해해보자.
User 클래스의 빈 객체가 폐기될 때 특정 메서드를 실행하게 만들자.

@PreDestory

1
2
3
4
5
6
7
8
9
@Component
public class User {
public static boolean IS_BEAN_INITIALIZED = false;

@PreDestroy
public void setIsBeanInitialized() {
IS_BEAN_INITIALIZED = true;
}
}

DisposableBean 인터페이스

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class User implements DisposableBean {
public static boolean IS_BEAN_INITIALIZED = false;

public void setIsBeanInitialized() {
IS_BEAN_INITIALIZED = true;
}

@Override
public void destroy() throws Exception {
IS_BEAN_INITIALIZED = false;
}
}

XML destroy-method

1
<bean id="user" class="nextstep.helloworld.core.componentscan.user.User" destroy-method="setIsBeanInitialized"></bean>

이렇게 하면 빈 객체가 제거 될 때(컨테이너가 종료될 때) 해당 메서드를 실행시킬 수 있다.

Share