Spring Boot 자동 구성과 빈 생명주기 동작 원리 완벽 가이드
Spring Boot 자동 구성과 빈 생명주기 동작 원리 완벽 가이드
이전 글에서 Spring의 IoC/DI 개념과 AOP/@Transactional의 프록시 기반 동작 원리를 다루었다. 그런데 “어노테이션 하나 붙이면 다 알아서 되는” 그 마법의 이면에는 정교한 메커니즘이 숨어 있다.
@SpringBootApplication 하나로 톰캣이 뜨고, spring-boot-starter-data-jpa를 의존성에 추가하면 DataSource와 EntityManagerFactory가 자동으로 구성되고, @Component를 붙이면 빈으로 등록된다. 어떻게? 이 질문에 답하지 못하면, 설정이 안 먹히거나 빈 충돌이 발생했을 때 디버깅할 수 없다.
이 글은 Spring Boot의 자동 구성(Auto-Configuration)의 작동 메커니즘, 빈의 전체 생명주기, ApplicationContext의 초기화 과정, 그리고 실무에서 마주치는 빈 관련 문제와 해결 전략을 깊이 있게 다룬다.
1. @SpringBootApplication 해부
1.1 세 가지 어노테이션의 합성
@SpringBootApplication은 단일 어노테이션이 아니라, 세 가지 어노테이션의 합성(Meta-annotation)이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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 {
// ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@SpringBootApplication의 내부:
┌─────────────────────────────────────────────────────────┐
│ @SpringBootApplication │
│ │
│ ┌───────────────────┐ │
│ │ @SpringBootConfig │ = @Configuration │
│ │ │ 이 클래스가 Spring 설정 클래스임 │
│ └───────────────────┘ │
│ │
│ ┌───────────────────────────────────┐ │
│ │ @EnableAutoConfiguration │ │
│ │ │ │
│ │ @AutoConfigurationPackage │ │
│ │ → 메인 클래스의 패키지를 기본 패키지로 │ │
│ │ │ │
│ │ @Import(AutoConfigurationImport │ │
│ │ SelectionFilter.class) │ │
│ │ → spring.factories에서 자동 구성 │ │
│ │ 클래스 목록을 로드 │ │
│ └───────────────────────────────────┘ │
│ │
│ ┌───────────────────┐ │
│ │ @ComponentScan │ │
│ │ 메인 클래스 패키지 │ │
│ │ 하위를 스캔 │ │
│ └───────────────────┘ │
└─────────────────────────────────────────────────────────┘
1.2 @SpringBootConfiguration
@Configuration의 특수화된 형태이다. 기능적으로는 @Configuration과 동일하지만, Spring Boot 애플리케이션에서 하나만 존재해야 한다는 의미론적 제약이 있다. 테스트에서 @SpringBootTest가 설정 클래스를 찾을 때 이 어노테이션을 기준으로 탐색한다.
1.3 @ComponentScan의 동작
@ComponentScan은 메인 클래스가 위치한 패키지를 기본 베이스 패키지로 하여, 하위 패키지에서 @Component, @Service, @Repository, @Controller, @Configuration 등이 붙은 클래스를 찾아 빈으로 등록한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
패키지 구조와 컴포넌트 스캔:
com.example.myapp ← @SpringBootApplication 위치 (베이스 패키지)
├── MyApplication.java
├── controller/
│ └── UserController.java ← @RestController → 스캔 대상 ✓
├── service/
│ └── UserService.java ← @Service → 스캔 대상 ✓
├── repository/
│ └── UserRepository.java ← @Repository → 스캔 대상 ✓
└── config/
└── WebConfig.java ← @Configuration → 스캔 대상 ✓
com.example.other/ ← 다른 패키지 → 스캔 대상 ✗
└── ExternalService.java
흔한 실수: 메인 클래스를
com.example.myapp.config같은 하위 패키지에 두면,com.example.myapp.controller의 빈이 스캔되지 않는다. 메인 클래스는 최상위 패키지에 두어야 한다.
1.4 @ComponentScan의 내부 과정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ComponentScan 처리 과정:
① ClassPathBeanDefinitionScanner 생성
│
② 베이스 패키지 결정 (메인 클래스 패키지)
│
③ 클래스패스에서 .class 파일 탐색
│
④ 각 클래스의 어노테이션 확인
│ @Component, @Service, @Repository, @Controller, @Configuration
│
⑤ 필터 적용 (includeFilters, excludeFilters)
│
⑥ BeanDefinition 생성 및 BeanDefinitionRegistry에 등록
│
⑦ 빈 이름 결정 (기본: 클래스명의 camelCase)
│ UserService → "userService"
│ @Component("customName") → "customName"
2. 자동 구성 (Auto-Configuration) 메커니즘
2.1 자동 구성이란
Spring Boot의 자동 구성은 클래스패스에 있는 라이브러리, 기존 빈 정의, 프로퍼티 설정을 기반으로 필요한 빈을 자동으로 등록하는 메커니즘이다.
spring-boot-starter-web을 의존성에 추가하면 DispatcherServlet, EmbeddedTomcat, Jackson ObjectMapper 등이 자동으로 빈 등록된다. 직접 @Bean으로 등록하지 않아도 된다.
2.2 자동 구성 클래스 로딩
Spring Boot 2.7 이전에는 META-INF/spring.factories 파일에, 3.0부터는 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일에 자동 구성 클래스 목록이 정의된다.
1
2
3
4
5
6
7
8
9
# META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
# (Spring Boot 3.0+)
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
자동 구성 로딩 과정:
@EnableAutoConfiguration
│
│ @Import(AutoConfigurationImportSelector.class)
▼
AutoConfigurationImportSelector
│
│ getAutoConfigurationEntry()
▼
① spring.factories / AutoConfiguration.imports에서
자동 구성 클래스 목록 로드 (100+ 개)
│
② 중복 제거
│
③ exclude 처리 (@SpringBootApplication(exclude = ...))
│
④ AutoConfigurationImportFilter 적용
(OnClassCondition 사전 필터링 — 클래스패스에 없으면 즉시 제외)
│
⑤ 나머지 클래스의 @Conditional 평가
│
⑥ 조건을 만족하는 클래스만 빈 등록
2.3 @Conditional 어노테이션 — 조건부 빈 등록
자동 구성의 핵심은 @Conditional이다. “이 조건이 충족될 때만 빈을 등록한다”는 선언적 방식이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// DataSource 자동 구성 클래스 (간략화)
@AutoConfiguration(after = DataSourceAutoConfiguration.class)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
@Configuration
@Conditional(EmbeddedDatabaseCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
static class EmbeddedDatabaseConfiguration {
// H2, HSQLDB 등 임베디드 DB가 클래스패스에 있으면 DataSource 자동 생성
}
@Configuration
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
static class PooledDataSourceConfiguration {
// HikariCP 등 커넥션 풀 DataSource 자동 생성
}
}
| 어노테이션 | 조건 |
|---|---|
@ConditionalOnClass |
클래스패스에 특정 클래스가 존재할 때 |
@ConditionalOnMissingClass |
클래스패스에 특정 클래스가 없을 때 |
@ConditionalOnBean |
특정 빈이 이미 등록되어 있을 때 |
@ConditionalOnMissingBean |
특정 빈이 등록되어 있지 않을 때 |
@ConditionalOnProperty |
특정 프로퍼티가 설정되어 있을 때 |
@ConditionalOnWebApplication |
웹 애플리케이션일 때 |
@ConditionalOnExpression |
SpEL 표현식이 true일 때 |
2.4 자동 구성의 우선순위
1
2
3
4
5
6
7
8
9
10
빈 등록 우선순위:
① 사용자 정의 빈 (@Bean, @Component 등) ← 최우선
② 자동 구성 빈 (@ConditionalOnMissingBean) ← 사용자 빈 없을 때만
이것이 핵심이다:
사용자가 직접 DataSource @Bean을 등록하면
→ DataSourceAutoConfiguration의 @ConditionalOnMissingBean 조건 실패
→ 자동 구성 DataSource는 생성되지 않음
→ 사용자의 DataSource가 사용됨
이 구조가 Spring Boot의 “Convention over Configuration(관례 우선, 설정은 필요할 때만)” 철학을 구현한다. 기본값은 자동 구성이 제공하되, 커스터마이징이 필요하면 사용자 빈이 자동 구성을 오버라이드한다.
2.5 자동 구성 디버깅
어떤 자동 구성이 적용되었고 어떤 것이 제외되었는지 확인하는 방법:
1
2
3
# application.properties
debug=true
# 또는 실행 시 --debug 플래그
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
============================
CONDITIONS EVALUATION REPORT
============================
Positive matches: (적용된 자동 구성)
-----------------
DataSourceAutoConfiguration matched:
- @ConditionalOnClass found required classes 'javax.sql.DataSource',
'org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType'
- @ConditionalOnMissingBean (types: io.r2dbc.spi.ConnectionFactory;
SearchStrategy: all) did not find any beans
Negative matches: (제외된 자동 구성)
-----------------
RedisAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find required class
'org.springframework.data.redis.core.RedisOperations'
3. Spring Boot Starter의 동작 원리
3.1 Starter란
Starter는 관련된 의존성들을 묶어놓은 편의 의존성이다. 코드가 아니다.
1
2
3
4
5
6
7
8
9
10
11
12
spring-boot-starter-web의 구조:
spring-boot-starter-web
├── spring-boot-starter (핵심 스타터: spring-boot, spring-context 등)
├── spring-boot-starter-json (Jackson)
├── spring-boot-starter-tomcat (Embedded Tomcat)
├── spring-web (Spring Web)
└── spring-webmvc (Spring MVC)
→ 이 의존성들이 클래스패스에 들어오면
→ 자동 구성 클래스의 @ConditionalOnClass 조건이 충족되고
→ DispatcherServlet, Tomcat, ObjectMapper 등이 자동 등록
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Starter의 동작 흐름:
① build.gradle에 starter 추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
│
② Gradle/Maven이 의존성 다운로드
→ hibernate-core, spring-data-jpa, HikariCP 등이 클래스패스에 추가
│
③ Spring Boot 시작 시 자동 구성 스캔
│
④ HibernateJpaAutoConfiguration:
@ConditionalOnClass(EntityManager.class) → ✓ (hibernate-core에 있음)
@ConditionalOnMissingBean(EntityManagerFactory.class) → ✓
│
⑤ EntityManagerFactory, TransactionManager 자동 빈 등록
3.2 커스텀 Starter 만들기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// my-custom-starter의 자동 구성 클래스
@AutoConfiguration
@ConditionalOnClass(MyService.class)
@EnableConfigurationProperties(MyProperties.class)
public class MyAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public MyService myService(MyProperties properties) {
return new MyService(properties.getUrl(), properties.getTimeout());
}
}
// 프로퍼티 클래스
@ConfigurationProperties(prefix = "my.service")
public class MyProperties {
private String url = "http://localhost:8080";
private int timeout = 5000;
// getter, setter
}
1
2
# META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.MyAutoConfiguration
사용하는 쪽에서는 의존성만 추가하면 된다:
1
2
3
4
5
# application.yml
my:
service:
url: https://api.example.com
timeout: 3000
4. 빈 생명주기 (Bean Lifecycle)
4.1 전체 생명주기 흐름
Spring 빈은 단순히 “생성 → 사용 → 소멸”이 아니라, 여러 확장 포인트를 거치는 정교한 생명주기를 가진다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
빈 생명주기 전체 흐름:
① BeanDefinition 등록
│ @ComponentScan, @Bean, XML 등에서 빈 메타데이터 수집
│
② BeanFactoryPostProcessor 실행
│ BeanDefinition 자체를 수정할 수 있는 확장 포인트
│ 예: PropertySourcesPlaceholderConfigurer (${} 치환)
│
③ 빈 인스턴스 생성 (Instantiation)
│ 생성자 호출
│
④ 의존성 주입 (Dependency Injection)
│ @Autowired, 생성자 주입, 세터 주입
│
⑤ BeanPostProcessor.postProcessBeforeInitialization()
│ 예: @PostConstruct 처리 (CommonAnnotationBeanPostProcessor)
│ 예: @Autowired 처리 (AutowiredAnnotationBeanPostProcessor)
│
⑥ 초기화 콜백
│ @PostConstruct → InitializingBean.afterPropertiesSet() → @Bean(initMethod)
│
⑦ BeanPostProcessor.postProcessAfterInitialization()
│ 예: AOP 프록시 생성 (AbstractAutoProxyCreator)
│ 이 시점에서 실제 빈이 프록시로 교체될 수 있다!
│
⑧ 빈 사용 (Application 실행 중)
│
⑨ 소멸 콜백
│ @PreDestroy → DisposableBean.destroy() → @Bean(destroyMethod)
│
⑩ 빈 소멸
4.2 각 단계 상세
BeanDefinition — 빈의 설계도
1
2
3
4
5
6
7
8
9
10
11
// 빈이 어떻게 생성되어야 하는지에 대한 메타데이터
public interface BeanDefinition {
String getBeanClassName(); // 빈 클래스명
String getScope(); // singleton, prototype 등
boolean isLazyInit(); // 지연 초기화 여부
String[] getDependsOn(); // 의존 빈
boolean isAutowireCandidate(); // 자동 주입 대상 여부
String getInitMethodName(); // 초기화 메서드
String getDestroyMethodName(); // 소멸 메서드
// ...
}
BeanFactoryPostProcessor — 빈 정의를 수정하는 확장점
빈 인스턴스가 생성되기 전에, BeanDefinition 자체를 수정할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class CustomBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
// 모든 빈 정의를 순회하며 수정 가능
BeanDefinition bd = beanFactory.getBeanDefinition("userService");
bd.setScope("prototype"); // 스코프 변경!
// PropertySourcesPlaceholderConfigurer는 여기서
// ${db.url} 같은 플레이스홀더를 실제 값으로 치환한다
}
}
BeanPostProcessor — 빈 인스턴스를 가공하는 확장점
빈이 생성된 후에, 빈 인스턴스를 수정하거나 다른 객체로 교체할 수 있다. Spring의 핵심 마법이 여기서 일어난다.
1
2
3
4
5
6
7
8
9
10
11
12
public interface BeanPostProcessor {
// 초기화 전 (@PostConstruct 전)
default Object postProcessBeforeInitialization(Object bean, String beanName) {
return bean;
}
// 초기화 후 (@PostConstruct 후)
// → AOP 프록시가 여기서 생성된다!
default Object postProcessAfterInitialization(Object bean, String beanName) {
return bean; // 다른 객체(프록시)를 반환할 수 있다!
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Spring 내부에서 사용하는 주요 BeanPostProcessor:
┌──────────────────────────────────────────────────────┐
│ AutowiredAnnotationBeanPostProcessor │
│ → @Autowired, @Value 처리 │
├──────────────────────────────────────────────────────┤
│ CommonAnnotationBeanPostProcessor │
│ → @PostConstruct, @PreDestroy, @Resource 처리 │
├──────────────────────────────────────────────────────┤
│ AbstractAutoProxyCreator │
│ → @Transactional, @Async, @Cacheable 등의 │
│ AOP 프록시 생성 │
├──────────────────────────────────────────────────────┤
│ ApplicationListenerDetector │
│ → ApplicationListener 구현체 자동 등록 │
├──────────────────────────────────────────────────────┤
│ ScheduledAnnotationBeanPostProcessor │
│ → @Scheduled 처리 │
└──────────────────────────────────────────────────────┘
4.3 초기화와 소멸 콜백
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Component
public class DatabaseConnectionPool implements InitializingBean, DisposableBean {
// === 초기화 (순서: @PostConstruct → afterPropertiesSet → initMethod) ===
@PostConstruct
public void postConstruct() {
System.out.println("1. @PostConstruct — JSR-250 표준");
// 의존성 주입 완료 후 호출
// 가장 권장되는 초기화 방법
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("2. InitializingBean.afterPropertiesSet — Spring 인터페이스");
// Spring에 종속적이므로 @PostConstruct보다 비권장
}
// @Bean(initMethod = "customInit")으로 지정된 경우
public void customInit() {
System.out.println("3. @Bean(initMethod) — 외부 라이브러리 빈에 유용");
}
// === 소멸 (순서: @PreDestroy → destroy → destroyMethod) ===
@PreDestroy
public void preDestroy() {
System.out.println("1. @PreDestroy");
}
@Override
public void destroy() throws Exception {
System.out.println("2. DisposableBean.destroy");
}
// @Bean(destroyMethod = "customDestroy")
public void customDestroy() {
System.out.println("3. @Bean(destroyMethod)");
}
}
실무 권장:
@PostConstruct와@PreDestroy를 사용한다. JSR-250 표준이므로 Spring에 종속적이지 않고, 코드가 간결하다.InitializingBean인터페이스는 외부 라이브러리의 빈을 설정할 때만 사용한다.
4.4 빈 스코프
| 스코프 | 설명 | 생명주기 |
|---|---|---|
| singleton (기본) | 컨테이너에 하나만 존재 | 컨테이너 시작 ~ 종료 |
| prototype | 요청마다 새로 생성 | 생성까지만 관리, 소멸 콜백 없음! |
| request | HTTP 요청마다 하나 | 요청 시작 ~ 응답 |
| session | HTTP 세션마다 하나 | 세션 시작 ~ 만료 |
| application | ServletContext마다 하나 | 앱 시작 ~ 종료 |
1
2
3
4
5
6
@Component
@Scope("prototype")
public class PrototypeBean {
// 매번 새 인스턴스가 생성된다
// 주의: @PreDestroy가 호출되지 않는다!
}
Singleton 빈에서 Prototype 빈 주입 시 주의
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Component // singleton
public class SingletonService {
private final PrototypeBean prototypeBean; // prototype
// 문제: SingletonService 생성 시 한 번만 주입됨
// 이후 prototypeBean은 항상 같은 인스턴스!
public SingletonService(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
}
// 해결법 1: ObjectProvider
@Component
public class SingletonService {
private final ObjectProvider<PrototypeBean> prototypeBeanProvider;
public SingletonService(ObjectProvider<PrototypeBean> provider) {
this.prototypeBeanProvider = provider;
}
public void doSomething() {
// 호출할 때마다 새 인스턴스 생성
PrototypeBean bean = prototypeBeanProvider.getObject();
}
}
// 해결법 2: @Lookup
@Component
public abstract class SingletonService {
public void doSomething() {
PrototypeBean bean = createPrototypeBean(); // 매번 새 인스턴스
}
@Lookup
protected abstract PrototypeBean createPrototypeBean();
}
5. ApplicationContext 초기화 과정
5.1 Spring Boot 시작 흐름
SpringApplication.run()이 호출되면 무엇이 일어나는가?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
SpringApplication.run(MyApp.class, args)
│
├── ① SpringApplication 인스턴스 생성
│ └─ 웹 애플리케이션 타입 추론 (SERVLET, REACTIVE, NONE)
│ └─ ApplicationContextInitializer 로드
│ └─ ApplicationListener 로드
│
├── ② Environment 준비
│ └─ application.yml / properties 로드
│ └─ 커맨드라인 인수 처리
│ └─ 프로파일(active profiles) 결정
│
├── ③ ApplicationContext 생성
│ └─ AnnotationConfigServletWebServerApplicationContext (웹의 경우)
│
├── ④ ApplicationContext 초기화 (refresh)
│ └─ 아래 5.2에서 상세 설명
│
├── ⑤ 내장 웹 서버 시작 (Tomcat)
│
├── ⑥ ApplicationRunner / CommandLineRunner 실행
│
└── ⑦ ApplicationStartedEvent 발행
5.2 AbstractApplicationContext.refresh() — 가장 핵심
Spring 컨테이너의 초기화는 refresh() 메서드에 집약되어 있다. 이 하나의 메서드가 빈 등록부터 프록시 생성까지 모든 것을 수행한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// AbstractApplicationContext.refresh() 간략화
public void refresh() throws BeansException, IllegalStateException {
// 1. BeanFactory 준비
prepareBeanFactory(beanFactory);
// 2. BeanFactoryPostProcessor 실행
// → BeanDefinition 수정, PropertyPlaceholder 처리
// → @Configuration 클래스의 @Bean 메서드 파싱
invokeBeanFactoryPostProcessors(beanFactory);
// 3. BeanPostProcessor 등록
// → AutowiredAnnotationBeanPostProcessor
// → CommonAnnotationBeanPostProcessor
// → AbstractAutoProxyCreator
registerBeanPostProcessors(beanFactory);
// 4. MessageSource 초기화 (국제화)
initMessageSource();
// 5. ApplicationEventMulticaster 초기화
initApplicationEventMulticaster();
// 6. 특수 빈 초기화 (웹 서버 등)
onRefresh();
// 7. ApplicationListener 등록
registerListeners();
// 8. 나머지 싱글톤 빈 인스턴스화
// → 여기서 대부분의 빈이 생성됨!
// → 의존성 주입, 초기화 콜백, BeanPostProcessor 처리
finishBeanFactoryInitialization(beanFactory);
// 9. 컨텍스트 초기화 완료
// → ContextRefreshedEvent 발행
finishRefresh();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
refresh() 시각적 흐름:
┌─────────────────────────────────────────────┐
│ 1. BeanFactory 준비 │
└──────────────────┬──────────────────────────┘
│
┌──────────────────▼──────────────────────────┐
│ 2. BeanFactoryPostProcessor 실행 │
│ - @Configuration 파싱 │
│ - ${} 플레이스홀더 치환 │
│ - 자동 구성 클래스 로딩 │
└──────────────────┬──────────────────────────┘
│
┌──────────────────▼──────────────────────────┐
│ 3. BeanPostProcessor 등록 │
│ (아직 실행 X, 등록만) │
└──────────────────┬──────────────────────────┘
│
┌──────────────────▼──────────────────────────┐
│ 8. 싱글톤 빈 생성 (가장 무거운 단계) │
│ 각 빈에 대해: │
│ ┌────────────────────────────────────┐ │
│ │ 생성자 호출 → DI → Before Init → │ │
│ │ @PostConstruct → After Init │ │
│ │ (프록시 생성 가능) │ │
│ └────────────────────────────────────┘ │
└──────────────────┬──────────────────────────┘
│
┌──────────────────▼──────────────────────────┐
│ 9. 완료 → ContextRefreshedEvent │
└─────────────────────────────────────────────┘
5.3 @Configuration과 CGLIB 프록시
@Configuration 클래스는 CGLIB 프록시로 감싸진다. 이것이 @Bean 메서드 간의 의존성을 올바르게 처리하는 핵심이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
public class AppConfig {
@Bean
public DataSource dataSource() {
return new HikariDataSource();
}
@Bean
public JdbcTemplate jdbcTemplate() {
// 이 호출이 새 DataSource를 생성하는 것이 아니라
// 스프링 컨테이너에서 기존 빈을 반환한다!
return new JdbcTemplate(dataSource());
}
@Bean
public UserRepository userRepository() {
// 여기서도 같은 DataSource 빈을 반환
return new JdbcUserRepository(dataSource());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration CGLIB 프록시의 동작:
일반 클래스라면:
jdbcTemplate() → dataSource() 호출 → 새 HikariDataSource 생성
userRepository() → dataSource() 호출 → 또 새 HikariDataSource 생성
→ DataSource가 2개! (의도와 다름)
CGLIB 프록시 적용 시:
AppConfig$$EnhancerBySpringCGLIB$$xxx
dataSource() 호출 시:
① 빈 컨테이너에 "dataSource" 빈이 있는가?
② 있으면 → 기존 빈 반환
③ 없으면 → 원본 메서드 호출 → 빈 등록 → 반환
→ DataSource는 항상 1개 (싱글톤 보장)
@Configuration(proxyBeanMethods = false)— “Lite Mode”로 CGLIB 프록시를 사용하지 않는다.@Bean메서드 간 호출 시 싱글톤이 보장되지 않지만, 시작 속도가 빨라진다. 자동 구성 클래스에서 주로 사용된다.
6. 내장 웹 서버 (Embedded Tomcat)
6.1 전통적 배포 vs 내장 서버
1
2
3
4
5
6
7
전통적 배포:
Tomcat 설치 → WAR 파일 배포 → Tomcat이 WAR를 로드
Tomcat이 Spring을 호출하는 구조
Spring Boot:
JAR 파일에 Tomcat이 내장됨 → java -jar app.jar
Spring이 Tomcat을 호출하는 구조 (제어 역전!)
6.2 내장 톰캣 시작 과정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
내장 Tomcat 시작:
① ServletWebServerFactoryAutoConfiguration 자동 구성
│
② @ConditionalOnClass(Tomcat.class) → spring-boot-starter-web에 포함
│
③ TomcatServletWebServerFactory 빈 등록
│
④ ApplicationContext.onRefresh()에서 WebServer 생성
│
⑤ Tomcat 인스턴스 생성
├── Connector 설정 (포트, 프로토콜)
├── Engine → Host → Context 생성
└── DispatcherServlet 등록
│
⑥ Tomcat.start()
│
⑦ "Tomcat started on port 8080" 로그 출력
1
2
3
4
5
6
7
8
9
10
11
12
// 내장 서버 커스터마이징
@Component
public class TomcatCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
@Override
public void customize(TomcatServletWebServerFactory factory) {
factory.setPort(9090);
factory.addConnectorCustomizers(connector -> {
connector.setMaxPostSize(10 * 1024 * 1024); // 10MB
});
}
}
7. 순환 참조 (Circular Dependency)
7.1 순환 참조란
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
public class ServiceA {
private final ServiceB serviceB;
public ServiceA(ServiceB serviceB) {
this.serviceB = serviceB; // ServiceB 필요
}
}
@Service
public class ServiceB {
private final ServiceA serviceA;
public ServiceB(ServiceA serviceA) {
this.serviceA = serviceA; // ServiceA 필요 → 무한 루프!
}
}
1
2
3
4
5
6
7
순환 참조 발생 과정:
ServiceA 생성하려면 → ServiceB 필요
ServiceB 생성하려면 → ServiceA 필요
ServiceA 생성하려면 → ServiceB 필요
...
→ BeanCurrentlyInCreationException!
7.2 Spring Boot 2.6+ 기본 동작
Spring Boot 2.6부터 순환 참조가 기본적으로 금지된다. 이전에는 세터 주입의 경우 3단계 캐시를 통해 해결했지만, 이 동작이 제거되었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Spring의 3단계 캐시 (레거시, 참고용):
1차 캐시 (singletonObjects): 완전히 초기화된 빈
2차 캐시 (earlySingletonObjects): 초기화 중인 빈 (프록시 가능)
3차 캐시 (singletonFactories): 빈 팩토리 (ObjectFactory)
세터 주입에서의 순환 참조 해결:
① A 생성 (생성자만, DI 전) → 3차 캐시에 A의 Factory 등록
② A의 의존성 주입 → B 필요
③ B 생성 → 3차 캐시에 B의 Factory 등록
④ B의 의존성 주입 → A 필요 → 3차 캐시에서 A 조기 참조 획득!
⑤ B 초기화 완료
⑥ A의 의존성 주입 완료 (B 주입)
⑦ A 초기화 완료
7.3 순환 참조 해결 방법
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 해결법 1: 설계 변경 (가장 좋은 방법)
// → 공통 로직을 별도 서비스로 분리
@Service
public class ServiceA {
private final CommonService commonService; // A, B 공통 로직 분리
}
@Service
public class ServiceB {
private final CommonService commonService;
}
// 해결법 2: @Lazy
@Service
public class ServiceA {
private final ServiceB serviceB;
public ServiceA(@Lazy ServiceB serviceB) {
// ServiceB의 프록시가 주입됨
// 실제 ServiceB는 처음 사용할 때 생성
this.serviceB = serviceB;
}
}
// 해결법 3: 이벤트 기반 (가장 느슨한 결합)
@Service
public class ServiceA {
private final ApplicationEventPublisher eventPublisher;
public void doSomething() {
// ServiceB를 직접 호출하지 않고 이벤트 발행
eventPublisher.publishEvent(new SomethingHappenedEvent(this));
}
}
@Service
public class ServiceB {
@EventListener
public void handleEvent(SomethingHappenedEvent event) {
// 이벤트를 받아서 처리
}
}
8. 프로파일 (Profile)과 외부 설정
8.1 프로파일
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
@Profile("production")
public class ProductionConfig {
@Bean
public DataSource dataSource() {
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl("jdbc:mysql://prod-db:3306/app");
return ds;
}
}
@Configuration
@Profile("local")
public class LocalConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.build();
}
}
8.2 프로퍼티 우선순위
Spring Boot의 외부 설정은 17단계의 우선순위를 가진다. 높은 순위가 낮은 순위를 오버라이드한다.
1
2
3
4
5
6
7
8
9
10
프로퍼티 우선순위 (높은 → 낮은, 주요 항목):
① 커맨드라인 인수 (--server.port=9090)
② SPRING_APPLICATION_JSON (환경변수의 JSON)
③ 서블릿 init 파라미터
④ OS 환경변수
⑤ application-{profile}.yml (프로파일별)
⑥ application.yml (기본)
⑦ @PropertySource
⑧ SpringApplication.setDefaultProperties()
9. 면접에서 자주 나오는 Spring Boot 동작 원리 질문
Q1: @SpringBootApplication은 내부적으로 무엇을 하나요?
@SpringBootConfiguration(설정 클래스 선언), @EnableAutoConfiguration(자동 구성 활성화), @ComponentScan(컴포넌트 스캔) 세 가지 어노테이션의 합성입니다. @ComponentScan은 메인 클래스 패키지 하위의 @Component를 찾아 빈으로 등록하고, @EnableAutoConfiguration은 spring.factories 또는 AutoConfiguration.imports 파일에서 자동 구성 클래스를 로드하여 @Conditional 조건을 평가한 뒤 필요한 빈을 등록합니다.
Q2: 자동 구성(Auto-Configuration)의 동작 원리를 설명하세요.
Spring Boot는 spring.factories(2.x) 또는 AutoConfiguration.imports(3.x) 파일에 등록된 100개 이상의 자동 구성 클래스를 로드합니다. 각 클래스는 @ConditionalOnClass, @ConditionalOnMissingBean 등의 조건 어노테이션이 붙어 있어, 클래스패스에 특정 라이브러리가 있고 사용자가 직접 빈을 등록하지 않았을 때만 자동으로 빈을 생성합니다. 사용자 빈이 항상 자동 구성보다 우선하므로, 필요하면 커스터마이징이 가능합니다.
Q3: BeanFactoryPostProcessor와 BeanPostProcessor의 차이는?
BeanFactoryPostProcessor는 빈이 생성되기 전에 BeanDefinition(빈의 메타데이터)을 수정합니다. PropertySourcesPlaceholderConfigurer가 대표적으로, ${} 플레이스홀더를 실제 값으로 치환합니다. BeanPostProcessor는 빈 인스턴스가 생성된 후에 해당 인스턴스를 가공합니다. @Autowired 처리, @PostConstruct 처리, AOP 프록시 생성 등이 모두 BeanPostProcessor에서 일어납니다. 시점의 차이가 핵심입니다.
Q4: 빈의 생명주기를 설명하세요.
빈 정의 등록 → BeanFactoryPostProcessor → 인스턴스 생성(생성자) → 의존성 주입 → BeanPostProcessor.beforeInit → @PostConstruct → InitializingBean.afterPropertiesSet → BeanPostProcessor.afterInit(프록시 생성) → 사용 → @PreDestroy → DisposableBean.destroy 순서입니다. AOP 프록시가 afterInit에서 생성되므로, @PostConstruct 시점에는 아직 프록시가 아닌 원본 빈입니다.
Q5: @Configuration의 CGLIB 프록시는 왜 필요한가요?
@Configuration 클래스 내에서 @Bean 메서드가 다른 @Bean 메서드를 호출할 때 싱글톤을 보장하기 위해서입니다. CGLIB 프록시가 @Bean 메서드 호출을 가로채서, 이미 빈이 등록되어 있으면 기존 빈을 반환합니다. 프록시 없이는 메서드 호출마다 새 인스턴스가 생성되어 싱글톤이 깨집니다. Spring Boot 3.x에서는 proxyBeanMethods=false(Lite Mode)를 자동 구성 클래스에 많이 적용하여 시작 속도를 개선합니다.
Q6: 순환 참조란 무엇이고, 어떻게 해결하나요?
빈 A가 빈 B를 의존하고, 빈 B가 빈 A를 의존하는 상태입니다. 생성자 주입에서는 양쪽 다 생성할 수 없어 예외가 발생합니다. 해결 방법은 세 가지입니다. 첫째, 설계를 변경하여 공통 로직을 별도 서비스로 분리합니다(가장 좋은 방법). 둘째, @Lazy로 한쪽을 지연 로딩합니다. 셋째, 이벤트 기반으로 결합도를 낮춥니다. Spring Boot 2.6부터 순환 참조가 기본 금지되었습니다.
Q7: Spring Boot Starter는 코드인가요?
아닙니다. Starter는 관련된 의존성들을 묶어놓은 편의 모듈입니다. spring-boot-starter-web은 Spring MVC, Jackson, Embedded Tomcat 의존성을 포함합니다. 이 의존성들이 클래스패스에 들어오면 자동 구성 클래스의 @ConditionalOnClass 조건이 충족되어 관련 빈이 자동 등록됩니다. Starter 자체에는 자동 구성 코드가 없고, spring-boot-autoconfigure 모듈에 있습니다.
Q8: 내장 톰캣은 어떻게 시작되나요?
ServletWebServerFactoryAutoConfiguration이 @ConditionalOnClass(Tomcat.class) 조건이 충족되면 TomcatServletWebServerFactory 빈을 등록합니다. ApplicationContext의 refresh() 과정 중 onRefresh() 단계에서 이 팩토리를 사용하여 Tomcat 인스턴스를 생성하고, DispatcherServlet을 등록한 뒤 start()합니다. 전통적 배포와 달리 Spring이 Tomcat을 호출하는 구조(제어 역전)입니다.
Q9: Singleton 빈에서 Prototype 빈을 주입하면 어떤 문제가 있나요?
Singleton 빈은 한 번만 생성되므로, 생성 시점에 Prototype 빈도 한 번만 주입됩니다. 이후 Singleton 빈이 사용하는 Prototype 빈은 항상 같은 인스턴스입니다. 해결하려면 ObjectProvider
Q10: spring.factories와 AutoConfiguration.imports의 차이는?
spring.factories는 Spring Boot 2.x까지 자동 구성 클래스 목록을 저장하던 파일(META-INF/spring.factories)입니다. Spring Boot 3.0부터 자동 구성 전용 파일인 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports로 분리되었습니다. 클래스명을 한 줄에 하나씩 나열하는 방식으로 단순화되었고, spring.factories보다 로딩 성능이 개선되었습니다.
정리
Spring Boot의 동작 원리를 정리하면 다음과 같다.
@SpringBootApplication은 @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan의 합성이다. ComponentScan이 사용자 빈을 등록하고, EnableAutoConfiguration이 자동 구성 빈을 등록한다.
자동 구성은 @Conditional 어노테이션을 통해 “클래스패스에 특정 라이브러리가 있고, 사용자가 직접 빈을 등록하지 않았을 때”만 빈을 자동 등록한다. 사용자 빈은 항상 자동 구성보다 우선한다.
빈 생명주기는 BeanDefinition 등록 → BeanFactoryPostProcessor → 인스턴스 생성 → DI → BeanPostProcessor(before) → 초기화 콜백 → BeanPostProcessor(after, 프록시 생성) → 사용 → 소멸 콜백의 흐름이다. Spring의 핵심 마법(AOP 프록시, @Autowired, @PostConstruct 등)은 모두 BeanPostProcessor에서 일어난다.
ApplicationContext.refresh()가 이 모든 과정을 조율하는 메서드이다. BeanFactoryPostProcessor 실행, BeanPostProcessor 등록, 싱글톤 빈 생성, 이벤트 발행까지 컨테이너 초기화의 전 과정이 여기에 집약되어 있다.