namalzi

← 글 목록

Spring 소스 파헤치기 2: 전체 지도 — 모듈과 패키지는 어떻게 나뉘어 있나

Spring · 게시

Spring Framework 저장소를 처음 열어 보는 사람을 위한 지도입니다. 모듈 22개가 각각 무슨 일을 하고 서로 어떻게 기대고 있는지, spring-core·spring-beans·spring-context·spring-aop 안에는 무엇이 있는지, 컨테이너가 켜질 때의 큰 흐름까지 실제 빌드 파일과 소스로 확인하며 따라갑니다.

이 글에서 다루는 것

"Spring 소스 파헤치기"의 두 번째 글입니다. 1편에서는 Spring이 무엇인지, 소스를 왜 읽고 어떤 순서로 읽을지를 이야기했습니다. 이번 글은 코드를 한 줄씩 읽기 전에 펴 놓는 전체 지도입니다.

Spring Framework 저장소를 내려받아 열면 spring-으로 시작하는 폴더가 스무 개 넘게 나옵니다. 이름만 봐서는 어디가 중심이고 어디가 변두리인지 알 수 없습니다. 이 글을 끝까지 읽으면 다음을 알게 됩니다.

뒤의 글에서 "이 클래스는 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"
}

settings.gradle 5–36행

여기서 세 가지를 읽을 수 있습니다.

  1. spring-으로 시작하는 줄이 22개입니다. 이것이 Spring Framework 그 자체이고, 저장소 맨 위의 폴더 이름과 하나씩 맞습니다.
  2. 나머지 다섯 개(framework-api, framework-bom, framework-docs, framework-platform, integration-tests)는 문서, 버전 목록, 통합 테스트입니다. 프레임워크의 동작을 읽을 때는 보지 않아도 됩니다.
  3. 마지막 세 줄 때문에 모듈의 빌드 파일 이름이 흔히 보는 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 에는 include 줄이 27개 있다. 그 가운데 spring- 으로 시작하는 22개가 Spring Framework 그 자체다. 예를 들어 spring-beans 폴더 안에는 빌드 파일 spring-beans.gradle(build.gradle 이 아니라 모듈 이름.gradle), 이 시리즈가 읽는 실제 소스인 src/main/java/org/springframework/beans/…, 그리고 테스트인 src/test/… 가 있다. 나머지 5개(framework-api, framework-bom, framework-docs, framework-platform, integration-tests)는 문서·버전 목록·통합 테스트라 읽지 않아도 된다 settings.gradle 한 파일에서 갈라지는 저장소의 모양. 초록 칸이 이 시리즈가 읽는 곳, 회색 칸은 건너뛰어도 되는 곳입니다.

2. 한 장으로 보는 네 층

22개를 한꺼번에 외울 필요는 없습니다. 먼저 층으로 나누어 봅니다.

Spring Framework의 주요 모듈을 네 층으로 나눈 그림. 맨 위에 웹과 데이터, 가운데에 컨테이너와 AOP, 맨 아래에 바탕인 spring-core가 있고 화살표는 위에서 아래로 향한다. 화살표는 "위 층이 아래 층을 가져다 쓴다"는 뜻입니다. 테두리가 굵은 가운데 층이 이 시리즈가 먼저 읽는 곳입니다.

중요한 것은 방향입니다. 위 층은 아래 층을 알지만 아래 층은 위 층을 모릅니다. 가운데 층의 네 모듈은 데이터나 웹 층의 모듈을 하나도 가져다 쓰지 않습니다. 그래서 가운데 층만 떼어서 읽을 수 있고, 가운데 층을 알고 나면 위 층은 "그 위에 무엇을 더 얹었나"만 보면 됩니다.

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에 있고, 주석에 "프로젝트 자신의 컴파일·런타임 클래스패스에는 들어가지만, 이 프로젝트에 의존하는 쪽의 클래스패스에는 영향을 주지 않는다"고 적혀 있습니다.

중심 축: 다섯 모듈

가운데 층과 바탕을 모듈 단위로 풀면 이렇게 됩니다.

spring-context가 spring-aop와 spring-expression에 의존하고, spring-aop는 spring-beans에, spring-beans와 spring-expression은 spring-core에 의존하는 그림. A → B는 "A가 B를 가져다 쓴다"입니다. 모두 api 의존이고, 다른 화살표를 따라가면 닿는 것은 그리지 않았습니다(예: spring-context는 spring-beans와 spring-core도 직접 선언합니다).

아래에서 위로 읽으면 쌓이는 순서가 보입니다.

  1. spring-core가 맨 아래입니다. 다른 모듈을 가리키는 줄이 빌드 파일에 하나도 없습니다.
  2. spring-beans는 spring-core만 씁니다. 컨테이너의 핵심이 여기 있습니다.
  3. spring-aop는 spring-beans 위에 올라갑니다. 프록시를 만들려면 감쌀 빈이 있어야 하기 때문입니다.
  4. spring-expression은 spring-core만 씁니다. spring-beans와는 상관없이 따로 서 있는 모듈입니다.
  5. spring-context가 이 넷을 모두 모읍니다. 우리가 보통 "Spring 컨테이너"라고 부르는 ApplicationContext가 여기 있습니다.

축 위에 얹히는 모듈들

데이터와 웹 층의 모듈 몇 개를 같은 방법으로 그리면 이렇습니다.

spring-webmvc와 spring-webflux가 spring-web에, spring-orm이 spring-jdbc에, spring-jdbc가 spring-tx에 의존하고, spring-web과 spring-tx는 spring-beans에 실선으로, spring-context에는 점선으로 연결된 그림. 실선은 api, 점선은 optional입니다. 그린 화살표는 모두 빌드 파일에서 확인한 것이고, 읽기 좋게 일부는 생략했습니다(예: 거의 모든 모듈이 spring-core를 직접 선언합니다).

헷갈리기 쉬운 곳: 점선은 "없어도 된다"는 뜻이다

"트랜잭션을 쓰려면 당연히 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천 줄입니다. 양으로 보면 전체의 일부지만 나머지 모듈이 모두 이 위에 서 있습니다.

모듈 22개의 src/main/java 아래 .java 파일 수를 큰 것부터 늘어놓은 막대그래프. spring-web 792, spring-core 738, spring-context 625, spring-test 460, spring-webmvc 363, spring-beans 338, spring-webflux 276, spring-jdbc 253, spring-messaging 245, spring-aop 226, spring-tx 173, spring-websocket 170, spring-expression 123, spring-jms 122, spring-context-support 87, spring-orm 65, spring-r2dbc 65, spring-core-test 45, spring-oxm 28, spring-aspects 14, spring-context-indexer 13, spring-instrument 2 위 표의 파일 수를 막대로 옮긴 것입니다. 컨테이너의 핵심인 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 위에 있는 이유입니다.

spring-context 안에서는 context.support 패키지의 AbstractApplicationContext 와 GenericApplicationContext 가 context 패키지의 인터페이스 ApplicationContext·ConfigurableApplicationContext 를 구현한다. spring-beans 안에서는 beans.factory.support 의 DefaultListableBeanFactory 가 beans.factory 의 인터페이스 BeanFactory·ListableBeanFactory 를 구현한다. 그리고 ApplicationContext 가 ListableBeanFactory 를 상속하므로 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 지원

헷갈리기 쉬운 곳: 이름과 위치가 어긋나는 것들

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편에서 합니다).

컨테이너 기동의 네 단계. 정의 수집, 정의 가공, 빈 생성(인스턴스화, 주입, 초기화), 운영을 거쳐 종료로 이어진다. 컨테이너가 켜져서 꺼질 때까지의 큰 흐름. ③ 안의 세 칸은 빈 하나가 만들어지는 과정이고, 빈마다 되풀이됩니다.

  1. 정의 수집. 컨테이너는 객체를 바로 만들지 않습니다. 먼저 "어떤 빈을 어떻게 만들지"를 적은 설계도를 모읍니다. 이 설계도가 BeanDefinition입니다(3편에서 자세히 봅니다). 위 예시에서는 register가 AppConfig의 설계도를 등록하고, AppConfig 안의 @Bean 메서드는 invokeBeanFactoryPostProcessors 단계에서 읽혀 설계도로 추가됩니다.
  2. 정의 가공. 설계도가 모이면 객체를 만들기 전에 설계도를 고치거나 더할 기회를 줍니다. 이 일을 하는 것이 BeanFactoryPostProcessor이고, refresh()에서는 invokeBeanFactoryPostProcessors입니다. 바로 다음 줄 registerBeanPostProcessors는 ③에서 쓸 BeanPostProcessor들을 미리 준비해 둡니다.
  3. 빈 생성. finishBeanFactoryInitialization이 설계도대로 빈을 실제로 만듭니다. 소스의 주석대로 지연 초기화(lazy-init)가 아닌 싱글톤(컨테이너에 하나만 만들어 두고 함께 쓰는 빈)이 대상입니다. 빈 하나는 인스턴스화(객체를 만든다) → 주입(필요한 다른 빈을 넣는다) → 초기화(초기화 메서드를 부르고, 필요하면 프록시로 감싼다)의 세 걸음으로 만들어집니다. 이 세 걸음이 AbstractAutowireCapableBeanFactory의 doCreateBean에 createBeanInstance, populateBean, initializeBean이라는 이름으로 차례로 나옵니다. 4편의 주제입니다.
  4. 운영과 종료. finishRefresh가 "준비가 끝났다"는 이벤트(ContextRefreshedEvent)를 알리면 기동이 끝납니다. 나중에 close()를 부르면 종료 이벤트를 알리고 빈을 없앱니다.

헷갈리기 쉬운 곳

7. 여러 기능이 되풀이하는 세 조각

4장의 표에는 트랜잭션, 캐시, 비동기 실행 같은 기능이 여럿 있었습니다. 따로따로 만든 것처럼 보이지만 속을 열어 보면 같은 세 조각을 되풀이합니다.

@EnableXxx에서 시작해 @Import, BeanPostProcessor, 프록시를 차례로 거쳐 @Transactional 같은 어노테이션이 동작하게 되는 흐름. 파란 칸 세 개가 되풀이되는 조각입니다. 정의를 등록하고, 빈 생성에 끼어들고, 호출을 가로챕니다.

트랜잭션으로 따라가 봅니다. @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-beans(빈이 만들어지는 과정, 3~6편) → ② spring-context(refresh()와 어노테이션 설정, 7~9편) → ③ spring-aop(프록시, 10~11편) → ④ 그 위의 데이터(17~19편)와 웹(20~25편). spring-core 는 따로 떼어 읽지 않고, 앞의 세 단계를 읽다가 막히는 곳에서 꺼내 본다 화살표의 아래쪽부터 읽는 순서를 편 번호와 함께 놓았습니다. 점선은 "필요할 때 꺼내 본다"는 뜻입니다.

정리

이 글에서 뺀 것

지도를 한 장으로 유지하려고 뺀 것들입니다.

다음에 볼 것

지도를 접고 첫 번째 목적지로 들어갑니다. 다음 글은 spring-beans의 beans.factory 패키지들입니다. BeanFactory라는 인터페이스가 왜 여러 개로 쪼개져 있는지, 그것들을 모두 구현하는 DefaultListableBeanFactory까지 어떤 클래스들이 쌓여 있는지, 그리고 6장에서 "설계도"라고만 부른 BeanDefinition에 실제로 무엇이 적혀 있는지를 읽습니다.

Spring 소스 파헤치기 3: BeanFactory 계층 구조와 BeanDefinition

다른 글