개발 경력이 어느덧 5년 차에 접어들었다. 그동안 스프링을 사용해 다양한 프로젝트를 진행해왔고, 이제는 API를 빠르게 만드는 데 어느 정도 익숙해졌다.
하지만, 그렇게 만들어내는 내자신이 어느 순간부터 ‘API 공장’처럼 느껴지기 시작했다.
기능은 동작하지만, 그 안에 숨어 있는 더 나은 설계나 최적화 기회들을 놓치고 있는 듯한 기분이었다.
이제는 스프링을 단순히 기능을 구현하는 도구로만 쓰기보다는, 그 이면에 숨겨진 원리와 철학을 더 깊이 이해해야 할 시점이 아닐까 하는 생각이 들었다. API의 성능이나 구조적인 문제를 마주할 때마다 “이게 정말 이렇게 만드는 게 맞나?“라는 질문이 끊임없이 떠올랐다.
그럴 때마다 스스로 해답을 찾기 위해 여러 자료들을 뒤져보곤 했지만, 명확한 방향을 찾기가 쉽지 않았다.
그래서 이번에는 조금 다른 접근을 해보기로 했다. 스프링이라는 도구를 더 깊이 이해하고, 내가 만든 API의 개선점을 명확히 잡아나갈 수 있는 실마리를 찾아보려고 한다. 이를 위해 스프링 레퍼런스 가이드를 제대로 파헤쳐 보며, 내가 놓친 부분들을 하나씩 점검해볼 생각이다.
https://docs.spring.io/spring-boot/reference/features/spring-application.html
오늘 파해칠 첫번째 단락이다. 기본 스프링의 경우 문서는 너무 방대하여 SpringBoot 문서를 위주로 다뤄볼 예정이다.
1. SpringApplication
먼저 Spring의 시작이 되는 클래스이다.
@SpringBootApplication 어노테이션을 통해 스프링이 시작되는 클래스를 선언할 수 있다.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MyTestApplication {
public static void main(String[] args) {
SpringApplication.run(MyTestApplication.class, args);
}
}
1-1. @SpringBootApplication
일반적인 Spring 프레임워크와 Spring Boot의 가장 큰 차이점 중 하나는 자동 설정과 간편한 구동이다. 그 중심에는 바로 @SpringBootApplication 어노테이션이 있다. 이 어노테이션은 단순히 애플리케이션의 시작점을 표시하는 것 이상으로, Spring Boot의 핵심적인 동작을 결정짓는 중요한 역할을 한다.
1-2. @SpringBootApplication의 역할
해당 어노테이션의 상단부 옵션을 보면 다음과 같다.
@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은 사실 세 가지 중요한 어노테이션을 결합한 복합 어노테이션이다.
1. @SpringBootConfiguration:
이 어노테이션은 스프링 애플리케이션의 설정 클래스를 지정한다. 애플리케이션 구동 시 설정을 위한 빈(Bean)을 정의하고, 이를 통해 애플리케이션의 전반적인 동작을 조율한다. 이는 기존 Spring의 @Configuration과 동일한 역할을 수행하지만, Spring Boot의 설정을 관리하는 특수한 버전이라 할 수 있다.
2. @EnableAutoConfiguration:
Spring Boot의 강력한 기능 중 하나인 자동 설정을 활성화한다. Spring Boot는 클래스 경로에 있는 라이브러리를 분석하여 애플리케이션에 적절한 빈들을 자동으로 구성해준다. 예를 들어, spring-boot-starter-web이 있다면 Spring Boot는 이를 감지하고 자동으로 내장 웹 서버와 관련된 설정을 추가해준다. 이 덕분에 우리는 별도의 설정 없이도 바로 애플리케이션을 실행할 수 있다.
3. @ComponentScan:
이 어노테이션은 컴포넌트 스캔을 통해, 특정 패키지와 그 하위 패키지에 있는 빈(Bean)들을 자동으로 탐색하여 스프링 컨텍스트에 등록한다. 이를 통해 개발자는 별도로 빈을 수동으로 등록할 필요 없이, 애플리케이션이 자동으로 필요한 빈을 스캔하고 관리할 수 있게 된다.
이러한 컴포넌트들은 모두 스프링 컨테이너에 저장되며, 스프링 컨테이너는 애플리케이션의 빈을 관리하고 의존성을 주입하는 핵심 역할을 담당한다. 스프링 컨테이너에 대해서는 추후에 더 자세히 설명할 예정이다.
1-3. Spring과 Spring Boot의 차이점에서 @SpringBootApplication의 중요성
전통적인 Spring 애플리케이션에서는 구성 파일을 작성하고, 애플리케이션의 모든 빈을 수동으로 설정하는 것이 일반적이었다. 반면에, Spring Boot는 자동 설정을 통해 이러한 반복적인 설정 작업을 대폭 줄여준다. 그 중심에 있는 @SpringBootApplication은 단순한 어노테이션이 아니라, Spring Boot의 철학인 “컨벤션에 의한 설정” 을 실현하는 핵심 도구다.
이 어노테이션을 통해 개발자는 복잡한 설정을 최소화하고, 빠르게 애플리케이션을 실행할 수 있다. 특히 @EnableAutoConfiguration 덕분에 필요한 라이브러리만 추가하면, Spring Boot는 해당 라이브러리에 맞는 기본 설정을 자동으로 적용해주어 개발 속도를 크게 향상시킨다.
@SpringBootApplication은 스프링 부트를 더 효율적이고 생산적으로 사용할 수 있도록 해주는 핵심적인 요소다. 이 어노테이션 덕분에 개발자는 스프링 부트 애플리케이션을 쉽게 구동할 수 있고, 자동 설정과 컴포넌트 스캔을 통해 빠르고 유연하게 애플리케이션을 개발할 수 있다.
1-4. SpringApplication 실행 과정
Spring Boot 애플리케이션은 SpringApplication.run()을 통해 실행된다.
이 과정에서 애플리케이션 컨텍스트가 생성되고, 빈(Bean)이 초기화된다. 애플리케이션이 실행되면 컨텍스트가 설정되고, 각종 리스너(listener)들이 애플리케이션의 시작과 종료 이벤트를 처리하게 된다.
// SpringApplication.class 내부
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class<?>[] { primarySource }, args);
}
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
public ConfigurableApplicationContext run(String... args) {
Startup startup = Startup.create();
if (this.registerShutdownHook) {
SpringApplication.shutdownHook.enableShutdownHookAddition();
}
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
startup.started();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), startup);
}
listeners.started(context, startup.timeTakenToStarted());
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
throw handleRunFailure(context, ex, listeners);
}
try {
if (context.isRunning()) {
listeners.ready(context, startup.ready());
}
}
catch (Throwable ex) {
throw handleRunFailure(context, ex, null);
}
return context;
}
간단히 요약하자면 이 클래스는 Spring Boot 애플리케이션의 실행 과정을 처리하는 핵심 역할을 한다.
실행은 환경 설정, 컨텍스트 초기화, 컨텍스트 리프레시, 그리고 런너 실행 순으로 진행된다. 이 과정에서 다양한 리스너가 실행되어 애플리케이션의 시작과 종료를 관리하며, 덕분에 최소한의 설정으로 쉽게 애플리케이션을 실행할 수 있다.
여기서 가장 중요한 동작은 컨텍스트를 생성하고 그 컨텍스트에 빈(Bean)을 등록하고 초기화하는 과정이라고 볼 수 있다.
prepareContext() 메서드를 통해 컨텍스트와 빈을 준비하고, Lazy 로딩을 통해 실제로 필요할 때 빈이 로드될 수 있게 한다. 이후, refreshContext()가 호출되면 모든 빈이 초기화되며, 애플리케이션은 본격적으로 실행될 준비가 완료된다.
이 모든 과정은 스프링 컨테이너에서 이루어지며, 스프링 컨테이너는 빈을 관리하고, 의존성을 주입하는 핵심 역할을 담당한다. 스프링 컨테이너는 이러한 빈 등록 및 초기화 과정을 통해 애플리케이션이 효율적으로 동작할 수 있도록 지원한다.
private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments, Banner printedBanner) {
context.setEnvironment(environment);
postProcessApplicationContext(context);
addAotGeneratedInitializerIfNecessary(this.initializers);
applyInitializers(context);
listeners.contextPrepared(context);
bootstrapContext.close(context);
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
// Add boot specific singleton beans
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
if (beanFactory instanceof AbstractAutowireCapableBeanFactory autowireCapableBeanFactory) {
autowireCapableBeanFactory.setAllowCircularReferences(this.allowCircularReferences);
if (beanFactory instanceof DefaultListableBeanFactory listableBeanFactory) {
listableBeanFactory.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
}
if (this.lazyInitialization) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
if (this.keepAlive) {
context.addApplicationListener(new KeepAlive());
}
context.addBeanFactoryPostProcessor(new PropertySourceOrderingBeanFactoryPostProcessor(context));
if (!AotDetector.useGeneratedArtifacts()) {
// Load the sources
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
load(context, sources.toArray(new Object[0]));
}
listeners.contextLoaded(context);
}
listeners.contextLoaded를 통해 스프링의 다음 리스너들이 context 로드가 완료되었다는 이벤트를 받음으로써 빈이 모두 준비된 이후의 동작을 진행할수 있게 해준다.
이후 컴포넌트가 존재할때 각 컴포넌트를 Bean으로 생성하여 만들어 주는데 이는 또 깊이가 있음으로 다음에 준비해 보도록 하겠다.
최종적으로 따라가게 되면
prepareContext 를 통해 사용할 빈 및 각종 어플리케이션과 컨텍스트를 준비한 후 refresh를 통해 해당 빈들을 초기화 해주면서 스프링에 서 사용할 각 클래스를 Bean 형태로 사용할수 있게 준비해주는 과정이라 보면 된다.
이후 callRunners 매서드 호출을 통해 ApplicationRunner/CommandLineRunner 구현체의 클래스들의 run 메서드를 실행시켜 주는 과정으로 스프링은 동작하게 된다.
callRunners의 동작이 모두 끝나면, 컨텍스트가 정상적으로 초기화되어 context.isRunning() 상태가 true로 바뀐다. 이 시점에서 리스너(listener)에게 ready 이벤트가 발생해 애플리케이션이 실행 준비가 완료되었음을 알린다.
listeners.ready(context, startup.ready())가 호출되면, 애플리케이션이 완전히 준비되었음을 리스너에게 알린다.
이후부터는 웹 요청 처리, 스케줄링 작업, 배치 작업 등의 다양한 스프링 기능들이 활성화된다.
예를 들어, spring-boot-starter-web, spring-boot-starter-batch, 그리고 스케줄링 작업은 spring-boot-starter-task와 @EnableScheduling을 통해 사용할 수 있다.
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.3.4)
2024-10-08T01:11:30.943+09:00 INFO 95170 --- [my-test] [ main] com.example.mytest.MyTestApplication : Starting MyTestApplication using Java 23 with PID 95170 (/Users/choehyegeun/Desktop/java-lab/my-test/build/classes/java/main started by choehyegeun in /Users/choehyegeun/Desktop/java-lab/my-test)
2024-10-08T01:11:30.944+09:00 INFO 95170 --- [my-test] [ main] com.example.mytest.MyTestApplication : No active profile set, falling back to 1 default profile: "default"
2024-10-08T01:11:31.121+09:00 INFO 95170 --- [my-test] [ main] com.example.mytest.MyTestApplication : Started MyTestApplication in 0.288 seconds (process running for 0.678)
Process finished with exit code 0
마지막으로, 애플리케이션이 모든 작업을 마치고 더 이상 실행할 작업이 없을 경우, 스프링 애플리케이션은 정상적으로 종료되며, 프로세스가 끝난다.
정리하자면 스프링 부트 어플리케이션은
1. @SpringBootApplication (@SpringBootConfiguration, @EnableAutoConfiguration, @ComponentScan)을 통해 스프링에서 사용될 빈의 정의를 등록하고, 컴포넌트 스캔을 통해 해당 빈들이 스프링 컨테이너에 등록된다. 이 과정에서는 빈의 클래스 정보와 의존성들이 스프링 컨테이너에 등록되며, 실제 인스턴스화(객체 생성)는 이루어지지 않는다.
2. SpringApplication.run(MyTestApplication.class, args) 메서드를 통해 컨텍스트가 초기화되면서, **등록된 빈을 인스턴스화(객체로 생성)**하고 의존성 주입을 완료한 후, 스프링 애플리케이션 로직을 실행시킨다. 이 시점에서 실제 빈 객체가 생성되며, 스프링 애플리케이션의 각종 로직과 의존성 주입이 진행된다.
이처럼 스프링은 수많은 자바 클래스로 이루어진 복잡한 프레임워크다.
물론, 이러한 내부 동작을 깊이 알지 못해도 애플리케이션을 충분히 개발할 수 있다.
하지만 스프링의 원리와 동작 방식을 이해하는 것은 단순한 기능 구현을 넘어, 스프링의 강력한 기능들을 더 효과적으로 활용하는 데 큰 도움이 될 것이다.
이번 과정을 통해 스프링을 더 “스프링답게” 사용할 수 있는 방법을 고민하게 되었고, 앞으로도 그 가능성을 탐구하며 성장해 나가야겠다는 생각을 하며 글을 마무리한다.
개인의 공부를 정리해가며 작성한 글이며 오타가 있거나 잘못 작성된 내용이 있을수 있습니다. 잘못된 정보나 다른 의견이 있으시다면 댓글로 작성 부탁드립니다.
'Web > Spring' 카테고리의 다른 글
[SCDF] 우당탕탕 Spring Cloud Data Flow 시작기.. (0) | 2023.02.23 |
---|---|
[Spring Cloud] API Gateway (2) | 2022.08.26 |
[Spring Cloud] Eureka 서버 기동하기 (0) | 2022.08.18 |
[Spring] 트랜잭션 전파(Transactional Propagation) (0) | 2022.07.13 |
[Query dsl] maven QueryDsl 설정 (0) | 2022.07.11 |
댓글