namalzi

← 글 목록

Spring 소스 파헤치기 7: refresh() 12단계 — 컨테이너가 켜지는 순서

Spring · 게시

new AnnotationConfigApplicationContext(...) 한 줄 뒤에서 도는 refresh()를 열두 단계 하나씩 엽니다. ApplicationContext가 BeanFactory에 무엇을 더하는지, 왜 GenericApplicationContext와 AbstractRefreshableApplicationContext 두 갈래인지, 켜다가 실패하면 그리고 close()하면 무슨 일이 일어나는지를 실제 소스와 예제로 확인합니다.

이 글에서 다루는 것

"Spring 소스 파헤치기" 시리즈의 일곱 번째 글입니다. 6편에서는 빈 하나가 만들어지는 동안 컨테이너에 끼어드는 자리들(BeanPostProcessor, @PostConstruct 같은 생명주기 콜백)이 어떤 순서로 불리는지 봤습니다. 이번에는 시야를 넓혀 컨테이너 자체가 켜지는 순서를 봅니다. 1편과 2편에서 "refresh() 안에서 열두 단계를 밟는다"고 하고 넘어간 그 메서드를 한 줄씩 엽니다.

답하려는 질문은 네 가지입니다.

항목 내용
기준 소스 Spring Framework 7.1.0-SNAPSHOT
커밋 f447f3c310 (2026-10-01, main 브랜치)
주로 읽는 모듈 spring-context
주로 읽는 클래스 AbstractApplicationContext, GenericApplicationContext, AbstractRefreshableApplicationContext, ConfigurableApplicationContext

글에 나오는 줄 번호와 세부 동작은 모두 이 커밋 기준입니다. 다른 버전에서는 줄 번호가 다르고 세부 내용도 달라질 수 있습니다. 같은 커밋의 소스를 받는 방법은 1편에 있습니다.

공부 노트는 단계마다 훨씬 깊이 들어가지만, 이 글은 Spring을 처음 보는 분이 따라올 수 있는 큰 줄기만 골랐습니다. 빼놓은 것은 글 끝의 "더 깊이 들어가면"에 이름만 적어 두었습니다.

먼저 알아 둘 말

빈과 컨테이너는 앞 글들과 같습니다. 빈은 Spring이 대신 만들어 관리하는 객체, 컨테이너는 그 빈을 만들고 보관하는 주체입니다. 컨테이너의 뼈대 인터페이스가 BeanFactory이고(3편), 빈 하나가 만들어지는 과정은 4편에 있습니다.

**ApplicationContext**는 BeanFactory를 확장한 인터페이스입니다. 우리가 코드에서 new AnnotationConfigApplicationContext(...)로 만드는 것이 이것입니다. 빈을 꺼내 주는 일 말고도 설정값 읽기, 이벤트 보내기, 파일 읽기 같은 일을 함께 맡습니다. 1절에서 정확히 무엇을 더하는지 봅니다. 이 글에서 "컨텍스트"라고 하면 ApplicationContext를 뜻합니다.

**refresh()**는 컨텍스트를 실제로 켜는 메서드입니다. 설계도를 읽고, 가공하고, 빈을 만들고, "준비됐다"고 알리는 일이 전부 이 안에서 일어납니다. 이름이 "새로 고침"인 이유는 7절에서 설명합니다.

템플릿 메서드 패턴은 부모 클래스가 전체 순서를 한 메서드에 고정해 두고, 그 가운데 몇 단계만 자식 클래스가 채워 넣게 하는 설계입니다. refresh()가 바로 그 "순서를 고정한 메서드"이고, 자식이 채우는 자리가 몇 군데 있습니다.

이벤트와 리스너: 컨텍스트는 "켜졌다", "닫힌다" 같은 일을 이벤트 객체로 만들어 보내고, ApplicationListener를 구현한 빈이 그것을 받습니다. 이 글에서는 ContextRefreshedEvent와 ContextClosedEvent 두 개만 나옵니다. 이벤트 구조 자체는 12편의 주제입니다.

후처리기는 6편의 주제였습니다. BeanFactoryPostProcessor는 설계도(BeanDefinition)를, BeanPostProcessor는 만들어지고 있는 빈 객체를 손보는 장치입니다. 이 글에서는 둘이 refresh()의 어느 단계에서 불리는지만 봅니다.

1. ApplicationContext는 BeanFactory에 무엇을 더하나

인터페이스 선언 한 줄에 답이 있습니다.

public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
		MessageSource, ApplicationEventPublisher, ResourcePatternResolver {

spring-context/src/main/java/org/springframework/context/ApplicationContext.java 59–60행

여섯 인터페이스를 한데 묶은 것입니다. 둘은 3편에서 본 BeanFactory 계열이고, 넷이 새로 더해진 것입니다.

인터페이스 더해 주는 일 어디서 왔나
ListableBeanFactory 빈을 이름 하나씩이 아니라 목록으로 조회 spring-beans (3편)
HierarchicalBeanFactory 부모 컨테이너를 둘 수 있다 spring-beans (3편)
EnvironmentCapable 설정값과 프로파일(Environment)을 가진다 spring-core
MessageSource 다국어 메시지를 찾아 준다 spring-context
ApplicationEventPublisher 이벤트를 보낸다(publishEvent) spring-context
ResourcePatternResolver 파일·클래스패스 자원을 패턴으로 읽는다 spring-core

ApplicationContext가 상속하는 여섯 인터페이스와, 그 위에 Lifecycle과 Closeable을 더한 ConfigurableApplicationContext, 그것을 구현한 AbstractApplicationContext의 관계. 회색 둘은 3편에서 본 BeanFactory 계열입니다. 파란 둘이 이 글의 무대입니다.

그런데 이 목록에는 refresh()도 close()도 없습니다. ApplicationContext는 설명 주석대로 읽기 전용이고(28–30행), 켜고 끄는 메서드는 한 단계 아래의 ConfigurableApplicationContext에 있습니다.

public interface ConfigurableApplicationContext extends ApplicationContext, Lifecycle, Closeable {

spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java 45행

여기에 refresh()(222행), close()(267행), registerShutdownHook()(256행), isActive()(289행), 그리고 안에 든 BeanFactory를 꺼내는 getBeanFactory()(310행)가 선언되어 있습니다. 빈을 쓰는 쪽에는 읽기 전용 ApplicationContext를 주고, 켜고 끄는 일은 Configurable…에만 열어 둔 것입니다.

이 모든 것을 실제로 구현한 클래스가 AbstractApplicationContext입니다(AbstractApplicationContext 145–146행). 이 글에서 읽는 메서드는 거의 전부 이 클래스에 있습니다.

컨텍스트는 BeanFactory를 "가지고" 있습니다

AbstractApplicationContext가 BeanFactory의 메서드를 어떻게 구현하는지 보면 설계가 보입니다.

	@Override
	public Object getBean(String name) throws BeansException {
		assertBeanFactoryActive();
		return getBeanFactory().getBean(name);
	}

spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java 1297–1301행

직접 만들지 않고 안에 든 BeanFactory에 넘깁니다. 컨텍스트는 BeanFactory를 상속해서 커진 것이 아니라, 4편에서 본 DefaultListableBeanFactory를 안에 하나 품고 그 둘레에 설정값·이벤트·메시지 같은 기능을 두른 것입니다. 앞의 assertBeanFactoryActive()는 "켜져 있는가"를 확인하는 한 줄입니다.

	protected void assertBeanFactoryActive() {
		if (!this.active.get()) {
			if (this.closed.get()) {
				throw new IllegalStateException(getDisplayName() + " has been closed already");
			}
			else {
				throw new IllegalStateException(getDisplayName() + " has not been refreshed yet");
			}
		}
	}

spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java 1281–1290행

active와 closed는 AtomicBoolean 필드 둘입니다(209–212행). refresh()가 active를 켜고, close()가 closed를 켜고 active를 끕니다. 이 두 깃발이 글 전체를 관통합니다.

2. refresh()의 뼈대 — 세어 보니 열두 개

new AnnotationConfigApplicationContext(AppConfig.class)의 생성자가 register와 refresh 두 줄이라는 것은 1편에서 봤습니다(AnnotationConfigApplicationContext 89–93행). 그 refresh()의 본문입니다. 예외 처리 부분은 8절에서 따로 보므로 여기서는 // ...로 줄였습니다.

	@Override
	public void refresh() throws BeansException, IllegalStateException {
		this.startupShutdownLock.lock();
		try {
			this.startupShutdownThread = Thread.currentThread();

			StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");

			// Prepare this context for refreshing.
			prepareRefresh();

			// Tell the subclass to refresh the internal bean factory.
			ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

			// Prepare the bean factory for use in this context.
			prepareBeanFactory(beanFactory);

			try {
				// Allows post-processing of the bean factory in context subclasses.
				postProcessBeanFactory(beanFactory);

				StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
				// Invoke factory processors registered as beans in the context.
				invokeBeanFactoryPostProcessors(beanFactory);
				// Register bean processors that intercept bean creation.
				registerBeanPostProcessors(beanFactory);
				beanPostProcess.end();

				// Initialize message source for this context.
				initMessageSource();

				// Initialize event multicaster for this context.
				initApplicationEventMulticaster();

				// Initialize other special beans in specific context subclasses.
				onRefresh();

				// Check for listener beans and register them.
				registerListeners();

				// Instantiate all remaining (non-lazy-init) singletons.
				finishBeanFactoryInitialization(beanFactory);

				// Last step: publish corresponding event.
				finishRefresh();
			}

			catch (RuntimeException | Error ex) {
				// ...
			}

			finally {
				contextRefresh.end();
			}
		}
		finally {
			this.startupShutdownThread = null;
			this.startupShutdownLock.unlock();
		}
	}

spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java 581–662행

주석이 한 줄씩 붙은 메서드 호출을 세어 보면 prepareRefresh부터 finishRefresh까지 정확히 열두 개입니다. 1편과 2편에서 "열두 단계"라고 쓴 것이 이 커밋에서도 맞습니다. 참고로 StartupStep은 각 구간의 소요 시간을 재는 측정 도구라 단계에 넣지 않았습니다.

refresh()의 열두 단계를 준비, 설계도 가공, 부속 준비, 빈 생성과 운영 시작의 네 묶음으로 나눈 그림. 2편의 네 덩어리와 열두 단계를 맞춘 그림입니다. 회색은 기본 구현이 비어 있는 하위 클래스용 자리, 파란색은 후처리기와 빈 생성, 초록색이 마지막입니다.

단계 메서드 한 줄 요약 절
1 prepareRefresh active 켜기, 환경 검사, 이른 이벤트 그릇 준비 3
2 obtainFreshBeanFactory 안에 둘 BeanFactory를 얻는다 (두 갈래가 갈리는 곳) 3, 7
3 prepareBeanFactory BeanFactory에 컨텍스트 전용 장치를 끼운다 3
4 postProcessBeanFactory 하위 클래스용 빈자리 (기본은 비어 있음) 4
5 invokeBeanFactoryPostProcessors 설계도를 고치고 더한다 4
6 registerBeanPostProcessors 빈 후처리기를 미리 만들어 등록한다 4
7 initMessageSource 메시지 소스 준비 5
8 initApplicationEventMulticaster 이벤트 전달기 준비 5
9 onRefresh 하위 클래스용 빈자리 (기본은 비어 있음) 5
10 registerListeners 리스너 등록, 밀린 이벤트 보내기 5
11 finishBeanFactoryInitialization 싱글톤 빈 전부 만들기 6
12 finishRefresh Lifecycle 시작, ContextRefreshedEvent 6

메서드 맨 앞의 startupShutdownLock은 켜기와 끄기가 동시에 일어나지 않게 하는 락입니다(215행). 8절의 close()도 같은 락을 잡습니다. startupShutdownThread는 지금 그 락을 잡고 일하는 스레드를 기록해 두는 필드입니다(218행).

3. 1~3단계: 준비

1단계 prepareRefresh()(668–703행)는 네 가지를 합니다. 시작 시각을 적고 closed를 끄고 active를 켭니다(670–672행). Environment에 "반드시 있어야 한다"고 표시된 설정값이 다 있는지 검사합니다(688행). refresh() 전에 코드로 직접 등록해 둔 리스너를 따로 보관해 둡니다(691–698행). 그리고 earlyApplicationEvents라는 빈 집합을 만듭니다(702행). 이 집합의 쓸모는 5절에서 드러납니다. (기본 구현이 비어 있는 initPropertySources() 호출(710–712행)은 세지 않았습니다.)

**2단계 obtainFreshBeanFactory()**는 두 줄입니다.

	protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
		refreshBeanFactory();
		return getBeanFactory();
	}

spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java 720–723행

refreshBeanFactory()와 getBeanFactory()는 둘 다 추상 메서드입니다(1633행, 1656행). 안에 둘 BeanFactory를 어떻게 마련할지는 부모가 정하지 않고 자식에게 맡긴 것입니다. 이것이 7절의 두 갈래입니다.

**3단계 prepareBeanFactory()**가 이 글에서 가장 눈여겨볼 준비 단계입니다. 방금 얻은 평범한 BeanFactory에 "컨텍스트 안에서 쓰이는 BeanFactory"로서 필요한 장치를 끼웁니다. 요지만 남기면 이렇습니다.

	protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
		// Tell the internal bean factory to use the context's class loader etc.
		beanFactory.setBeanClassLoader(getClassLoader());
		// ...

		// Configure the bean factory with context callbacks.
		beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));
		beanFactory.ignoreDependencyInterface(EnvironmentAware.class);
		// ...
		beanFactory.ignoreDependencyInterface(ApplicationContextAware.class);
		// ...

		// BeanFactory interface not registered as resolvable type in a plain factory.
		// MessageSource registered (and found for autowiring) as a bean.
		beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory);
		beanFactory.registerResolvableDependency(ResourceLoader.class, this);
		beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this);
		beanFactory.registerResolvableDependency(ApplicationContext.class, this);

		// Register early post-processor for detecting inner beans as ApplicationListeners.
		beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this));
		// ...

		// Register default environment beans.
		if (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) {
			beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment());
		}
		// ...
	}

spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java 730–776행

세 가지를 끼웁니다.

  1. ApplicationContextAwareProcessor(737행). 빈이 ApplicationContextAware, EnvironmentAware 같은 인터페이스를 구현하고 있으면 컨텍스트나 Environment를 넣어 주는 **BeanPostProcessor**입니다. 열어 보면 postProcessBeforeInitialization에서 instanceof로 일곱 종류를 차례로 확인합니다(ApplicationContextAwareProcessor 83–112행). 즉 "컨텍스트가 Aware 인터페이스를 알아본다"는 기능도 4편에서 본 후처리기 하나로 구현되어 있습니다. 그 아래 ignoreDependencyInterface(738–744행)는 같은 일곱 인터페이스의 setter를 자동 주입 대상에서 빼라는 뜻입니다. 후처리기가 넣어 줄 것이니 주입 쪽이 끼어들지 말라는 것입니다.
  2. registerResolvableDependency(748–751행). BeanFactory, ApplicationContext 같은 타입은 빈으로 등록된 적이 없는데도 @Autowired ApplicationContext ctx;가 되는 이유가 이 네 줄입니다. "이 타입을 달라고 하면 이 객체를 줘라"고 BeanFactory에 미리 적어 둡니다. 소스의 주석대로 MessageSource는 여기 없고 7단계에서 빈으로 등록됩니다.
  3. ApplicationListenerDetector(754행). 만들어진 빈이 ApplicationListener면 리스너 명단에 올려 주는 후처리기입니다(ApplicationListenerDetector 75–95행). 10단계의 registerListeners가 이름으로 찾아 등록하지 못하는 내부 빈(inner bean)을 위한 보완 장치입니다.

마지막으로 environment, systemProperties, systemEnvironment, applicationStartup 네 객체를 싱글톤 빈으로 넣어 둡니다(764–775행). @Autowired Environment env;가 되는 이유입니다.

3단계 prepareBeanFactory()가 2단계에서 얻은 BeanFactory 에 끼우는 네 가지. 후처리기 목록에는 6단계보다 앞자리로 ① Aware 일곱 가지를 넣어 주는 ApplicationContextAwareProcessor 와 ② 리스너 빈을 명단에 올리는 ApplicationListenerDetector 가 들어간다. ignoreDependencyInterface 로 같은 Aware 일곱 가지의 setter 를 자동 주입에서 뺀다. registerResolvableDependency 로 빈이 아닌데도 주입되는 타입 넷(BeanFactory, ResourceLoader, ApplicationEventPublisher, ApplicationContext)을 적어 둔다. 그리고 environment, systemProperties, systemEnvironment, applicationStartup 을 싱글톤으로 바로 넣는다 위 코드에서 끼우는 것을 종류별로 묶은 그림입니다. 파란 칸은 빈이 만들어질 때 일하는 후처리기, 노란 칸은 주입할 때 꺼내 쓰는 객체입니다(위 코드에서 줄인 LoadTimeWeaver 관련 줄은 넣지 않았습니다).

헷갈리기 쉬운 곳

4. 4~6단계: 설계도 가공

**4단계 postProcessBeanFactory()**는 기본 구현이 비어 있습니다(787–788행). 템플릿 메서드 패턴의 "자식이 채우는 자리"입니다. 설명 주석에 적힌 시점이 중요합니다. 설계도는 읽혔지만 후처리기는 아직 돌지 않았고 빈은 하나도 만들어지지 않은 때입니다(779–782행). 웹 환경의 컨텍스트가 서블릿 관련 후처리기를 여기서 끼웁니다(GenericWebApplicationContext 153행, 20편에서 봅니다).

**5단계 invokeBeanFactoryPostProcessors()**는 한 줄짜리 위임입니다.

	protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
		PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());
		// ...
	}

spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java 795–805행

getBeanFactoryPostProcessors()는 코드로 직접 addBeanFactoryPostProcessor해 둔 것들의 목록이고(203행), 빈으로 등록된 BeanFactoryPostProcessor는 PostProcessorRegistrationDelegate가 BeanFactory에서 찾아 만들고 순서대로 부릅니다(PostProcessorRegistrationDelegate 68행부터). 그 순서 규칙은 6편의 주제였으므로 여기서는 되풀이하지 않습니다.

이 단계가 중요한 이유는 어노테이션 설정의 해석이 여기서 일어나기 때문입니다. @Configuration 클래스를 읽어 @Bean 메서드와 @ComponentScan 결과를 설계도로 추가하는 ConfigurationClassPostProcessor가 BeanFactoryPostProcessor의 한 종류라서 이 단계에서 돕니다(AnnotationConfigUtils 158–161행에서 등록). 2편에서 "정의 수집의 많은 부분이 정의 가공의 장치로 구현되어 있다"고 한 것이 이 뜻입니다. 자세한 내용은 8편에서 봅니다.

**6단계 registerBeanPostProcessors()**도 같은 위임입니다(812–814행, 구현은 PostProcessorRegistrationDelegate 211행부터). 빈으로 등록된 BeanPostProcessor를 지금 전부 만들어 BeanFactory에 끼워 둡니다. 후처리기는 다른 빈이 만들어질 때 끼어들어야 하므로, 평범한 빈보다 먼저 존재해야 합니다. 그래서 11단계가 아니라 6단계입니다.

5단계와 6단계의 차이. 5단계 invokeBeanFactoryPostProcessors 는 BeanFactoryPostProcessor 빈을 찾아 만들고(코드로 add 해 둔 것도 함께) 순서대로 불러 설계도를 고치고 더하게 한다. 예를 들어 ConfigurationClassPostProcessor 가 @Bean 과 @ComponentScan 결과를 설계도로 추가한다. 이들의 일은 이 단계에서 끝난다. 6단계 registerBeanPostProcessors 는 BeanPostProcessor 빈을 평범한 빈보다 먼저 지금 전부 만들어 BeanFactory 의 후처리기 목록에 끼워 두기만 하고, 이들은 11단계에서 빈이 하나씩 만들어질 때마다 그제야 불린다 이름이 비슷한 두 단계지만, 5단계의 후처리기는 그 자리에서 일하고 6단계의 후처리기는 11단계까지 기다립니다.

헷갈리기 쉬운 곳

5. 7~10단계: 부속 준비

7, 8단계는 같은 모양입니다. "그 이름의 빈이 있으면 그것을 쓰고, 없으면 기본 구현을 만들어 빈으로 등록한다."

**9단계 onRefresh()**는 4단계와 같은 빈자리입니다(907–909행). 설명 주석대로 "특수한 빈을 초기화하되, 싱글톤 인스턴스화 전에" 할 일을 자식이 넣는 곳입니다. 이 저장소의 어느 모듈에도(테스트 코드 하나를 빼면) 이것을 재정의하는 클래스가 없습니다. 바깥 프레임워크를 위한 자리인 셈입니다(Spring Boot가 내장 웹 서버를 여기서 띄우는 것으로 알려져 있는데, Spring Boot는 이 저장소와 별개의 프로젝트라서 소스로 확인한 내용은 아닙니다).

**10단계 registerListeners()**는 세 토막입니다.

	protected void registerListeners() {
		// Register statically specified listeners first.
		for (ApplicationListener<?> listener : getApplicationListeners()) {
			getApplicationEventMulticaster().addApplicationListener(listener);
		}

		// Do not initialize FactoryBeans here: We need to leave all regular beans
		// uninitialized to let post-processors apply to them!
		String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
		for (String listenerBeanName : listenerBeanNames) {
			getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
		}

		// Publish early application events now that we finally have a multicaster...
		Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents;
		this.earlyApplicationEvents = null;
		if (!CollectionUtils.isEmpty(earlyEventsToProcess)) {
			for (ApplicationEvent earlyEvent : earlyEventsToProcess) {
				getApplicationEventMulticaster().multicastEvent(earlyEvent);
			}
		}
	}

spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java 915–936행

첫 토막은 코드로 직접 등록한 리스너, 둘째 토막은 빈으로 등록된 리스너입니다. 둘째에서 눈여겨볼 것은 이름만 넘긴다는 점(addApplicationListenerBean)입니다. 주석대로 아직 빈을 만들면 안 되는 때라, 이름을 적어 두고 실제 객체는 11단계 뒤에 꺼냅니다.

셋째 토막이 1단계에서 만든 earlyApplicationEvents의 쓸모입니다. publishEvent()를 열어 보면 이 집합이 null이 아닌 동안은 이벤트를 보내지 않고 모아 둡니다.

		// Multicast right now if possible - or lazily once the multicaster is initialized
		if (this.earlyApplicationEvents != null) {
			this.earlyApplicationEvents.add(applicationEvent);
		}
		else if (this.applicationEventMulticaster != null) {
			this.applicationEventMulticaster.multicastEvent(applicationEvent, eventType);
		}

spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java 444–450행

1단계에서 집합을 만들고(null이 아니게 되고) 10단계에서 다시 null로 돌리므로, 그 사이에 누가 publishEvent를 부르면 — 예를 들어 5단계의 BeanFactoryPostProcessor가 — 이벤트는 집합에 쌓였다가 10단계에서 한꺼번에 나갑니다. 전달기(8단계)도 리스너(10단계)도 없는 때 보낸 이벤트가 사라지지 않게 한 장치입니다.

이른 이벤트가 지나는 길. 1단계 prepareRefresh 가 earlyApplicationEvents 에 빈 집합을 만든다(null 이 아니게 된다). 1단계와 10단계 사이에 publishEvent()가 불리면(예: 5단계의 BeanFactoryPostProcessor) 이벤트는 보내지지 않고 집합에 쌓인다. 8단계 initApplicationEventMulticaster 가 이벤트 전달기를 만들고, 10단계는 ① 코드로 등록한 리스너와 ② 리스너 빈의 이름을 전달기에 등록한 뒤, ③ 집합을 꺼내고 필드를 null 로 돌려 쌓여 있던 이벤트를 한꺼번에 multicastEvent 로 보낸다. 그 뒤의 publishEvent()는 전달기로 바로 간다 earlyApplicationEvents가 null이 아닌 동안(1~10단계)이 "모아 두는 시간"입니다.

헷갈리기 쉬운 곳

6. 11~12단계: 빈 생성과 운영 시작

**11단계 finishBeanFactoryInitialization()**이 4편에서 본 preInstantiateSingletons()를 부르는 곳입니다. 그 앞에 준비가 몇 줄 있습니다.

	protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
		// ...
		// Initialize conversion service for this context.
		if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
				beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
			beanFactory.setConversionService(
					beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
		}

		// Register a default embedded value resolver if no BeanFactoryPostProcessor
		// (such as a PropertySourcesPlaceholderConfigurer bean) registered any before:
		// at this point, primarily for resolution in annotation attribute values.
		if (!beanFactory.hasEmbeddedValueResolver()) {
			beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
		}
		// ...

		// Stop using the temporary ClassLoader for type matching.
		beanFactory.setTempClassLoader(null);

		// Allow for caching all bean definition metadata, not expecting further changes.
		beanFactory.freezeConfiguration();

		// Instantiate all remaining (non-lazy-init) singletons.
		beanFactory.preInstantiateSingletons();
	}

spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java 943–996행

**12단계 finishRefresh()**는 다섯 줄입니다.

	protected void finishRefresh() {
		// Reset common introspection caches in Spring's core infrastructure.
		resetCommonCaches();

		// Clear context-level resource caches (such as ASM metadata from scanning).
		clearResourceCaches();

		// Initialize lifecycle processor for this context.
		initLifecycleProcessor();

		// Propagate refresh to lifecycle processor first.
		getLifecycleProcessor().onRefresh();

		// Publish the final event.
		publishEvent(new ContextRefreshedEvent(this));
	}

spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java 1003–1018행

예제: 어느 단계에서 무엇이 불리나

5, 6, 11, 12단계에 하나씩 걸쳐 두고 출력 순서를 봅니다.

// 이 글을 위해 쓴 예제입니다 (Spring의 소스가 아닙니다)
package ex.u05;

// ... (import 생략)

public class RefreshOrderExample {

    // 5단계 invokeBeanFactoryPostProcessors — 설계도(BeanDefinition)를 손볼 수 있는 때
    static class LoggingFactoryPostProcessor implements BeanFactoryPostProcessor {
        @Override
        public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
            String name = "refreshOrderExample.Greeter";   // 중첩 클래스라 빈 이름이 이렇게 붙는다
            System.out.println("[5] BeanFactoryPostProcessor: Greeter definition="
                    + beanFactory.containsBeanDefinition(name)
                    + ", instance=" + beanFactory.containsSingleton(name));
        }
    }

    // 6단계 registerBeanPostProcessors 에서 등록되고, 11단계에서 빈마다 불린다
    static class LoggingPostProcessor implements BeanPostProcessor {
        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) {
            if (bean.getClass().getPackageName().startsWith("ex.u05")) {
                System.out.println("[11]   BeanPostProcessor before: " + bean.getClass().getSimpleName());
            }
            return bean;
        }

        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) {
            if (bean.getClass().getPackageName().startsWith("ex.u05")) {
                System.out.println("[11]   BeanPostProcessor after:  " + bean.getClass().getSimpleName());
            }
            return bean;
        }
    }

    // 11단계 finishBeanFactoryInitialization — 평범한 싱글톤 빈 하나
    static class Greeter implements ApplicationContextAware, SmartInitializingSingleton {
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) {
            System.out.println("[11]   ApplicationContextAware: Greeter");
        }

        @PostConstruct
        void init() {
            System.out.println("[11]   @PostConstruct: Greeter");
        }

        @Override
        public void afterSingletonsInstantiated() {
            System.out.println("[11] SmartInitializingSingleton: after every singleton is created");
        }
    }

    // 12단계 finishRefresh — LifecycleProcessor.onRefresh() 가 start() 를 부른다
    static class Starter implements SmartLifecycle {
        private boolean running;

        @Override
        public void start() {
            this.running = true;
            System.out.println("[12] SmartLifecycle.start()");
        }

        @Override
        public void stop() {
            this.running = false;
        }

        @Override
        public boolean isRunning() {
            return this.running;
        }
    }

    // 12단계 finishRefresh — 마지막에 ContextRefreshedEvent
    static class RefreshedListener implements ApplicationListener<ContextRefreshedEvent> {
        @Override
        public void onApplicationEvent(ContextRefreshedEvent event) {
            System.out.println("[12] ContextRefreshedEvent");
        }
    }

    public static void main(String[] args) {
        System.out.println("-- refresh() begins");
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
                LoggingFactoryPostProcessor.class, LoggingPostProcessor.class,
                Greeter.class, Starter.class, RefreshedListener.class);
        System.out.println("-- refresh() done: " + context.getBean(Greeter.class).getClass().getSimpleName());
        context.close();
    }
}

출력입니다.

-- refresh() begins
[5] BeanFactoryPostProcessor: Greeter definition=true, instance=false
[11]   ApplicationContextAware: Greeter
[11]   BeanPostProcessor before: Greeter
[11]   @PostConstruct: Greeter
[11]   BeanPostProcessor after:  Greeter
[11]   BeanPostProcessor before: Starter
[11]   BeanPostProcessor after:  Starter
[11]   BeanPostProcessor before: RefreshedListener
[11]   BeanPostProcessor after:  RefreshedListener
[11] SmartInitializingSingleton: after every singleton is created
[12] SmartLifecycle.start()
[12] ContextRefreshedEvent
-- refresh() done: Greeter

예제 출력을 단계별로 묶은 그림. 5단계 BeanFactoryPostProcessor 에서는 Greeter 의 설계도만 있고 객체는 없다(definition=true, instance=false). 11단계 finishBeanFactoryInitialization 이 preInstantiateSingletons 로 Greeter(ApplicationContextAware → before → @PostConstruct → after), Starter(before → after), RefreshedListener(before → after)를 차례로 만들고, 세 빈이 모두 만들어진 뒤 SmartInitializingSingleton 을 한 번 부른다. 12단계 finishRefresh 에서 LifecycleProcessor.onRefresh()가 SmartLifecycle.start()를 부르고, 마지막에 ContextRefreshedEvent 가 나간다 위 출력의 [5], [11], [12] 줄을 단계 상자에 나눠 담은 것입니다. 순서는 실제 출력 그대로입니다.

읽을 점은 네 가지입니다.

이 예제는 릴리스 버전인 Spring Framework 7.0.9로 실행해 위 출력을 확인했습니다(이 시리즈가 읽는 7.1.0-SNAPSHOT을 직접 빌드해 돌린 것은 아닙니다).

7. 두 갈래: GenericApplicationContext와 AbstractRefreshableApplicationContext

2단계의 refreshBeanFactory()가 추상 메서드였습니다. 이것을 채우는 자식이 둘이고, 채우는 방식이 정반대입니다.

AbstractApplicationContext 아래의 두 갈래. GenericApplicationContext는 생성자에서 BeanFactory를 한 번 만들고 두 번째 refresh를 막으며, AbstractRefreshableApplicationContext는 refresh마다 BeanFactory를 새로 만들어 설계도를 다시 읽는다. 노란 상자가 BeanFactory를 마련하는 방식, 빨간 상자가 두 번째 refresh()의 운명입니다.

**GenericApplicationContext**는 생성자에서 BeanFactory를 한 번 만들어 final 필드에 둡니다.

	private final DefaultListableBeanFactory beanFactory;
	// ...
	private final AtomicBoolean refreshed = new AtomicBoolean();


	/**
	 * Create a new GenericApplicationContext.
	 * @see #registerBeanDefinition
	 * @see #refresh
	 */
	public GenericApplicationContext() {
		this.beanFactory = new DefaultListableBeanFactory();
	}

spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java 114–130행

그래서 refreshBeanFactory()가 할 일은 "두 번째가 아닌지 확인하기"뿐입니다.

	@Override
	protected final void refreshBeanFactory() throws IllegalStateException {
		if (!this.refreshed.compareAndSet(false, true)) {
			throw new IllegalStateException(
					"GenericApplicationContext does not support multiple refresh attempts: just call 'refresh' once");
		}
		this.beanFactory.setSerializationId(getId());
	}

spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java 295–302행

BeanFactory가 처음부터 있다는 것은 refresh() 전에 설계도를 등록할 수 있다는 뜻입니다. registerBean(Class)(499–500행)이나 BeanDefinitionRegistry의 registerBeanDefinition이 그것입니다. 1편의 AnnotationConfigApplicationContext가 이 갈래입니다(AnnotationConfigApplicationContext 57행). 생성자의 register(componentClasses)가 바로 "refresh() 전에 설계도 등록"입니다.

**AbstractRefreshableApplicationContext**는 반대로 refresh()마다 BeanFactory를 새로 만듭니다.

	@Override
	protected final void refreshBeanFactory() throws BeansException {
		if (hasBeanFactory()) {
			destroyBeans();
			closeBeanFactory();
		}
		try {
			DefaultListableBeanFactory beanFactory = createBeanFactory();
			beanFactory.setSerializationId(getId());
			beanFactory.setApplicationStartup(getApplicationStartup());
			customizeBeanFactory(beanFactory);
			loadBeanDefinitions(beanFactory);
			this.beanFactory = beanFactory;
		}
		catch (IOException ex) {
			throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
		}
	}

spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java 118–135행

이미 BeanFactory가 있으면 빈을 소멸시키고 버린 뒤, 새 DefaultListableBeanFactory를 만들고(195–197행) 추상 메서드 loadBeanDefinitions()로 설계도를 다시 읽습니다. XML 파일을 읽는 ClassPathXmlApplicationContext가 이 갈래입니다(ClassPathXmlApplicationContext 53행, loadBeanDefinitions는 AbstractXmlApplicationContext 83행). "설정 파일을 고치고 refresh()를 다시 불러 새로 읽는다"는 쓰임이 설계의 출발점이었고, 메서드 이름이 **"새로 고침"**인 것도 이 갈래 때문입니다. 클래스 설명 주석도 "refresh()를 여러 번 부를 수 있게, 매번 새 BeanFactory를 만드는" 기반 클래스라고 적고 있습니다(29–32행).

GenericApplicationContext AbstractRefreshableApplicationContext
BeanFactory를 만드는 때 생성자에서 한 번 refresh()마다
설계도를 넣는 쪽 바깥에서 (registerBean, BeanDefinitionReader) 안에서 (loadBeanDefinitions)
refresh() 두 번 IllegalStateException 가능 — 빈을 전부 버리고 다시 읽는다
대표 자식 AnnotationConfigApplicationContext ClassPathXmlApplicationContext
getBeanFactory() 늘 같은 객체를 돌려준다(324–326행) 없으면 IllegalStateException(164–171행)

예제: refresh() 전, 후, 두 번째, close() 뒤

// 이 글을 위해 쓴 예제입니다 (Spring의 소스가 아닙니다)
package ex.u05;

// ... (import 생략)

public class RefreshTwiceExample {

    static class Greeter {
        String hello() {
            return "hello";
        }
    }

    public static void main(String[] args) {
        GenericApplicationContext context = new GenericApplicationContext();
        context.registerBean(Greeter.class);           // 설계도만 등록 — 객체는 아직 없다

        try {
            context.getBean(Greeter.class);            // refresh() 전에 꺼내 보면?
        }
        catch (IllegalStateException ex) {
            System.out.println("before refresh, getBean: " + ex.getMessage());
        }

        context.refresh();                             // 컨테이너를 켠다
        System.out.println("after refresh, getBean: " + context.getBean(Greeter.class).hello());

        try {
            context.refresh();                         // 한 번 더 켜 보면?
        }
        catch (IllegalStateException ex) {
            System.out.println("second refresh: " + ex.getMessage());
        }

        context.close();
        try {
            context.getBean(Greeter.class);            // close() 뒤에 꺼내 보면?
        }
        catch (IllegalStateException ex) {
            System.out.println("after close, getBean: " + ex.getMessage());
        }
    }
}
before refresh, getBean: org.springframework.context.support.GenericApplicationContext@6fdb1f78 has not been refreshed yet
after refresh, getBean: hello
second refresh: GenericApplicationContext does not support multiple refresh attempts: just call 'refresh' once
after close, getBean: org.springframework.context.support.GenericApplicationContext@6fdb1f78 has been closed already

첫째와 넷째 줄이 1절의 assertBeanFactoryActive(), 셋째 줄이 위의 refreshBeanFactory()입니다. 같은 IllegalStateException이지만 메시지가 어느 깃발 때문인지를 알려 줍니다. (@6fdb1f78 부분은 객체 해시라 실행마다 다릅니다.) 이 예제도 Spring Framework 7.0.9로 실행했습니다.

헷갈리기 쉬운 곳

8. 실패하면, 그리고 닫을 때

refresh() 도중에 예외가 나면

2절에서 줄였던 catch 블록입니다.

			catch (RuntimeException | Error ex) {
				if (logger.isWarnEnabled()) {
					logger.warn("Exception encountered during context initialization - " +
							"cancelling refresh attempt: " + ex);
				}

				// Stop already started Lifecycle beans to avoid dangling resources.
				if (this.lifecycleProcessor != null && this.lifecycleProcessor.isRunning()) {
					try {
						this.lifecycleProcessor.stop();
					}
					catch (Throwable ex2) {
						logger.warn("Exception thrown from LifecycleProcessor on cancelled refresh", ex2);
					}
				}

				// Destroy already created singletons to avoid dangling resources.
				destroyBeans();

				// Reset 'active' flag.
				cancelRefresh(ex);

				// Propagate exception to caller.
				throw ex;
			}

spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java 628–652행

refresh() 도중 예외가 났을 때의 처리 순서. 경고 로그, 이미 시작된 Lifecycle 빈 중지, 이미 만든 싱글톤 소멸, cancelRefresh로 active 끄기, 예외 다시 던지기, finally에서 락 풀기. 빨간 상자가 들어오는 예외와 나가는 예외입니다. 그 사이에서 만들어 둔 것을 전부 치웁니다.

ConfigurableApplicationContext.refresh()의 설명 주석에 원칙이 적혀 있습니다. "이 메서드가 끝난 뒤에는 모든 싱글톤이 만들어졌거나, 하나도 만들어지지 않았거나 둘 중 하나여야 한다"(212–221행). 그래서 11단계에서 열 번째 빈을 만들다 실패하면 앞의 아홉 개도 destroyBeans()로 소멸시키고(645행, 구현은 1246–1248행), cancelRefresh()가 active를 끄고 캐시를 비웁니다(1025–1030행). 예외는 삼키지 않고 그대로 다시 던지므로 new AnnotationConfigApplicationContext(...)를 부른 쪽이 받습니다. 4단계부터 12단계까지만 try 안에 있다는 점도 눈여겨볼 만합니다. 1~3단계는 빈을 만들지 않으므로 치울 것이 없습니다.

close()와 doClose()

close()는 락을 잡고 doClose()를 부른 뒤, 등록해 둔 JVM 종료 훅이 있으면 떼어 냅니다(1146–1171행). 실제 일은 doClose()에 있습니다.

	protected void doClose() {
		// Check whether an actual close attempt is necessary...
		if (this.active.get() && this.closed.compareAndSet(false, true)) {
			// ...
			try {
				// Publish shutdown event.
				publishEvent(new ContextClosedEvent(this));
			}
			catch (Throwable ex) {
				logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);
			}

			// Stop all Lifecycle beans, to avoid delays during individual destruction.
			if (this.lifecycleProcessor != null) {
				try {
					this.lifecycleProcessor.onClose();
				}
				// ...
			}

			// Destroy all cached singletons in the context's BeanFactory.
			destroyBeans();

			// Close the state of this context itself.
			closeBeanFactory();

			// Let subclasses do some final clean-up if they wish...
			onClose();

			// Reset common introspection caches to avoid class reference leaks.
			resetCommonCaches();
			// ...

			// Reset internal delegates.
			this.applicationEventMulticaster = null;
			this.messageSource = null;
			this.lifecycleProcessor = null;

			// Switch to inactive.
			this.active.set(false);
		}
	}

spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java 1182–1233행

close()의 흐름. 락을 잡고, active이며 아직 닫히지 않았을 때만 ContextClosedEvent 발행, Lifecycle 빈 중지, 싱글톤 소멸, BeanFactory 닫기, 캐시 비우기와 active 끄기를 차례로 한다. 켜질 때와 거꾸로입니다. 이벤트를 먼저 보내고, 돌던 것을 멈추고, 그 다음 빈을 없앱니다.

순서가 켜질 때와 거꾸로입니다. 12단계에서 start() → ContextRefreshedEvent였다면, 닫을 때는 ContextClosedEvent → stop()(onClose()는 stopBeans를 부릅니다 — DefaultLifecycleProcessor 336–339행) → 빈 소멸입니다. 빈이 살아 있는 동안 "닫힌다"는 소식을 먼저 알리고, 돌고 있던 것을 멈춘 뒤에야 없앱니다. 맨 앞의 if는 closed를 compareAndSet으로 켜므로 close()를 두 번 불러도 두 번째는 조용히 지나갑니다.

destroyBeans()가 부르는 destroySingletons()는 소멸 콜백이 있는 빈의 이름 목록을 뒤에서부터 돕니다(DefaultSingletonBeanRegistry 703–705행). 그 목록에는 빈이 만들어진 순서대로 들어가므로(AbstractAutowireCapableBeanFactory 640행), 나중에 만들어진 빈이 먼저 소멸합니다.

예제: close() 때의 순서

// 이 글을 위해 쓴 예제입니다 (Spring의 소스가 아닙니다)
package ex.u05;

// ... (import 생략)

public class CloseOrderExample {

    // 먼저 등록된 빈 — 소멸은 나중에
    static class Repository implements DisposableBean {
        @Override
        public void destroy() {
            System.out.println("[3] DisposableBean.destroy(): Repository");
        }
    }

    // 나중에 등록된 빈 — 소멸은 먼저
    static class Service {
        @PreDestroy
        void shutdown() {
            System.out.println("[3] @PreDestroy: Service");
        }
    }

    static class Starter implements SmartLifecycle {
        private boolean running;

        @Override
        public void start() {
            this.running = true;
        }

        @Override
        public void stop() {
            this.running = false;
            System.out.println("[2] SmartLifecycle.stop()");
        }

        @Override
        public boolean isRunning() {
            return this.running;
        }
    }

    static class ClosedListener implements ApplicationListener<ContextClosedEvent> {
        @Override
        public void onApplicationEvent(ContextClosedEvent event) {
            System.out.println("[1] ContextClosedEvent");
        }
    }

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
                Repository.class, Service.class, Starter.class, ClosedListener.class);
        System.out.println("-- close() begins");
        context.close();
        System.out.println("-- close() done: isActive=" + context.isActive() + ", isClosed=" + context.isClosed());
    }
}
-- close() begins
[1] ContextClosedEvent
[2] SmartLifecycle.stop()
[3] @PreDestroy: Service
[3] DisposableBean.destroy(): Repository
-- close() done: isActive=false, isClosed=true

이벤트 → stop() → 소멸의 순서와, Repository보다 나중에 등록된 Service가 먼저 소멸하는 것이 보입니다. 이 예제도 Spring Framework 7.0.9로 실행했습니다.

헷갈리기 쉬운 곳

더 깊이 들어가면

이 글이 이름만 적고 지나간 것들입니다. 7.x의 restart()·pause()와 ContextRestartedEvent·ContextPausedEvent(ConfigurableApplicationContext 234–244행). 부모 컨텍스트로 이벤트를 함께 전파하는 publishEvent의 뒷부분(453–460행). 종료 훅이 락을 못 잡을 때 기동 스레드가 System.exit로 멈춰 있는지 알아내는 isStartupShutdownThreadStuck()(1118–1136행). 11단계 앞부분의 bootstrapExecutor(빈을 백그라운드 스레드에서 만드는 실행기, 948–952행)와 BeanFactoryInitializer(969–972행). 3단계와 5단계에 나오는 LoadTimeWeaver(클래스 로딩 시점에 바이트코드를 바꾸는 장치, 757–761행). refresh() 대신 쓰는 AOT 처리용 refreshForAotProcessing(GenericApplicationContext, 16편). StartupStep으로 기동 시간을 재는 ApplicationStartup(587행). 그리고 Environment·MessageSource·이벤트 전달기의 내부는 12편에서 봅니다.

정리

개념 한 줄 요약
ApplicationContext BeanFactory 둘 + Environment, MessageSource, 이벤트 발행, 자원 읽기. 읽기 전용
ConfigurableApplicationContext 거기에 refresh(), close(), Lifecycle을 더한 "켜고 끄는" 쪽
AbstractApplicationContext 위를 구현. BeanFactory를 상속하지 않고 안에 품어서 위임한다
refresh() 열두 단계. 준비(1~3) → 설계도 가공(4~6) → 부속 준비(7~10) → 빈 생성과 운영 시작(11~12)
3단계 prepareBeanFactory Aware 후처리기, 리스너 감지기, @Autowired ApplicationContext가 되게 하는 등록
5단계와 6단계 BeanFactoryPostProcessor는 여기서 실행, BeanPostProcessor는 여기서 등록만
이른 이벤트 1단계~10단계 사이에 보낸 이벤트는 모아 뒀다가 10단계에서 보낸다
11단계 freezeConfiguration 뒤 preInstantiateSingletons. 끝에 SmartInitializingSingleton
12단계 Lifecycle.start() 다음 ContextRefreshedEvent
GenericApplicationContext BeanFactory를 생성자에서 한 번. refresh() 전에 등록 가능, 두 번째 refresh()는 예외
AbstractRefreshableApplicationContext refresh()마다 새 BeanFactory에 설계도를 다시 읽는다
실패 만든 싱글톤을 전부 소멸시키고 active를 끈 뒤 예외를 다시 던진다
close() ContextClosedEvent → stop() → 빈 소멸(만든 순서의 거꾸로) → active 끄기

다음에 볼 것

5단계에서 "ConfigurationClassPostProcessor가 @Configuration 클래스를 읽어 설계도를 추가한다"고 한 줄로 지나갔습니다. @Bean 메서드 하나가 어떻게 BeanDefinition이 되는지, @ComponentScan은 클래스패스를 어떻게 뒤지는지, @Configuration 클래스는 왜 CGLIB으로 감싸지는지가 거기에 들어 있습니다. 8편: @Configuration과 @ComponentScan은 어떻게 설계도가 되나에서 봅니다.

다른 글