이 글에서 다루는 것
"Spring 소스 파헤치기"의 두 번째 글입니다. 1편에서는 Spring이 무엇인지, 소스를 왜 읽고 어떤 순서로 읽을지를 이야기했습니다. 이번 글은 코드를 한 줄씩 읽기 전에 펴 놓는 전체 지도입니다.
Spring Framework 저장소를 내려받아 열면 spring-으로 시작하는 폴더가 스무 개 넘게 나옵니다. 이름만 봐서는 어디가 중심이고 어디가 변두리인지 알 수 없습니다. 이 글을 끝까지 읽으면 다음을 알게 됩니다.
- Spring Framework가 어떤 모듈로 나뉘어 있고, 각 모듈이 한 줄로 무슨 일을 하는지
- 모듈끼리 누가 누구에게 기대고 있는지, 그리고 그 관계가 어디에 적혀 있는지
- 앞으로 계속 나올
spring-core,spring-beans,spring-context,spring-aop안에 무엇이 들어 있는지 - 컨테이너가 켜지고 빈이 만들어지는 과정의 큰 그림
- 트랜잭션, 캐시 같은 기능들이 공통으로 되풀이하는 세 조각
뒤의 글에서 "이 클래스는 spring-beans에 있습니다"라고 하면, 그곳이 지도의 어디쯤이고 무엇을 하는 곳인지 바로 떠올릴 수 있게 하는 것이 목표입니다.
| 항목 | 내용 |
|---|---|
| 읽는 대상 | Spring Framework 7.1.0-SNAPSHOT |
| 커밋 | f447f3c310 (2026-10-01, main 브랜치) |
| 확인한 방법 | 모듈 목록은 settings.gradle, 의존 관계는 모듈마다 있는 빌드 파일, 파일 수는 직접 세어서 확인 |
다른 버전에서는 줄 번호와 세부 내용이 다를 수 있습니다. 같은 커밋의 소스를 받는 방법은 1편에 있습니다.
먼저 알아 둘 말 다섯 가지
모듈(module)은 따로 빌드되어 따로 배포되는 코드 묶음입니다. Spring Framework는 큰 덩어리 하나가 아니라 spring-core, spring-beans 같은 모듈 여러 개의 모음이고, 모듈 하나가 jar 파일(자바 클래스를 묶은 압축 파일) 하나가 됩니다. 그래서 쓰는 쪽은 필요한 모듈만 골라 가져올 수 있습니다.
패키지(package)는 모듈 안에서 클래스를 나누어 담는 자바의 폴더입니다. org.springframework.beans.factory 같은 이름입니다. 이 글에서는 앞의 org.springframework.를 떼고 beans.factory처럼 줄여 씁니다.
의존(dependency)은 "가져다 쓴다"는 뜻입니다. A 모듈의 코드가 B 모듈의 클래스를 쓰면 "A가 B에 의존한다"고 하고, 그림에서는 A → B로 그립니다. B가 없으면 A는 컴파일되지 않습니다.
빈(Bean)과 컨테이너. Spring이 대신 만들어서 관리해 주는 객체를 빈이라고 하고, 빈을 만들고 서로 연결하고 보관하는 것을 컨테이너라고 합니다. 컨테이너가 빈에게 필요한 다른 빈을 넣어 주는 일이 의존성 주입(DI)입니다. 자세한 설명은 1편에 있습니다.
프록시(proxy)와 AOP. 프록시는 진짜 객체를 감싸고 호출을 대신 받는 대리 객체입니다. 호출을 먼저 받기 때문에 진짜 메서드가 실행되기 앞뒤에 다른 일을 끼워 넣을 수 있습니다. 이 방법으로 "트랜잭션을 시작하고 끝낸다" 같은 공통 작업을 여러 메서드에 한꺼번에 입히는 기법을 AOP(관점 지향 프로그래밍)라고 합니다.
1. 저장소를 열면 보이는 것: 모듈 22개
Spring Framework는 Gradle이라는 빌드 도구(소스를 컴파일해서 jar로 묶어 주는 프로그램)를 씁니다. 저장소 맨 위에 있는 settings.gradle이 "이 저장소에는 이런 모듈이 있다"를 적어 둔 목록입니다.
include "spring-aop"
include "spring-aspects"
include "spring-beans"
include "spring-context"
// ...
include "spring-webmvc"
include "spring-websocket"
include "framework-api"
include "framework-bom"
include "framework-docs"
include "framework-platform"
include "integration-tests"
rootProject.name = "spring"
rootProject.children.each {project ->
project.buildFileName = "${project.name}.gradle"
}
여기서 세 가지를 읽을 수 있습니다.
spring-으로 시작하는 줄이 22개입니다. 이것이 Spring Framework 그 자체이고, 저장소 맨 위의 폴더 이름과 하나씩 맞습니다.- 나머지 다섯 개(
framework-api,framework-bom,framework-docs,framework-platform,integration-tests)는 문서, 버전 목록, 통합 테스트입니다. 프레임워크의 동작을 읽을 때는 보지 않아도 됩니다. - 마지막 세 줄 때문에 모듈의 빌드 파일 이름이 흔히 보는
build.gradle이 아니라<모듈 이름>.gradle입니다. 예를 들어spring-context모듈의 빌드 파일은spring-context/spring-context.gradle입니다. 3장에서 이 파일을 열어 봅니다.
모듈 폴더 안의 모양은 모두 같습니다. spring-beans/src/main/java/org/springframework/beans/…처럼 src/main/java 아래에 실제 소스가 있고, src/test 아래에 테스트가 있습니다. 이 시리즈가 읽는 것은 src/main/java입니다.
settings.gradle 한 파일에서 갈라지는 저장소의 모양. 초록 칸이 이 시리즈가 읽는 곳, 회색 칸은 건너뛰어도 되는 곳입니다.
2. 한 장으로 보는 네 층
22개를 한꺼번에 외울 필요는 없습니다. 먼저 층으로 나누어 봅니다.
화살표는 "위 층이 아래 층을 가져다 쓴다"는 뜻입니다. 테두리가 굵은 가운데 층이 이 시리즈가 먼저 읽는 곳입니다.
- 바탕 — spring-core. 타입 정보 읽기, 파일 같은 자원 다루기, 설정값, 각종 유틸리티가 있습니다. 다른 Spring 모듈에 하나도 의존하지 않습니다.
- 컨테이너와 AOP — spring-beans, spring-aop, spring-expression, spring-context. 빈을 만들고 연결하는 컨테이너, 그리고 프록시가 여기 있습니다. Spring의 심장입니다.
- 데이터 — spring-tx, spring-jdbc, spring-orm, spring-r2dbc. 트랜잭션과 데이터베이스 접근입니다.
- 웹 — spring-web, spring-webmvc, spring-webflux, spring-websocket. HTTP 요청을 받아 처리합니다.
중요한 것은 방향입니다. 위 층은 아래 층을 알지만 아래 층은 위 층을 모릅니다. 가운데 층의 네 모듈은 데이터나 웹 층의 모듈을 하나도 가져다 쓰지 않습니다. 그래서 가운데 층만 떼어서 읽을 수 있고, 가운데 층을 알고 나면 위 층은 "그 위에 무엇을 더 얹었나"만 보면 됩니다.
3. 의존 관계는 어디에 적혀 있나
위 그림의 화살표는 짐작이 아니라 빌드 파일에 적혀 있는 내용입니다. 직접 확인해 봅니다.
빌드 파일의 api와 optional
spring-context 모듈의 빌드 파일에서 다른 모듈을 가리키는 부분입니다.
dependencies {
api(project(":spring-aop"))
api(project(":spring-beans"))
api(project(":spring-core"))
api(project(":spring-expression"))
// ...
optional(project(":spring-instrument"))
// ...
optional("jakarta.annotation:jakarta.annotation-api")
// ...
}
spring-context/spring-context.gradle 9–19행
project(":spring-aop")은 같은 저장소 안의 spring-aop 모듈을 가리킵니다. 앞에 붙은 말이 두 가지입니다.
| 적힌 말 | 뜻 | 이 글의 그림 |
|---|---|---|
api(...) |
반드시 필요하다. spring-context를 가져다 쓰는 쪽에도 함께 딸려 간다 |
실선 |
optional(...) |
컴파일할 때는 쓰지만, 가져다 쓰는 쪽에 딸려 가지 않는다. 있으면 쓰고 없으면 그 기능만 빠진다 | 점선 |
optional은 Gradle에 원래 있는 말이 아니라 Spring이 자기 빌드에 직접 만들어 넣은 것입니다. 정의는 OptionalDependenciesPlugin.java에 있고, 주석에 "프로젝트 자신의 컴파일·런타임 클래스패스에는 들어가지만, 이 프로젝트에 의존하는 쪽의 클래스패스에는 영향을 주지 않는다"고 적혀 있습니다.
중심 축: 다섯 모듈
가운데 층과 바탕을 모듈 단위로 풀면 이렇게 됩니다.
A → B는 "A가 B를 가져다 쓴다"입니다. 모두 api 의존이고, 다른 화살표를 따라가면 닿는 것은 그리지 않았습니다(예: spring-context는 spring-beans와 spring-core도 직접 선언합니다).
아래에서 위로 읽으면 쌓이는 순서가 보입니다.
- spring-core가 맨 아래입니다. 다른 모듈을 가리키는 줄이 빌드 파일에 하나도 없습니다.
- spring-beans는 spring-core만 씁니다. 컨테이너의 핵심이 여기 있습니다.
- spring-aop는 spring-beans 위에 올라갑니다. 프록시를 만들려면 감쌀 빈이 있어야 하기 때문입니다.
- spring-expression은 spring-core만 씁니다. spring-beans와는 상관없이 따로 서 있는 모듈입니다.
- spring-context가 이 넷을 모두 모읍니다. 우리가 보통 "Spring 컨테이너"라고 부르는
ApplicationContext가 여기 있습니다.
축 위에 얹히는 모듈들
데이터와 웹 층의 모듈 몇 개를 같은 방법으로 그리면 이렇습니다.
실선은 api, 점선은 optional입니다. 그린 화살표는 모두 빌드 파일에서 확인한 것이고, 읽기 좋게 일부는 생략했습니다(예: 거의 모든 모듈이 spring-core를 직접 선언합니다).
- 데이터 쪽은 한 줄로 쌓입니다.
spring-orm → spring-jdbc → spring-tx → spring-beans. - 웹 쪽은
spring-web이 공통 바탕이고, 그 위에spring-webmvc와spring-webflux가 나란히 올라갑니다. 공통 바탕은 20편, MVC는 21편, WebFlux는 24편에서 읽습니다. spring-tx와spring-web은spring-beans에는 실선으로,spring-context에는 점선으로 이어져 있습니다.spring-webmvc는spring-context를 실선(api)으로,spring-webflux는 점선(optional)으로 가리킵니다.
헷갈리기 쉬운 곳: 점선은 "없어도 된다"는 뜻이다
"트랜잭션을 쓰려면 당연히 spring-context가 있어야 하지 않나?"라고 생각하기 쉽습니다. 빌드 파일은 다르게 말합니다.
dependencies {
api(project(":spring-beans"))
api(project(":spring-core"))
optional(project(":spring-aop"))
optional(project(":spring-context")) // for JCA, @EnableTransactionManagement
// ...
}
spring-tx/spring-tx.gradle 5–9행
spring-tx가 반드시 필요로 하는 것은 spring-beans와 spring-core뿐입니다. spring-context는 주석에 적힌 대로 JCA와 @EnableTransactionManagement를 위해서만 쓰입니다. 모듈을 잘게 나누고 꼭 필요한 것만 실선으로 묶어 두었기 때문에, 쓰는 쪽이 필요 없는 모듈까지 떠안지 않습니다.
그러면 코드에서는 "있을 수도 있고 없을 수도 있는 것"을 어떻게 다룰까요. 그 클래스가 지금 있는지 먼저 물어봅니다. 앞의 spring-context 빌드 파일에 optional("jakarta.annotation:jakarta.annotation-api")가 있었는데, 그것을 쓰는 코드가 이렇게 생겼습니다.
private static final boolean JAKARTA_ANNOTATIONS_PRESENT =
ClassUtils.isPresent("jakarta.annotation.PostConstruct", classLoader);
spring-context/…/context/annotation/AnnotationConfigUtils.java 119–120행
ClassUtils.isPresent는 "이 이름의 클래스를 불러올 수 있는가"를 true/false로 알려 줍니다. 소스를 읽다가 이런 줄을 만나면 "선택 의존을 다루는 곳이구나" 하고 넘어가면 됩니다.
4. 모듈 22개 한 줄 정의
이제 전체 목록입니다. 파일 수는 각 모듈의 src/main/java 아래에 있는 .java 파일을 체크아웃에서 직접 센 것입니다(package-info.java 포함, 테스트와 Kotlin 소스 제외).
| 모듈 | 하는 일 | 파일 수 |
|---|---|---|
| spring-core | 타입·어노테이션 정보 읽기, 자원(파일 등), 환경(설정값), 타입 변환, 유틸리티 | 738 |
| spring-beans | BeanFactory(컨테이너의 핵심), BeanDefinition(빈의 설계도), 의존성 주입 |
338 |
| spring-aop | 프록시로 하는 AOP | 226 |
| spring-expression | SpEL. 문자열로 적은 식을 실행 중에 계산하는 작은 언어 | 123 |
| spring-context | ApplicationContext, @Configuration·@Bean·@Component 같은 어노테이션 설정, 이벤트, 그리고 캐시·스케줄링·검증 같은 부가 기능 |
625 |
| spring-context-support | 외부 라이브러리 연결(Caffeine·JCache 캐시, 메일, Quartz, FreeMarker) | 87 |
| spring-tx | 트랜잭션과 @Transactional, 데이터 접근 예외 |
173 |
| spring-jdbc | JdbcTemplate 등 JDBC를 편하게 쓰는 도구 |
253 |
| spring-orm | JPA(Hibernate 포함) 연결 | 65 |
| spring-r2dbc | 리액티브 방식의 데이터베이스 접근 | 65 |
| spring-web | HTTP를 다루는 공통 바탕. RestClient 같은 HTTP 클라이언트도 여기 |
792 |
| spring-webmvc | DispatcherServlet이 중심인 서블릿 방식 웹 프레임워크(Spring MVC) |
363 |
| spring-webflux | DispatcherHandler가 중심인 리액티브 방식 웹 프레임워크, WebClient |
276 |
| spring-websocket | WebSocket, SockJS | 170 |
| spring-messaging | Message 추상화, STOMP 메시징 |
245 |
| spring-jms | JMS 메시징(JmsTemplate) |
122 |
| spring-oxm | 객체와 XML 사이의 변환 | 28 |
| spring-test | 테스트 지원(TestContext 프레임워크, MockMvc 등) |
460 |
| spring-aspects | AspectJ로 작성한 애스펙트. 프록시 방식인 spring-aop와는 다른 것 | 14 |
| spring-instrument | JVM 에이전트 클래스 하나(InstrumentationSavingAgent) |
2 |
| spring-context-indexer | 컴파일할 때 컴포넌트 목록을 미리 만드는 도구. 6.1부터 폐기 예정(deprecated) 표시 | 13 |
| spring-core-test | Spring 자체 테스트에 쓰는 도구 | 45 |
"리액티브"는 요청 하나에 스레드 하나를 붙잡아 두지 않고 비동기로 처리하는 방식이라고만 알아 두면 됩니다.
모두 더하면 5,223개 파일, 약 84만 줄입니다(줄 수는 같은 파일들에 wc -l을 돌린 값). 가장 큰 것은 spring-web(약 13만 2천 줄)과 spring-core(약 12만 8천 줄)입니다. 컨테이너의 핵심인 spring-beans는 338개 파일, 약 6만 4천 줄입니다. 양으로 보면 전체의 일부지만 나머지 모듈이 모두 이 위에 서 있습니다.
위 표의 파일 수를 막대로 옮긴 것입니다. 컨테이너의 핵심인 spring-beans는 여섯 번째입니다.
5. 핵심 네 모듈 안에는 무엇이 있나
다음 글들이 계속 드나들 네 모듈만 한 겹 더 들어가 봅니다. 클래스 이름을 지금 외울 필요는 없습니다. "이런 이름은 이쯤에 있다"는 감만 잡으면 됩니다.
spring-core: 모두가 쓰는 도구 상자
| 패키지 | 대표 클래스 | 무엇을 하나 |
|---|---|---|
core |
ResolvableType, MethodParameter, Ordered |
제네릭을 포함한 타입 정보, 순서 매기기 |
core.annotation |
MergedAnnotations, AnnotationUtils, @AliasFor |
어노테이션 읽기 |
core.io |
Resource, ResourceLoader |
파일이든 클래스패스든 같은 방식으로 자원 읽기 |
core.env |
Environment, PropertySource, Profiles |
설정값과 프로파일 |
core.convert |
ConversionService |
타입 변환(문자열 → 숫자 등) |
util |
ClassUtils, ReflectionUtils, StringUtils, Assert |
유틸리티 (16편) |
asm, cglib |
— | 외부 라이브러리를 옮겨 담은 것(아래 설명) |
spring-beans: 컨테이너의 핵심
| 패키지 | 대표 클래스 | 무엇을 하나 |
|---|---|---|
beans.factory |
BeanFactory, ListableBeanFactory, FactoryBean, ObjectProvider |
컨테이너를 쓰는 쪽이 보는 인터페이스 |
beans.factory.config |
BeanDefinition, BeanPostProcessor, BeanFactoryPostProcessor, Scope |
빈의 설계도와, 컨테이너에 끼어드는 확장 지점 |
beans.factory.support |
DefaultListableBeanFactory, AbstractAutowireCapableBeanFactory, DefaultSingletonBeanRegistry |
실제로 일하는 구현 클래스 |
beans.factory.annotation |
@Autowired, @Qualifier, @Value, AutowiredAnnotationBeanPostProcessor |
어노테이션으로 하는 주입 |
beans |
BeanWrapper, BeanUtils |
객체의 프로퍼티를 읽고 쓰기 |
beans.factory.xml |
— | XML 설정 파일 읽기 |
3편은 위의 세 줄(beans.factory, .config, .support)을, 4편은 beans.factory.support 안에서 빈이 만들어지는 과정을 읽습니다.
spring-context: 우리가 실제로 쓰는 컨테이너
| 패키지 | 대표 클래스 | 무엇을 하나 |
|---|---|---|
context |
ApplicationContext, ConfigurableApplicationContext, ApplicationEvent, Lifecycle |
컨테이너의 인터페이스, 이벤트 |
context.support |
AbstractApplicationContext, GenericApplicationContext |
컨테이너의 구현. 기동 순서가 여기 있다 |
context.annotation |
AnnotationConfigApplicationContext, @Configuration, @Bean, @ComponentScan, @Import, ConfigurationClassPostProcessor |
어노테이션으로 하는 설정 |
stereotype |
@Component, @Service, @Repository, @Controller |
"이 클래스를 빈으로 등록해 달라"는 표시 |
context.event |
@EventListener, ApplicationEventMulticaster |
이벤트 전달 |
cache, scheduling, validation, resilience 등 |
@Cacheable, @Scheduled, @Async, Validator, @Retryable |
컨테이너 위에 얹은 부가 기능 |
ApplicationContext는 BeanFactory를 대신하는 다른 물건이 아니라 BeanFactory에 기능을 더한 것입니다. 선언부를 보면 ApplicationContext가 ListableBeanFactory를 상속하고 있습니다(ApplicationContext.java 59–60행). 2장의 층 그림에서 spring-context가 spring-beans 위에 있는 이유입니다.
파란 칸은 인터페이스가 모인 패키지, 초록 칸은 그 구현이 모인 support 패키지입니다. 구현 관계는 바로 위가 아니라 중간 인터페이스를 거치기도 합니다(예: DefaultListableBeanFactory는 ConfigurableListableBeanFactory를 통해 ListableBeanFactory를 구현합니다).
spring-aop: 프록시
| 패키지 | 대표 클래스 | 무엇을 하나 |
|---|---|---|
org.aopalliance |
Advice, MethodInterceptor, MethodInvocation |
메서드 호출을 가로채는 표준 인터페이스 |
aop |
Pointcut, Advisor, TargetSource |
"어디에"(Pointcut) "무엇을"(Advice) 끼워 넣을지의 기본 개념 |
aop.framework |
ProxyFactory, JdkDynamicAopProxy, CglibAopProxy |
프록시를 실제로 만드는 곳 |
aop.framework.autoproxy |
AbstractAutoProxyCreator |
빈이 만들어질 때 자동으로 프록시를 씌우는 곳 |
aop.aspectj |
— | AspectJ 표현식과 @Aspect 지원 |
헷갈리기 쉬운 곳: 이름과 위치가 어긋나는 것들
- 어노테이션이 사는 모듈이 제각각입니다.
@Autowired는 spring-beans(beans.factory.annotation)에,@Component·@Service는 spring-context(stereotype)에,@Configuration·@Bean은 spring-context(context.annotation)에,@Transactional은 spring-tx(transaction.annotation)에 있습니다. "주입"은 컨테이너의 핵심 기능이라 아래쪽에, "클래스를 훑어서 빈으로 등록"은 더 위쪽의 기능이라 위쪽에 있는 것입니다. support가 붙은 패키지는 구현이 모인 곳입니다.beans.factory에는 인터페이스가,beans.factory.support에는 그것을 구현한 클래스가 있습니다.context와context.support도 같은 관계입니다. 인터페이스를 보다가 "실제로는 누가 하지?" 싶으면 옆의support를 열면 됩니다.- spring-core 안의
asm,cglib는 Spring이 쓴 코드가 아닙니다. 바이트코드를 다루는 외부 라이브러리 ASM과 CGLIB를 패키지 이름만 바꿔 옮겨 담은 것입니다. 패키지 설명에 "Spring's repackaging of ASM 9.x (with Spring-specific patches; for internal use only)"라고 적혀 있습니다. 두 패키지를 합쳐 174개 파일인데, 읽을 대상에서 빼도 됩니다. - spring-aop 안에는
org.springframework로 시작하지 않는 패키지가 있습니다.org.aopalliance입니다. 여러 AOP 구현이 함께 쓰려고 만든 표준 인터페이스가 이 모듈에 같이 들어 있습니다. - spring-aop와 spring-aspects는 다른 것입니다. spring-aop는 실행 중에 프록시를 만들어 감싸는 방식이고, spring-aspects는 AspectJ라는 별도 도구로 클래스 자체에 코드를 짜 넣는 방식을 위한 모듈입니다. 이 시리즈가 읽는 것은 spring-aop입니다.
6. 컨테이너가 켜질 때 일어나는 일
모듈이 어디 있는지 알았으니, 이번에는 시간 순서로 봅니다. 아래는 Spring을 쓰는 가장 작은 프로그램입니다.
// 이 글을 위해 쓴 예시입니다 (Spring의 소스가 아닙니다)
@Configuration
class AppConfig {
@Bean
GreetingService greetingService() {
return new GreetingService();
}
}
class GreetingService {}
public class Main {
public static void main(String[] args) {
var context = new AnnotationConfigApplicationContext(AppConfig.class);
GreetingService service = context.getBean(GreetingService.class);
context.close();
}
}
@Configuration, @Bean, AnnotationConfigApplicationContext는 모두 spring-context의 context.annotation 패키지에 있고, getBean은 spring-beans의 BeanFactory에 선언된 메서드입니다. 지도 위에 놓으면 이 짧은 프로그램은 spring-context와 그 아래의 spring-beans를 함께 쓰고 있는 것입니다.
new AnnotationConfigApplicationContext(AppConfig.class) 한 줄이 컨테이너를 켭니다. 생성자를 열어 보면 세 줄입니다.
public AnnotationConfigApplicationContext(Class<?>... componentClasses) {
this();
register(componentClasses);
refresh();
}
spring-context/…/context/annotation/AnnotationConfigApplicationContext.java 89–93행
넘겨받은 클래스를 등록하고(register), refresh()를 부릅니다. 컨테이너 기동의 거의 모든 일이 이 refresh() 안에서 일어납니다. 메서드 안에는 열두 단계가 주석과 함께 차례로 적혀 있는데, 그 가운데 이 글에 필요한 줄만 남기면 이렇습니다.
public void refresh() throws BeansException, IllegalStateException {
// ...
// Prepare this context for refreshing.
prepareRefresh();
// ...
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
// ...
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
// ...
}
spring-context/…/context/support/AbstractApplicationContext.java 582–662행
링크를 열어 전체를 보면 차례표처럼 읽힙니다. 열두 단계를 지금 다 알 필요는 없고, 크게 네 덩어리로 묶어서 보면 됩니다(열두 단계를 하나씩 따라가는 것은 7편에서 합니다).
컨테이너가 켜져서 꺼질 때까지의 큰 흐름. ③ 안의 세 칸은 빈 하나가 만들어지는 과정이고, 빈마다 되풀이됩니다.
- 정의 수집. 컨테이너는 객체를 바로 만들지 않습니다. 먼저 "어떤 빈을 어떻게 만들지"를 적은 설계도를 모읍니다. 이 설계도가
BeanDefinition입니다(3편에서 자세히 봅니다). 위 예시에서는register가AppConfig의 설계도를 등록하고,AppConfig안의@Bean메서드는invokeBeanFactoryPostProcessors단계에서 읽혀 설계도로 추가됩니다. - 정의 가공. 설계도가 모이면 객체를 만들기 전에 설계도를 고치거나 더할 기회를 줍니다. 이 일을 하는 것이
BeanFactoryPostProcessor이고,refresh()에서는invokeBeanFactoryPostProcessors입니다. 바로 다음 줄registerBeanPostProcessors는 ③에서 쓸BeanPostProcessor들을 미리 준비해 둡니다. - 빈 생성.
finishBeanFactoryInitialization이 설계도대로 빈을 실제로 만듭니다. 소스의 주석대로 지연 초기화(lazy-init)가 아닌 싱글톤(컨테이너에 하나만 만들어 두고 함께 쓰는 빈)이 대상입니다. 빈 하나는 인스턴스화(객체를 만든다) → 주입(필요한 다른 빈을 넣는다) → 초기화(초기화 메서드를 부르고, 필요하면 프록시로 감싼다)의 세 걸음으로 만들어집니다. 이 세 걸음이 AbstractAutowireCapableBeanFactory의 doCreateBean에createBeanInstance,populateBean,initializeBean이라는 이름으로 차례로 나옵니다. 4편의 주제입니다. - 운영과 종료.
finishRefresh가 "준비가 끝났다"는 이벤트(ContextRefreshedEvent)를 알리면 기동이 끝납니다. 나중에close()를 부르면 종료 이벤트를 알리고 빈을 없앱니다.
헷갈리기 쉬운 곳
BeanFactoryPostProcessor와BeanPostProcessor는 이름이 비슷하지만 다루는 대상이 다릅니다. 앞의 것은 설계도(BeanDefinition)를 다루고 ②에서 일합니다. 뒤의 것은 만들어지고 있는 빈 객체를 다루고 ③에서 일합니다. 이 둘을 구별해 두면 뒤의 글들이 훨씬 쉽게 읽힙니다. 두 가지가 각각 어디에 끼어드는지는 6편에서 한 장의 지도로 봅니다.- ①과 ②는 깔끔하게 나뉘지 않습니다. 어노테이션 방식에서는
@Configuration클래스를 읽어 설계도를 추가하는 일(①)을ConfigurationClassPostProcessor가 하는데, 이 클래스 자신이BeanFactoryPostProcessor의 한 종류입니다. 즉 ①의 많은 부분이 ②의 장치로 구현되어 있습니다. 그림의 네 단계는 "무슨 일이 일어나는가"의 순서로 이해하면 됩니다. - 프록시는 설계도 단계에서 만들어지지 않습니다. 감쌀 객체가 있어야 하므로 ③의 초기화 걸음에서, 객체가 다 만들어진 뒤에 씌웁니다. (빈끼리 서로를 필요로 하는 순환 참조일 때는 더 일찍 만들어지기도 하는데, 이것은 4편에서 다룹니다.)
7. 여러 기능이 되풀이하는 세 조각
4장의 표에는 트랜잭션, 캐시, 비동기 실행 같은 기능이 여럿 있었습니다. 따로따로 만든 것처럼 보이지만 속을 열어 보면 같은 세 조각을 되풀이합니다.
파란 칸 세 개가 되풀이되는 조각입니다. 정의를 등록하고, 빈 생성에 끼어들고, 호출을 가로챕니다.
트랜잭션으로 따라가 봅니다. @Transactional을 메서드에 붙이면 그 메서드가 트랜잭션 안에서 실행됩니다. 이 기능을 켜는 스위치가 @EnableTransactionManagement입니다.
조각 ① — @Import로 빈 정의를 등록한다. 스위치 어노테이션의 선언부입니다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {
spring-tx/…/transaction/annotation/EnableTransactionManagement.java 159–163행
@Import는 "이 클래스가 말하는 빈 정의도 함께 등록하라"는 뜻입니다. 가리킨 클래스의 selectImports 메서드를 따라가면, 기본 모드(PROXY)에서 두 가지를 가져옵니다. AutoProxyRegistrar는 프록시를 자동으로 씌워 주는 BeanPostProcessor인 InfrastructureAdvisorAutoProxyCreator의 설계도를 등록하고, ProxyTransactionManagementConfiguration은 "트랜잭션을 시작하고 끝내는" 실제 일을 하는 TransactionInterceptor 등을 빈으로 등록합니다. 6장의 ①·② 단계에서 일어나는 일입니다.
조각 ② — BeanPostProcessor가 빈 생성에 끼어든다. BeanPostProcessor는 컨테이너가 빈을 만들 때마다 불러 주는 확장 지점입니다. 방금 등록된 InfrastructureAdvisorAutoProxyCreator의 부모 클래스 AbstractAutoProxyCreator에 이런 메서드가 있습니다.
public @Nullable Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
if (bean != null) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
if (this.earlyBeanReferences.remove(cacheKey) != bean) {
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
spring-aop/…/aop/framework/autoproxy/AbstractAutoProxyCreator.java 285–293행
이름 그대로 "초기화가 끝난 뒤"에 불립니다. 6장 그림의 ③에서 마지막 칸입니다. 빈을 받아서, 필요하면 감싸고(wrapIfNecessary), 아니면 그대로 돌려줍니다.
조각 ③ — 프록시가 호출을 가로챈다. 감싸진 빈은 컨테이너에 프록시로 등록됩니다. 그 뒤로 누군가 이 빈의 메서드를 부르면 프록시가 먼저 받아서 트랜잭션을 시작하고, 진짜 메서드를 부르고, 끝나면 커밋하거나 되돌립니다.
다른 기능도 같은 길을 갑니다. 아래 표의 클래스 이름은 모두 소스에서 확인한 것입니다(기본 모드 기준).
| 기능 | 스위치 | @Import가 가져오는 것 |
끼어드는 BeanPostProcessor |
프록시 |
|---|---|---|---|---|
@Transactional |
@EnableTransactionManagement |
AutoProxyRegistrar, ProxyTransactionManagementConfiguration |
InfrastructureAdvisorAutoProxyCreator |
만든다 |
@Cacheable |
@EnableCaching |
AutoProxyRegistrar, ProxyCachingConfiguration |
InfrastructureAdvisorAutoProxyCreator |
만든다 |
@Async |
@EnableAsync |
ProxyAsyncConfiguration |
AsyncAnnotationBeanPostProcessor |
만든다 |
@Scheduled |
@EnableScheduling |
SchedulingConfiguration |
ScheduledAnnotationBeanPostProcessor |
만들지 않는다 |
마지막 줄이 눈여겨볼 곳입니다. @Scheduled는 "이 메서드를 정해진 시간마다 불러 달라"는 것이라 호출을 가로챌 필요가 없습니다. ScheduledAnnotationBeanPostProcessor는 빈을 살펴 일정만 등록하고 빈은 그대로 돌려줍니다. 세 조각을 모두 쓰는 기능도 있고 앞의 두 조각만 쓰는 기능도 있는 것입니다.
그래서 가운데 층을 먼저 읽습니다. 빈 정의가 어떻게 등록되는지, BeanPostProcessor가 언제 불리는지, 프록시가 어떻게 만들어지는지를 알고 나면 위 층의 기능 대부분은 "이 세 조각을 어떻게 조합했나"로 읽을 수 있습니다.
8. 그래서 어떤 순서로 읽나
지도가 읽는 순서도 정해 줍니다. 화살표의 아래쪽, 곧 남들이 기대고 있는 쪽부터 읽습니다. spring-beans(빈이 만들어지는 과정) → spring-context(refresh()와 어노테이션 설정) → spring-aop(프록시) → 그 위의 데이터와 웹 순서입니다. spring-core는 따로 떼어 읽지 않고, 읽다가 막히는 곳에서 필요한 부분만 꺼내 봅니다. 읽기 단위를 어떻게 나누었고 지금 어디까지 왔는지는 1편에 정리해 두었습니다.
화살표의 아래쪽부터 읽는 순서를 편 번호와 함께 놓았습니다. 점선은 "필요할 때 꺼내 본다"는 뜻입니다.
정리
- Spring Framework는
spring-으로 시작하는 모듈 22개로 나뉘어 있고, 목록은settings.gradle에 있습니다. - 크게 네 층입니다. 바탕(spring-core) 위에 컨테이너와 AOP(spring-beans, spring-aop, spring-expression, spring-context)가 있고, 그 위에 데이터와 웹이 올라갑니다. 위 층은 아래 층을 쓰지만 아래 층은 위 층을 모릅니다.
- 의존 관계는 각 모듈의
<모듈 이름>.gradle에 적혀 있습니다.api는 반드시 필요한 의존,optional은 있으면 쓰는 의존입니다. - spring-beans에는
BeanFactory와BeanDefinition이, spring-context에는ApplicationContext와 어노테이션 설정이, spring-aop에는 프록시가 있습니다.support패키지는 구현이 모인 곳입니다. - 컨테이너 기동은
AbstractApplicationContext.refresh()하나에 차례대로 적혀 있습니다. 정의 수집 → 정의 가공 → 빈 생성(인스턴스화 → 주입 → 초기화) → 운영 순서입니다. - 트랜잭션, 캐시, 비동기 같은 기능은 "
@Import로 정의 등록 →BeanPostProcessor로 생성에 끼어들기 → 프록시로 호출 가로채기"를 되풀이합니다.
이 글에서 뺀 것
지도를 한 장으로 유지하려고 뺀 것들입니다.
- spring-expression, spring-tx, spring-web, spring-webmvc, spring-webflux의 패키지별 내용. 해당 모듈을 읽는 글에서 다룹니다.
- 모듈 사이의
optional의존 전체 목록. 그림에는 이야기에 필요한 것만 그렸습니다. refresh()의 열두 단계 하나하나와, 빈 생성 과정의 세부.- AOT(실행 전에 미리 처리해 두는 기능) 관련 패키지(16편에서 봅니다), Kotlin 지원 코드.
다음에 볼 것
지도를 접고 첫 번째 목적지로 들어갑니다. 다음 글은 spring-beans의 beans.factory 패키지들입니다. BeanFactory라는 인터페이스가 왜 여러 개로 쪼개져 있는지, 그것들을 모두 구현하는 DefaultListableBeanFactory까지 어떤 클래스들이 쌓여 있는지, 그리고 6장에서 "설계도"라고만 부른 BeanDefinition에 실제로 무엇이 적혀 있는지를 읽습니다.