티스토리 뷰
[Spring] SpringBoot 소스 코드 분석하기, 애플리케이션 컨텍스트(Application Context)와 빈팩토리(BeanFactory) - (2)
망나니개발자 2022. 2. 8. 10:00이번에는 SpringBoot의 실행 과정을 소스 코드로 직접 살펴보려고 합니다. 해당 내용을 이해하려면 먼저 애플리케이션 컨텍스트(Application Context)에 대해 알아야 합니다. 그래서 이번에는 애플리케이션 컨텍스트(Application Context)를 살펴보고자 합니다.
아래의 내용은 SpringBoot 2.6.3를 기준으로 작성되었습니다.
1. 애플리케이션 컨텍스트(Application Context)
[ 다양한 종류의 애플리케이션 컨텍스트(Application Context) ]
애플리케이션 컨텍스트는 빈들의 생성과 의존성 주입 등의 역할을 하는 일종의 DI 컨테이너이다. 우리가 직접 애플리케이션 컨텍스트를 생성할 수도 있지만 SpringBoot를 이용한다면 애플리케이션 종류에 따라 각기 다른 종류의 ApplicationContext가 내부에서 만들어진다.
(실제로 애플리케이션 컨텍스트를 생성하는 코드는 이어지는 포스팅들에서 볼 수 있다)
- 웹 애플리케이션이 아닌 경우
- 애플리케이션 컨텍스트: AnnotationConfigApplicationContext
- 웹서버: X
- 서블릿 기반의 웹 애플리케이션인 경우
- 애플리케이션 컨텍스트: AnnotationConfigServletWebServerApplicationContext
- 웹서버: Tomcat
- 리액티브 웹 애플리케이션인 경우
- 애플리케이션 컨텍스트: AnnotationConfigReactiveWebServerApplicationContext
- 웹서버: Reactor Netty
일반적인 ApplicationContext 관련 클래스들은 spring-context 프로젝트에 존재하며, spring-core나 spring-bean 또는 spring-aop를 추가하면 같이 불러와진다. 하지만 AnnotationConfigServletWebServerApplicationContext나 AnnotationConfigReactiveWebServerApplicationContext는 Springboot에서 추가된 클래스이므로 spring-boot-starter-web 또는 spring-boot-starter-webflux 같은 spring-boot 의존성을 추가해주어야 한다.
또한 기존의 Spring 프로젝트들과 달리 SpringBoot는 내장 웹서버를 가지고 있다. 그래서 타입에 맞는 웹서버를 만들고 애플리케이션 실행과 함께 내장 웹서버가 시작된다.
[ DI 컨테이너와 애플리케이션 컨텍스트(Application Context) ]
애플리케이션 컨텍스트는 이름 그대로 애플리케이션을 실행하기 위한 환경이다. 그럼에도 불구하고 애플리케이션 컨텍스트가 DI 컨테이너라고도 불리며 그러한 역할을 할 수 있는 이유는 ApplicationContext 상위에 빈들을 생성하는 BeanFactory 인터페이스를 부모로 상속받고 있기 때문이다.
BeanFactory는 애플리케이션 컨텍스트의 최상위 인터페이스 중 하나이며, 다음과 같이 1개의 빈을 찾기 위한 메소드들을 갖고 있다.
public interface BeanFactory {
String FACTORY_BEAN_PREFIX = "&";
Object getBean(String name) throws BeansException;
<T> T getBean(String name, Class<T> requiredType) throws BeansException;
Object getBean(String name, Object... args) throws BeansException;
<T> T getBean(Class<T> requiredType) throws BeansException;
<T> T getBean(Class<T> requiredType, Object... args) throws BeansException;
<T> ObjectProvider<T> getBeanProvider(Class<T> requiredType);
<T> ObjectProvider<T> getBeanProvider(ResolvableType requiredType);
boolean containsBean(String name);
boolean isSingleton(String name) throws NoSuchBeanDefinitionException;
boolean isPrototype(String name) throws NoSuchBeanDefinitionException;
boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException;
boolean isTypeMatch(String name, Class<?> typeToMatch) throws NoSuchBeanDefinitionException;
@Nullable
Class<?> getType(String name) throws NoSuchBeanDefinitionException;
@Nullable
Class<?> getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException;
String[] getAliases(String name);
}
하지만 스프링은 동일한 타입의 빈이 여러 개 존재할 때에도 List로 빈을 찾아서 주입해준다. 스프링이 List를 처리할 수 있는 이유는 위의 그림을 조금 자세히 살펴다보면 파악할 수 있다.
해당 그림을 조금 구체적으로 들여다보면 ApplicationContext가 BeanFactory를 바로 상속받는 것이 아니라 BeanFactory의 자식 인터페이스인 ListableBeanFactory와 HierarchicalBeanFactory를 통해 상속받는 것을 확인할 수 있다.
최상위 BeanFactory는 단일 빈을 처리하기 위한 퍼블릭 인터페이스만을 갖고 있는 반면 ListableBeanFactory는 빈 리스트를 처리하기 위한 퍼블릭 인터페이스를 갖는다. 또한 HierarchicalBeanFactory는 여러 BeanFactory들 간의 계층(부모-자식) 관계를 설정하기 위한 퍼블릭 인터페이스를 갖는다. 이를 통해 애플리케이션 컨텍스트는 단일 빈 외에도 다양하게 처리할 수 있는 것이다.
위의 그림에는 나와있지 않지만 ListableBeanFactory와 HierarchicalBeanFactory 외에도 @Autowired 처리를 위한 AutowireCapableBeanFactory 등도 있다. 하지만 애플리케이션 컨텍스트는 AutowireCapableBeanFactory를 상속받지 않는데, 그럼에도 불구하고 @Autowired를 처리할 수 있는 이유는 ApplicationContext의 퍼블릭 인터페이스를 보면 알 수 있다.
public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory,
HierarchicalBeanFactory, MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
@Nullable
String getId();
String getApplicationName();
String getDisplayName();
long getStartupDate();
@Nullable
ApplicationContext getParent();
AutowireCapableBeanFactory getAutowireCapableBeanFactory() throws IllegalStateException;
}
애플리케이션 컨텍스트는 @Autowired를 처리하기 위한 AutowireCapableBeanFactory를 합성(Has-A) 관계로 가지고 있기 때문이다. 그래서 ApplicationContext 코드를 보면 AutowireCapableBeanFactory를 반환하는 퍼블릭 인터페이스를 가지고 있는 것이다.
우리는 이제 Spring의 애플리케이션 컨텍스트가 BeanFactory, ListableBeanFactory, HierarchicalBeanFactory를 상속받고 있으므로 애플리케이션 컨텍스트를 통해 빈을 찾을 수 있다는 것을 알게 되었다.
하지만 그렇다고 스프링의 빈들이 진짜 애플리케이션 컨텍스트에서 관리되는 것은 아니다. ApplicationContext 하위에는 다양한 구현체들이 존재하며, 일반적으로 스프링부트가 만들어내는 3가지 애플리케이션 컨텍스트는 모두 GenericApplicationContext라는 애플리케이션 컨텍스트를 부모로 가지고 있다.
해당 클래스의 생성자를 살펴보면 다음과 같은데, 내부에서 진짜 빈들을 등록하여 관리하고 찾아주는 DefaultListableBeanFactory를 생성하고 있음을 확인할 수 있다.
public class GenericApplicationContext extends AbstractApplicationContext
implements BeanDefinitionRegistry {
private final DefaultListableBeanFactory beanFactory;
@Nullable
private ResourceLoader resourceLoader;
private boolean customClassLoader = false;
private final AtomicBoolean refreshed = new AtomicBoolean();
public GenericApplicationContext() {
this.beanFactory = new DefaultListableBeanFactory();
}
public GenericApplicationContext(DefaultListableBeanFactory beanFactory) {
Assert.notNull(beanFactory, "BeanFactory must not be null");
this.beanFactory = beanFactory;
}
public GenericApplicationContext(@Nullable ApplicationContext parent) {
this();
setParent(parent);
}
public GenericApplicationContext(DefaultListableBeanFactory beanFactory, ApplicationContext parent) {
this(beanFactory);
setParent(parent);
}
...
}
즉, 애플리케이션 컨텍스트는 빈들을 관리하는 BeanFactory 구현체인 DefaultListableBeanFactory를 합성(Has-A) 관계로 내부에 가지고 있고, 애플리케이션 컨텍스트에 빈을 등록하거나 찾아달라는 빈 처리 요청이 오면 benaFactory로 이러한 요청을 위임하여 처리하는 것이다.
또한 우리는 애플리케이션 컨텍스트가 @Autowired를 처리해주는 빈 팩토리를 반환하는 getAutowireCapableBeanFactory를 퍼블릭 인터페이스로 가지고 있음을 확인하였는데, 해당 메소드를 호출하면 반환되는 빈 팩토리가 바로 DefaultListableBeanFactory이다.
위의 그림을 통해 살펴볼 수 있듯이 DefaultListableBeanFactory는 상위에 @Autowired 처리를 위한 인터페이스인 AutowireCapableBeanFactory와 그에 대한 추상클래스 구현체인 AbstractAutowireCapableBeanFactory를 상속받고 있음을 살펴볼 수 있다. 그래서 애플리케이션 컨텍스트로 getAutowireCapableBeanFactory를 요청하면 AutowireCapableBeanFactory 타입으로 추상화된 DefaultListableBeanFactory 구현체 객체를 반환받게 된다.
[ ConfigurableApplicationContext ]
ConfigurableApplicationContext는 거의 모든 애플리케이션 컨텍스트가 갖는 공통 애플리케이션 컨텍스트 인터페이스로써 ApplicationContext, Lifecycle, Closable 인터페이스를 상속받는다. 애플리케이션 컨텍스트가 시작되고 종료될 때 사용되는 메소드들을 가지며, 가장 처음 살펴보았던 3가지 애플리케이션 컨텍스트 모두 ConfigurableApplicationContext 인터페이스를 직접 구현하고 있다. 이러한 이유로 스프링 부트 애플리케이션을 실행하는 run 메소드를 호출하면 받는 반환 타입 역시 ConfigurableApplicationContext이다.
한 가지 흥미로운 점은 ConfigurableApplicationContext가 Closable 인터페이스를 상속받고 있다는 것이다. 그래서 우리가 애플리케이션을 실행하고 종료하고 반복해야 하는 경우에 try-with-resources를 사용하면 작업을 조금 편리하게 만들 수 있다.
@SpringBootApplication(proxyBeanMethods = false)
public class TestingApplication {
public static void main(String[] args) {
try (ConfigurableApplicationContext ctx = SpringApplication.run(TestingApplication.class,args)) {
} catch (Exception e) {
}
}
}
위와 같이 코드를 수정하면, run 호출 후에 try-with-resources에 의해 자동으로 close가 호출된다. 그래서 매번 끄고 재시작하지 않고 1번 실행 후에 자동 종료되도록 할 수 있다. 실제 close 메소드는 AbstractApplicationContext에 다음과 같이 구현되어 있다.
아래에 있는 shutdownHook과 관련해서는 이어지는 포스팅에서 살펴볼 예정이다.
@Override
public void close() {
synchronized (this.startupShutdownMonitor) {
doClose();
// If we registered a JVM shutdown hook, we don't need it anymore now:
// We've already explicitly closed the context.
if (this.shutdownHook != null) {
try {
Runtime.getRuntime().removeShutdownHook(this.shutdownHook);
}
catch (IllegalStateException ex) {
// ignore - VM is already shutting down
}
}
}
}
[ 잘 만들어진 Spring 코드들 ]
Spring은 Java와 Kotlin 같은 객체지향 언어를 이용해 객체지향적으로 프로그래밍을 할 수 있도록 도와주는 프레임워크이다. 그렇기 때문에 Spring 자체도 상당히 객체지향적으로 설계되어 있고 개발되어 있는데, 추상화된 인터페이스들만을 보아도 이를 느낄 수 있다.
우리가 지금까지 살펴본 내용은 그렇게 많지 않다. 그럼에도 불구하고 단일 빈을 처리하기 위한 BeanFactory, 여러 빈을 처리하기 위한 ListableBeanFactory, 계층 관계를 처리하기 위한 HierarchicalBeanFactory를 분리하여 인터페이스를 나누고 있는 것만 봐도 Spring 개발팀(Pivotal)이 객체지향의 원칙에 많은 노력을 들이고 있는지 파악할 수 있었다.
그 외에도 이어지는 포스팅들을 읽다 보면 상당히 직관적인 메소드 이름을 사용하고, 복잡한 로직들은 적절히 프라이빗 메소드로 세부 구현을 감추어 가독성을 높이며, 적절한 위치에 잘 맞는 디자인 패턴을 적용하는 등을 확인할 수 있을 것이다.
단순히 Spring에 대해 많이 알아가는 것도 의미가 있지만, 동시에 이러한 세계적인 오픈 소스를 통해 좋은 코드 작성에 대해 배워간다면 더욱 좋을 것이다. 이어지는 포스팅들에서도 이러한 부분들을 적절히 다루고자 한다.
위의 내용들은 개인적으로 공부를 하면서 작성을 한 내용이라 충분히 틀리거나 잘못된 내용들이 있을 수 있습니다. 혹시 수정 또는 추가할 내용들을 발견하셨다면 댓글 남겨주세요! 반영해서 수정하도록 하겠습니다:)
관련 포스팅
- SpringBoot 소스 코드 분석하기, SpringBoot의 장점과 특징 - (1)
- SpringBoot 소스 코드 분석하기, 애플리케이션 컨텍스트(Application Context)와 빈팩토리(BeanFactory) - (2)
- SpringBoot 소스 코드 분석하기, @SpringBootApplication 어노테이션의 속성과 구성요소 - (3)
- SpringBoot 소스 코드 분석하기, SpringBootApplication의 생성과 초기화 과정 - (4)
- SpringBoot 소스 코드 분석하기, SpringBootApplication의 run 메소드와 실행과정 - (5)
- SpringBoot 소스 코드 분석하기, 애플리케이션 컨텍스트(ApplicationContext)의 refreshContext 동작 과정 - (6)
- SpringBoot 소스 코드 분석하기, DispatcherServlet(디스패처 서블릿) 동작 과정 - (7)