Spring Boot는 간편한 설정과 강력한 자동화 기능 덕분에 많은 개발자들에게 사랑받고 있습니다.
하지만 막상 내부 구조를 깊이 들여다보면, 매우 정교하고 세부적인 단계들을 거치고 있음을 알 수 있습니다.
이 번 글에서는 SpringBoot가 애플리케이션 실행 시 내부적으로 어떤 흐름으로 동작되는지, 그리고 Bean이 어떤 기준으로 등록되는지에 대해 정리해 보고자 합니다.
이번 글에서 다루는 전체적인 목차는 다음과 같습니다.
- Spring Boot의 시작점과 전체 흐름
- ComponentScan의 원리와 범위
- Bean 후보로 등록되는 주요 애노테이션
- Bean 등록의 조건과 우선순위
- AutoConfiguration의 작동 원리
Spring Boot 애플리케이션의 시작점
대부분의 Spring Boot 애플리케이션은 아래의 코드에서 시작됩니다.
package com.example.hello;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class HelloApplication {
public static void main(String[] args) {
SpringApplication.run(HelloApplication.class, args);
}
}
SpringApplication.run() 메서드는 다음과 같은 과정을 거칩니다.
- SpringApplication 객체 생성 및 애플리케이션 유형 결정
- SERVLET, REACTIVE, NONE 중 하나로 결정
- Environment 설정 로딩
- application.yml 혹은 application.properties, 환경 변수 등
- 적절한 ApplicationContext 생성
- ComponentScan을 통한 Bean 후보 탐색
- AutoConfiguration을 로딩 및 조건을 평가하여 Bean 등록
- Singleton Bean들을 생성 및 의존성 주입
- 웹 서버 (Tomcat, Jetty등) 구동
- ApplicationReadyEvent를 발행
ComponentScan의 범위와 기준
@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을 포함합니다.
ComponentScan은 Bean 후보들을 찾아내는 역할을 합니다. 기본적으로는 메인 클래스가 위치한 패키지와 그 하위 패키지 전체를 스캔합니다.
Bean 후보로 등록되는 주요 애노테이션
- @Component : 기본 Bean 등록 대상
- @Service : 서비스 레이어를 나타냄
- @Repository : 데이터 접근 계층을 나타내며, 예외 변환 기능을 제공
- @Controller 및 @RestController : 웹 요청 처리 Bean
- @Configuration : 설정 클래스임을 나타냄
Bean 등록의 조건과 우선 순위
Bean 충돌 해결법
동일한 타입의 빈이 두 개 이상이면 NoUniqueBeanDefinitionException이 발생할 수 있습니다.
이를 해결하기 위해서 @Primary 혹은 @Qualifier를 사용할 수 있습니다.
interface PaymentService { }
@Service
@Primary
public class PrimaryPaymentService implements PaymentService {
// ...
}
@Service("paypalService")
public class PaypalService implements PaymentService {
// ...
}
public class OrderService {
private final PaymentService paymentService;
public OrderService (
@Qualifier("paypalService") PaymentService paymentService
) {
this.paymentService = paymentService;
}
// ...
}
- @Primary : 인터페이스를 주입받을 때 기본적으로 받을 사용하고자 하는 Bean에 적용합니다.
- @Qualifier : 특정 빈을 사용하고자 할 때, Bean의 이름을 기입하여 사용합니다.
AutoConfiguration 작동 원리
@SpringBootApplication 애노테이션에 @EnableAutoConfiguration이 붙어있습니다.
해당 애노테이션을 살펴보면 다음과 같습니다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
}
@Import 애노테이션의 경우 Component로 등록하고자 하는 클래스들을 명시할 수 있습니다.
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
}
AutoConfigurationImportSelector의 경우 DeferredImportSelector를 구현하고 있습니다.
DeferredImportSelector의 경우 META-INF/spring/에 있는. imports에 명시된 클래스 기준으로 Component를 등록해 주는 역할을 합니다.
SpringBoot 2.7부터 spring.factories 대신 META-INF/spring/**. imports 파일을 통해 자동 구성을 로딩합니다.
마무리하며
이번 글에서는 Spring Boot의 전반적인 실행 흐름과 Bean 등록 과정을 자세히 살펴보았습니다.
Spring Boot의 내부 구조를 명확히 이해하는 것은 실무에서 발생할 수 있는 Bean 충돌 문제를 효과적으로 해결하거나,
자동 구성의 불필요한 빈 등록을 방지해 성능을 최적화하는 데 반드시 필요한 지식입니다.
이 글을 통해 스프링 프레임워크에 대한 내부 원리를 한층 더 깊이 이해하는 데 도움을 얻으셨기를 바라며 글을 마칩니다.
'Spring' 카테고리의 다른 글
[Spring] 테스트 환경 통합 (0) | 2024.03.09 |
---|---|
[Spring] @Retryable, Event 적용기 (1) | 2023.12.04 |
Spring, 그리고 Test (4) | 2023.10.11 |
Spring MVC - 요청과 관련된 사용법 정리 (0) | 2023.07.06 |
Junit에서 TestInstance의 생명주기 (0) | 2023.07.03 |