평소 새로운 프로젝트에 투입되거나 여러 프로젝트를 동시에 진행할 때, 도메인 구조를 빠르게 파악할 수 있도록 도와주는 프로그램이 있으면 좋겠다고 생각했습니다.
제가 주력으로 사용하는 Spring Framework는 Bean 간의 관계를 잘 분석하면 도메인 구조를 다이어그램으로 시각화하는 것이 충분히
가능하다고 판단했고, 이를 자동화하는 도구를 개발하게 되었습니다.
우선 SpringFramework의 동작 과정에 대해서 학습이 필요했습니다.
Spring Boot가 실행될 때 내부적으로 어떤 과정이 발생하는가?
SpringApplication.run(...) 실행
- Spring Boot 애플리케이션을 시작하는 가장 기본적인 엔트리 포인트로, 내부적으로 SpringApplication 객체를 생성하고 실행함.
- 이 과정에서 가장 먼저 SpringApplicationContext(ApplicationContext)를 생성하고, Spring Bean 객체들을 초기화하게 됨.
@SpringBootApplication
public class DiagramOpenSourceApplication {
public static void main(String[] args) {
SpringApplication.run(DiagramOpenSourceApplication.class, args);
}
}
public ConfigurableApplicationContext run(String... args) {
Startup startup = SpringApplication.Startup.create();
if (this.properties.isRegisterShutdownHook()) {
shutdownHook.enableShutdownHookAddition();
}
DefaultBootstrapContext bootstrapContext = this.createBootstrapContext();
ConfigurableApplicationContext context = null;
this.configureHeadlessProperty();
SpringApplicationRunListeners listeners = this.getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);
Banner printedBanner = this.printBanner(environment);
context = this.createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
this.refreshContext(context);
this.afterRefresh(context, applicationArguments);
startup.started();
if (this.properties.isLogStartupInfo()) {
(new StartupInfoLogger(this.mainApplicationClass, environment)).logStarted(this.getApplicationLog(), startup);
}
listeners.started(context, startup.timeTakenToStarted());
this.callRunners(context, applicationArguments);
} catch (Throwable ex) {
throw this.handleRunFailure(context, ex, listeners);
}
try {
if (context.isRunning()) {
listeners.ready(context, startup.ready());
}
return context;
} catch (Throwable ex) {
throw this.handleRunFailure(context, ex, (SpringApplicationRunListeners)null);
}
}
1. Startup 객체 생성
Startup startup = SpringApplication.Startup.create();
- Spring Boot 3.x에서 추가된 기능으로, 애플리케이션 실행 과정에서 성능 측정을 위한 Startup 객체를 생성합니다.
- 실행 시간을 추적하는 역할을 합니다.
2. Shutdown Hook 설정
if (this.properties.isRegisterShutdownHook()) {
shutdownHook.enableShutdownHookAddition();
}
- isRegisterShutdownHook() 설정이 true일 경우, JVM 종료 시 애플리케이션을 정상적으로 종료하기 위한 Shutdown Hook을 등록합니다.
- Spring Boot는 애플리케이션 종료 시 리소스를 안전하게 정리하기 위해 이 기능을 제공합니다.
3. 부트스트랩 컨텍스트 생성
DefaultBootstrapContext bootstrapContext = this.createBootstrapContext();
- Bootstrap Context는 ApplicationContext가 생성되기 전에 필요한 리소스나 설정값을 로드하는 역할을 합니다.
- 초기 설정을 담당하는 객체입니다.
4. ApplicationContext 설정
ConfigurableApplicationContext context = null; this.configureHeadlessProperty();
- context 변수는 나중에 ApplicationContext를 저장할 변수입니다.
- configureHeadlessProperty() 메서드는 UI가 없는 서버 환경에서 실행할 때 java.awt.headless=true 설정을 적용하여 GUI 관련 기능을 비활성화합니다.
5. Spring Boot 실행 리스너 초기화 및 실행 시작
SpringApplicationRunListeners listeners = this.getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
- SpringApplicationRunListeners 객체를 생성하여 리스너를 가져옵니다.
- 실행을 시작할 때 starting()을 호출하여 애플리케이션의 실행을 알립니다.
- 보통 ApplicationListener<ApplicationStartingEvent> 등을 통해 외부에서 실행 이벤트를 받을 수 있습니다.
6. 애플리케이션 환경(Environment) 설정
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);
- DefaultApplicationArguments(args) : 커맨드 라인 인수를 ApplicationArguments로 변환합니다.
- prepareEnvironment() : Environment 객체를 생성하고, 프로퍼티 값을 설정합니다.
- application.properties 또는 application.yml의 내용을 로드합니다.
- 커맨드 라인 인수를 반영합니다.
- listeners를 통해 환경 변화를 감지하고 EnvironmentPreparedEvent를 발생시킵니다.
7. 배너 출력
Banner printedBanner = this.printBanner(environment);
- spring.banner.location에 지정된 경로에서 배너 파일을 찾고 콘솔에 출력합니다.
- 기본적으로 resources/banner.txt 파일을 로드합니다.
8. ApplicationContext 생성 및 설정
context = this.createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
- createApplicationContext()를 통해 Spring의 ApplicationContext를 생성합니다.
- 일반적으로 AnnotationConfigServletWebServerApplicationContext가 반환됩니다.
- context.setApplicationStartup(this.applicationStartup) : Startup 객체를 ApplicationContext에 설정하여 실행 정보를 추적합니다.
9. ApplicationContext 준비
this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
- 애플리케이션 컨텍스트를 준비하는 단계로, 주요 작업은 다음과 같습니다.
- Environment 설정을 컨텍스트에 적용합니다.
- BeanFactory 후처리기를 등록하여 Bean을 스캔하고 DI를 준비합니다.
- ApplicationContextInitializer를 실행하여 초기화 로직을 수행합니다.
- listeners.contextPrepared(context) 호출로 ContextPreparedEvent 이벤트를 발생시킵니다.
10. ApplicationContext 초기화
this.refreshContext(context);
- refreshContext()를 호출하여 Spring 애플리케이션 컨텍스트를 초기화합니다.
- refresh() 내부에서 이루어지는 작업:
- BeanFactoryPostProcessor 실행
- 모든 Bean을 인스턴스화 및 주입
- ApplicationListener 등록
- Spring 내부적으로 onRefresh() 메서드를 호출하여 서블릿 컨테이너를 준비합니다.
- listeners.contextLoaded(context) 호출로 ContextLoadedEvent 이벤트 발생
11. 애플리케이션 실행 후처리
this.afterRefresh(context, applicationArguments); startup.started();
- afterRefresh(context, applicationArguments) : 컨텍스트가 준비된 후 필요한 후처리를 수행하는 메서드입니다.
- startup.started(); : 실행이 시작되었음을 기록합니다.
12. 애플리케이션 실행 정보 로깅
if (this.properties.isLogStartupInfo()) {
(new StartupInfoLogger(this.mainApplicationClass, environment))
.logStarted(this.getApplicationLog(), startup);
}
- StartupInfoLogger를 사용하여 애플리케이션 시작 정보를 로그에 출력합니다.
- 실행된 환경과 JVM 정보를 기록합니다.
13. 리스너 실행 (started)
listeners.started(context, startup.timeTakenToStarted());
- started() 메서드를 호출하여 ApplicationStartedEvent 이벤트를 발생시킵니다.
- 이는 @EventListener(ApplicationStartedEvent.class)를 이용해 이벤트 리스너를 구현할 때 활용됩니다.
14. CommandLineRunner & ApplicationRunner 실행
this.callRunners(context, applicationArguments);
- CommandLineRunner와 ApplicationRunner 인터페이스를 구현한 Bean들을 실행합니다.
- 예제:
@Component
public class MyRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("CommandLineRunner 실행됨!");
}
}
15. 리스너 실행 (ready)
if (context.isRunning()) {
listeners.ready(context, startup.ready());
}
- ready() 메서드를 호출하여 ApplicationReadyEvent 이벤트를 발생시킵니다.
- 이는 애플리케이션이 완전히 실행된 이후에 발생하는 이벤트로, @EventListener(ApplicationReadyEvent.class)를 통해 특정 로직을 실행할 수 있습니다.
16. 예외 처리
} catch (Throwable ex) {
throw this.handleRunFailure(context, ex, listeners);
}
- 실행 중 예외가 발생하면 handleRunFailure()를 호출하여 오류를 처리합니다.
- 필요한 경우 Spring Boot Error Handling 로직을 수행하고, 적절한 종료 처리를 합니다.
17. ApplicationContext 반환
return context;
- 최종적으로 ApplicationContext를 반환하여 애플리케이션이 정상적으로 실행됩니다.
정리
- Startup 객체 생성 (실행 시간 측정)
- Shutdown Hook 설정
- Bootstrap Context 생성
- ApplicationContext 설정
- Spring 실행 리스너 초기화
- Environment 설정
- 배너 출력
- ApplicationContext 생성 및 설정
- ApplicationContext 준비 (Bean 등록 등)
- ApplicationContext 초기화 (DI 수행)
- 실행 후처리
- 실행 정보 로깅
- ApplicationStartedEvent 발생
- CommandLineRunner 및 ApplicationRunner 실행
- ApplicationReadyEvent 발생
- 예외 처리
- 최종적으로 ApplicationContext 반환
2. ApplicationContext가 생성되는 과정
- BeanDefinition 등록 → Bean 생성 → Bean 초기화 → 의존성 주입(DI) 과정이 순차적으로 진행됨.
- @SpringBootApplication이 선언된 클래스(메인 클래스)를 기준으로 컴포넌트 스캔을 실행하여 @Component, @Service, @Repository, @Controller 등의 Bean을 자동으로 찾고 등록함.
- @Autowired 및 @Resource를 통해 의존성 주입을 수행하고, 모든 Bean이 초기화됨.
ApplicationContext context = SpringApplication.run(DiagramOpenSourceApplication.class, args);
context.getBean(DefaultDependencyExtractor.class);
<T> T getBean(Class<T> requiredType) throws BeansException;
의존성 분석 과정
Spring 컨텍스트의 모든 Bean 탐색
- context.getBeanDefinitionNames()을 호출하여 현재 등록된 모든 빈(Bean)의 이름을 조회합니다.
- DI 주입이 발생하는 Bean들만 필터링하여 분석을 수행.
String[] beanNames = context.getBeanDefinitionNames();
public String[] getBeanDefinitionNames() {
return this.getBeanFactory().getBeanDefinitionNames();
}
스프링 핵심 원리(4) - 스프링 컨테이너와 스프링 빈
1. 스프링 컨테이너 생성 스프링 컨테이너 생성 과정 스프링 컨테이너 생성 ApplicationContext: 스프링 컨테이너 (인터페이스) 스프링 빈 등록 스프링 빈 의존관계 설정 2. 컨테이너에 등록된 모든 빈
velog.io
for(Method method : beanClass.getDeclaredMethods()) {
String mappingInfo = null;
if (method.isAnnotationPresent(GetMapping.class)) {
GetMapping gm = (GetMapping)method.getAnnotation(GetMapping.class);
String methodMapping = gm.value().length > 0 ? gm.value()[0] : "";
String var10000 = this.mergeMapping(classMapping, methodMapping);
mappingInfo = "GET " + var10000;
} else if (method.isAnnotationPresent(PostMapping.class)) {
PostMapping pm = (PostMapping)method.getAnnotation(PostMapping.class);
String methodMapping = pm.value().length > 0 ? pm.value()[0] : "";
String var65 = this.mergeMapping(classMapping, methodMapping);
mappingInfo = "POST " + var65;
else if (method.isAnnotationPresent(DeleteMapping.class)) {
DeleteMapping dm = (DeleteMapping)method.getAnnotation(DeleteMapping.class);
String methodMapping = dm.value().length > 0 ? dm.value()[0] : "";
String var66 = this.mergeMapping(classMapping, methodMapping);
mappingInfo = "DELETE " + var66;
} else if (method.isAnnotationPresent(PutMapping.class)) {
PutMapping pm = (PutMapping)method.getAnnotation(PutMapping.class);
String methodMapping = pm.value().length > 0 ? pm.value()[0] : "";
String var67 = this.mergeMapping(classMapping, methodMapping);
mappingInfo = "PUT " + var67;
} else if (method.isAnnotationPresent(RequestMapping.class)) {
RequestMapping rm = (RequestMapping)method.getAnnotation(RequestMapping.class);
String methodMapping = rm.value().length > 0 ? rm.value()[0] : "";
String var68 = this.mergeMapping(classMapping, methodMapping);
mappingInfo = "REQUEST " + var68;
}