profile image

L o a d i n g . . .

728x90

Spring boot를 사용하다 보면 이 마법 같은 프레임워크가 어떻게 동작하는지 궁금할 때가 있습니다. Spring boot는 어떻게 실행되는지, 자동 설정은 어떻게 수행되는지 등... Spring boot를 자주 사용하지만 정작 내부 원리를 알지 못하니 겉핥기 수준으로 Spring boot를 사용하고 있다는 느낌을 떨칠 수가 없습니다. 그래서 이번 포스팅을 계기로 조금 내부 원리를 이해해보고자 합니다. Spring boot도 결국에는 Java program이기 때문에 시작점은 main()입니다. 따라서 SpringApplication.run(SpringBootAutowireApplication.class, args); 가 Spring boot의 시작 지점입니다.

@SpringBootApplication
public class SpringBootAutowireApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootAutowireApplication.class, args);
    }
}

 

SpringApplication.run(...)

디버깅 모드를 통해 어떤 코드가 실행되는지 확인해보겠습니다.

public ConfigurableApplicationContext run(String... args) {
    long startTime = System.nanoTime(); // (1) 
    DefaultBootstrapContext bootstrapContext = this.createBootstrapContext(); // (2) 
    ConfigurableApplicationContext context = null; // (3) 
    this.configureHeadlessProperty(); // (4) 
    SpringApplicationRunListeners listeners = this.getRunListeners(args); // (5)
    listeners.starting(bootstrapContext, this.mainApplicationClass); // (6)

    try {
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); // (7)
        ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments); // (8)
        this.configureIgnoreBeanInfo(environment); // (9) 
        Banner printedBanner = this.printBanner(environment); // (10) 
        context = this.createApplicationContext(); // (11) 
        context.setApplicationStartup(this.applicationStartup); // (12)
        this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner); // (13)
        this.refreshContext(context); // (14)
        this.afterRefresh(context, applicationArguments); // (15)
        Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime); // (16)
        if (this.logStartupInfo) { // (17)
            (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), timeTakenToStartup);
        }

        listeners.started(context, timeTakenToStartup); // (18) 
        this.callRunners(context, applicationArguments); // (19)
    } catch (Throwable var12) {
        this.handleRunFailure(context, var12, listeners); // (20) 
        throw new IllegalStateException(var12); 
    }

    try {
        Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime); // (21) 
        listeners.ready(context, timeTakenToReady); // (22) 
        return context; 
    } catch (Throwable var11) {
        this.handleRunFailure(context, var11, (SpringApplicationRunListeners)null); 
        throw new IllegalStateException(var11);
    }
}

위의 코드는 앞서 봤었던 SpringApplication.run(SpringBootAutowireApplication.class, args);가 내부적으로 호출하는 메서드입니다. Spring boot의 시작에 필요한 관련 함수들을 모두 호출하는 걸 볼 수 있는데 하나씩 살펴보겠습니다.

 

long startTime = System.nanoTime();

long startTime = System.nanoTime();은 SpringApplicationRunListeners의 메서드를 호출할 때 인자로 넘겨주기 위해 사용되는 시간입니다. 위 코드에서 SpringApplicationRunListeners는 3 가지 메서드를 호출합니다(starting, started, ready). 각각의 메서드 용도를 살펴보면 다음과 같습니다(괄호 내의 숫자는 위의 코드에 표시된 숫자와 일치합니다).

  • (6) starting(ConfigurableBootstrapContext bootstrapContext): run메서드가 처음 실행될 때 호출되는 메서드입니다. 
  • (18) started(ConfigurableApplicationContext context, Duration timeTaken): context가 refreshed 된 상태이지만 CommandLineRunner(s)와 ApplicationRunner(s)가 실행되기 전에 호출되는 메서드입니다.
  • (23) ready(ConfigurableApplicationContext context, Duration timeTaken): run메서드가 종료되기 직전에 호출되는 메서드입니다. 모든 CommandLineRunner(s)와 ApplicationRunner(s)가 실행된 이후 호출됩니다.

 

DefaultBootstrapContext bootstrapContext = this.createBootstrapContext();

Spring BootstrapContext 인터페이스의 기본 구현체입니다. BootstrapContext 인터페이스는 Spring의 TestContext Framework가 bootstrap 되는 콘텍스트를 캡슐화한 인터페이스입니다.

 

ConfigurableApplicationContext context = null;

ApplicationContext 인터페이스를 상속한 인터페이스로 ApplicationContext의 lifecycle과 관련된 인터페이스입니다. Javadoc을 보면 startup과 shutdown 관련 메서드만 사용이 권고됩니다.

public interface ConfigurableApplicationContext extends ApplicationContext, Lifecycle, Closeable

 

this.configureHeadlessProperty();

private void configureHeadlessProperty() {
    System.setProperty("java.awt.headless", System.getProperty("java.awt.headless", Boolean.toString(this.headless)));
}

headless 속성이 true라면 Java application은 window 또는 dialog boxes를 표시하지 않고, 키보드 또는 마우스 입력을 받지 않으며 AWT(Abstract Window Tollkit) 컴포넌트를 사용하지 않습니다. Spring application은 기본적으로 headless 속성을 true로 설정합니다.

 

SpringApplicationRunListeners listeners = this.getRunListeners(args);

Spring run 메서드의 listener을 설정하는 부분입니다.

private SpringApplicationRunListeners getRunListeners(String[] args) {
    Class<?>[] types = new Class[]{SpringApplication.class, String[].class};
    return new SpringApplicationRunListeners(logger, 
        this.getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args), 
        this.applicationStartup);
}

해당 코드를 따라가 보면 spring.factories에 설정된 클래스를 등록하는 코드를 확인할 수 있습니다.

spring.factories에 등록된 클래스를 확인하는 원리

Spring은 Property properties = PropertiesLoadUtils.loadProperties(resource)를 통해서 spring.factories에 등록된 클래스를 읽습니다.

 

listeners.starting(bootstrapContext, this.mainApplicationClass)

Spring의 Environment 또는 ApplicationContext가 생성되기 전 실행되는 listener입니다. ApplicationContext가 생성되기 전에 실행되기 때문에 listener를 @Component로 등록해서 사용할 수 없습니다. ApplicationContext가 생성된 이후 실행되는 ApplicationStartedEvent 또는 ApplicationReadyEvent listener는 @Component로 등록해서 사용이 가능합니다.

@SpringBootApplication
public class SpringBootAutowireApplication {
    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(SpringBootAutowireApplication.class);
        application.addListeners(new StartingEventListener());
        application.run(args);
    }
}

class StartingEventListener implements ApplicationListener<ApplicationStartingEvent> {

    @Override
    public void onApplicationEvent(ApplicationStartingEvent event) {
        System.out.println("starting event");
    }
}

@Component
class StartedEventListener implements ApplicationListener<ApplicationStartedEvent> {

    @Override
    public void onApplicationEvent(ApplicationStartedEvent event) {
        System.out.println("started event");
    }
}

@Component
class ReadyEventListener implements ApplicationListener<ApplicationReadyEvent> {

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        System.out.println("ready event");
    }
}

애플리케이션을 실행하면 출력은 다음과 같습니다.

SpringApplicationEvent 등록 후 실행 결과

다음은 SpringApplicationEvent subClass 목록입니다. 

  • ApplicationContextInitializedEvent
  • ApplicationEnvironmentPreparedEvent
  • ApplicationFailedEvent
  • ApplicationReadyEvent
  • ApplicationStartedEvent
  • ApplicationStartingEvent

 

ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

SpringApplication의 run 메서드 실행 시 제공한 인자(args)에 접근하고 활용할 수 있는 객체입니다.

 

ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);

ConfigurableEnvironment는 Spring boot 애플리케이션의 environment 값과 관련된 클래스입니다. 해당 클래스를 통해 Property source를 지정하고 우선순위를 변경할 수도 있습니다.

Property source의 우선순위

 

24. Externalized Configuration

Getters and setters are usually mandatory, since binding is via standard Java Beans property descriptors, just like in Spring MVC. There are cases where a setter may be omitted:Maps, as long as they are initialized, need a getter but not necessarily a sett

docs.spring.io

 

this.prepareEnvironment(listeners, bootstrapContext, applicationArguments)는 spring boot의 환경변수를 설장힐 떼 사용하는 source를 지정하는 코드입니다. 해당 코드에 대한 상세 설명은 다음 포스팅을 참고해주세요.

2022.05.01 - [Java] - [Spring Boot] 알쏭달쏭한 환경변수 설정원리

 

[Spring Boot] 알쏭달쏭한 환경변수 설정원리

해당 포스팅은 [그래서 Spring Boot는 어떻게 실행되는겁니까?] 포스팅의 일부입니다. 그래서 Spring Boot는 어떻게 실행되는겁니까 ? Spring boot를 사용하다 보면 이 마법 같은 프레임워크가 어떻게 동

code-run.tistory.com

 

this.configureIgnoreBeanInfo(environment);

private void configureIgnoreBeanInfo(ConfigurableEnvironment environment) {
    if (System.getProperty("spring.beaninfo.ignore") == null) {
        Boolean ignore = (Boolean)environment.getProperty("spring.beaninfo.ignore", Boolean.class, Boolean.TRUE);
        System.setProperty("spring.beaninfo.ignore", ignore.toString());
    }
}

spring.beaninfo.ignore은 Spring property의 일종인데, Spring property는 Spring application의 low-level 설정 중 하나입니다. spring.beaninfo.ignore을 통해서 Spring이 JavaBeans Introspector을 호출할 때 Introspector.IGNORE_ALL_BEANINFO 모드로 실행될 수 있도록 합니다. Introspector.IGNORE_ALL_BEANINFO는 classloader가 JavaBean의 BeanInfo를 탐색하지 않도록 설정합니다. JavaBean, Introspector, BeanInfo에 대해 궁금하신 분들은 하단의 포스팅을 참고하세요.

2022.05.02 - [Java] - [Java] 처음 들어보는 java.beans.Introspector

 

[Java] 처음 들어보는 java.beans.Introspector

Docs에서 찾아본 Java의 Introspector의 기능은 .. 다음과 같습니다. The Introspector class provides a standard way for tools to learn about the properties, events, and methods supported by a target Jav..

code-run.tistory.com

 

(Banner printedBanner = this.printBanner(environment);

Spring boot가 실행될 때 터미널에 배너를 표시하는 코드입니다.

private Banner printBanner(ConfigurableEnvironment environment) {
    if (this.bannerMode == Mode.OFF) {
        return null;
    } else {
        ResourceLoader resourceLoader = this.resourceLoader != null ? this.resourceLoader : new DefaultResourceLoader((ClassLoader)null);
        SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter((ResourceLoader)resourceLoader, this.banner);
        return this.bannerMode == Mode.LOG ? bannerPrinter.print(environment, this.mainApplicationClass, logger) : bannerPrinter.print(environment, this.mainApplicationClass, System.out);
    }
}

this.bannerMode는 default로 Mode.CONSOLE 설정되기 때문에 별다른 설정이 없으면 배너는 console에 표시됩니다.

 

context = this.createApplicationContext();

ApplicationContext를 생성하는 strategy 메서드입니다. 별다른 설정을 하지 않게 되면 SpringApplication 초기화할 때 DEFAULT ApplicationContextFactory을 사용하게 됩니다. 

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    ...
    
    this.applicationContextFactory = ApplicationContextFactory.DEFAULT;
	
    ...
}

Spring의 ApplicationContext가 궁금하시다면 다음 포스팅을 참고해주세요. 

2022.05.05 - [Java] - [Spring Boot] 객체들이 살고있는 집 ApplicationContext 살펴보기

 

[Spring Boot] 객체들이 살고있는 집 ApplicationContext 살펴보기

해당 포스팅은 [Spring Boot는 대체 어떻게 실행되는걸까 ?] 포스팅의 일부입니다. 2022.05.01 - [Java] - Spring Boot는 대체 어떻게 실행되는걸까 ? 개요 ApplicationContext는 Spring의 advanced container입..

code-run.tistory.com

 

context.setApplicationStartup(this.applicationStartup); 

Spring의 ApplicationStartup interface는 application이 시작될 때 각 단계별로 처리 시간과 같은 관련된 데이터를 수집합니다. this.applicationStartup은 SpringApplication 객체가 초기화될 때 ApplicationStartup.DEFAULT값으로 설정됩니다. 

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    ...
    this.applicationStartup = ApplicationStartup.DEFAULT;
    ...
}

ApplicationStartup.DEFAULT는 DefaultApplicationStartup 객체입니다. DefaultApplicationStartup은 아무런 옵션이 설정되지 않은 ApplicationStartup의 구현체입니다. 따라서 데이터를 따로 수집하지 않습니다. 

public interface ApplicationStartup {
    ApplicationStartup DEFAULT = new DefaultApplicationStartup();

    StartupStep start(String name);
}

ApplicationStartup의 구현체로 BufferingApplicationStartup과 FlightRecorderApplicationStartup이 존재합니다. 해당 구현체를 사용하면 Spring application이 시작되는 각 단계별(step)로 걸린 시간을 상세히 파악할 수 있습니다. spring-boot-starter-web과 spring-boot-starter-actuator의존성을 더하고, 각각 BufferingApplicationStartup과 FlightRecorderApplicationStartup이 제공하는 정보를 확인해보겠습니다(의존성 및 코드는 해당 링크를 참조해주세요).

BufferingApplicationStartup을 설정하고 Spring의 endpoint를 확인했을 때 결과입니다(지면상 일부만 소개하도록 하겠습니다).

BufferingApplicationStartup을 사용할 때 Spring actuator endpoint 조회 결과

위와 같이 BufferingApplicationStartup을 사용하면 Spring이 시작되는 각 단계(startupStep) 별로 시간이 어느 정도 걸리는지 확인할 수 있습니다. 다음으로는 FlightRecorderApplicationStartup를 사용해보겠습니다. 다음과 같이 SpringApplication을 설정하고 jar파일을 만들어주겠습니다. 

@SpringBootApplication
public class ExampleSpringBootApplication {
    public static void main(String[] args) {
        SpringApplication springApplication = new SpringApplication(ExampleSpringBootApplication.class);
        springApplication.setApplicationStartup(new FlightRecorderApplicationStartup());
        springApplication.run(args);
    }
}
./mvnw package

. jar 파일이 생성되면 다음과 같이 커맨드를 실행합니다. 

java -XX:StartFlightRecording:filename=recording.jfr,duration=10s -jar target/example-spring-boot-application-0.0.1-SNAPSHOT.jar

해당 커맨드 실행의 결과로 recording.jfr 파일이 생성되는데, 해당 파일을 통해서 flight recording 결과를 확인할 수 있습니다. 

FlightRecorderApplicationStartup 결과

 

this.prepareContext(bootstrapContext,...); 

this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner); 메서드는 다음과 같습니다. 

private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
    context.setEnvironment(environment); // 1
    this.postProcessApplicationContext(context); // 2
    this.applyInitializers(context); // 3
    listeners.contextPrepared(context); // 4
    bootstrapContext.close(context); // 5
    if (this.logStartupInfo) { // 6
        this.logStartupInfo(context.getParent() == null);
        this.logStartupProfileInfo(context);
    }

    ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
    beanFactory.registerSingleton("springApplicationArguments", applicationArguments); // 7
    if (printedBanner != null) { // 8
        beanFactory.registerSingleton("springBootBanner", printedBanner); 
    }

    if (beanFactory instanceof AbstractAutowireCapableBeanFactory) { // 9
        ((AbstractAutowireCapableBeanFactory)beanFactory).setAllowCircularReferences(this.allowCircularReferences);
        if (beanFactory instanceof DefaultListableBeanFactory) {
            ((DefaultListableBeanFactory)beanFactory).setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
        }
    }

    if (this.lazyInitialization) { // 10
        context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
    }

    Set<Object> sources = this.getAllSources(); // 11
    Assert.notEmpty(sources, "Sources must not be empty");
    this.load(context, sources.toArray(new Object[0])); // 12
    listeners.contextLoaded(context); // 13
}

주요 코드 부분을 살펴보겠습니다. 

 

context.setEnvironment(environment);

ConfigurableApplicationContext에 ConfigurableEnvironment를 설정하는 코드입니다. ConfigurableEnvironment는 spring의 property source와 관련된 클래스입니다. ConfigurableEnvironment는  active, default property source의 설정, properties 관련 검증, conversion service에 대한 커스터마이징 기능을 제공합니다. 

 

this.postProcessApplicationContext(context); 

ApplicationContext와 관련된 postProcessing 작업을 진행합니다. ApplicationContext와 관련된 beanNameGenerqator, resourceLoader, conversionService 설정을 진행합니다. 

더보기
protected void postProcessApplicationContext(ConfigurableApplicationContext context) {
    if (this.beanNameGenerator != null) {
        context.getBeanFactory().registerSingleton("org.springframework.context.annotation.internalConfigurationBeanNameGenerator", this.beanNameGenerator);
    }

    if (this.resourceLoader != null) {
        if (context instanceof GenericApplicationContext) {
            ((GenericApplicationContext)context).setResourceLoader(this.resourceLoader);
        }

        if (context instanceof DefaultResourceLoader) {
            ((DefaultResourceLoader)context).setClassLoader(this.resourceLoader.getClassLoader());
        }
    }

    if (this.addConversionService) {
        context.getBeanFactory().setConversionService(context.getEnvironment().getConversionService());
    }

}

 

this.applyInitializers(context);

ApplicationContext가 refresh 되기 전에 ApplicationContextInitializer(s)를 적용합니다. ApplicationContextInitializer는 Spring의 ConfigurableApplicationContext가 초기화되는 과정에서, refresh 되기 전 callback 메서드를 실행하기 위한 인터페이스입니다. 

더보기
@FunctionalInterface
public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {
    void initialize(C applicationContext);
}

 

listeners.contextPrepared(context);

ApplicationContext가 생성되고 준비됐지만 아직 sources는 등록되지 않은 단계에서 SpringApplicationRunListeners을 호출하는 메서드입니다. 

 

bootstrapContext.close(context); 

ApplicationContext가 준비되고 BootstrapContext가 close 되기 직전 호출되는 메서드입니다. 

더보기
public void close(ConfigurableApplicationContext applicationContext) {
    this.events.multicastEvent(new BootstrapContextClosedEvent(this, applicationContext));
}

 

logStartupInfo 

Spring boot application이 시작함을 알리는 startup 로그를 남깁니다. 

logStartupInfo

beanFactory.registerSingleton("springApplicationArguments", applicationArguments); 

Spring Boot를 실행할 때 전달한 인자를 "springApplicationArguments" 이름의 singleton bean으로 등록합니다.


printBanner...

Spring Boot를 실행할 때 콘솔에 출력되는 Banner를 "springBootBanner" 이름의 singleton bean으로 등록합니다. 

 

beanFactory...

등록된 bean의 circular reference를 허용할지 안 할지 정합니다. 기본 설정은 false입니다. 만약 true로 설정된다면 circular reference 문제를 해결하려고 시도합니다. 

 

lazyInitialization...

Spring bean의 lazy initialization과 관련된 설정입니다. 

 

sources...

ApplicationContext에서 사용할 sources를 Immutable Set으로 반환합니다. 

더보기
public Set<Object> getAllSources() {
    Set<Object> allSources = new LinkedHashSet();
    if (!CollectionUtils.isEmpty(this.primarySources)) {
        allSources.addAll(this.primarySources);
    }

    if (!CollectionUtils.isEmpty(this.sources)) {
        allSources.addAll(this.sources);
    }

    return Collections.unmodifiableSet(allSources);
}

 

this.load(context, sources.toArray(new Object[0]));

ApplicationContext에 bean을 등록합니다. 

 

listeners.contextLoaded(context); 

refresh 되기 전의 ApplicationContext가 등록된 상태에서 SpringApplicationRunListeners를 호출하는 메서드입니다. 

 

this.refreshContext(context);

refreshContext는 Spring boot의 configuration과 관련된 작업을 진행합니다(bean등록, bean 후처리 등). 상세한 설명은 다음 포스팅을 참고해주세요. 

2022.05.11 - [Java] - [Spring Boot] refresh context 차근차근 따라가기

 

[Spring Boot] refresh context 차근차근 따라가기

해당 포스팅은 2022.05.01 - [Java] - Spring Boot는 대체 어떻게 실행되는걸까 ? 포스팅의 일부입니다. Spring Boot의 ApplicationContext가 초기화되는 과정에서 refresh 과정이 있습니다. 이번 포스팅을 통해..

code-run.tistory.com

 

this.afterRefresh(context, applicationArguments);

ApplicationContext가 refresh 된 이후 호출되는 함수입니다. 해당 메서드를 Override 함으로써 추가적인 로직을 수행할 수 있습니다. 

 

Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);

(1) 번에서 측정한 시간(startTime)과 refresh 작업이 완료된 현재까지 걸린 시간을 측정합니다. 

 

if (this.logStartupInfo) ... 

Spring boot application을 실행하는데 소요된 시간을 console에 출력합니다. 

Spring boot application을 실행하는데 소요된 시간을 출력

listeners.started(context, timeTakenToStartUp); 

SpringAppilcationRunListener에게 ApplicationContext가 refresh 된 후 started 된 상태임을 알립니다. CommandLineRunner(s)과 ApplicationRunner(s)은 아직 실행되지 않은 상태입니다. 

 

this.callRunners(context, applicationArguments); 

CommandLineRunner(s)와 ApplicationRunner(s)을 실행시킵니다. 

private void callRunners(ApplicationContext context, ApplicationArguments args) {
    List<Object> runners = new ArrayList();
    runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
    runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
    AnnotationAwareOrderComparator.sort(runners);
    Iterator var4 = (new LinkedHashSet(runners)).iterator();

    while(var4.hasNext()) {
        Object runner = var4.next();
        if (runner instanceof ApplicationRunner) {
            this.callRunner((ApplicationRunner)runner, args);
        }

        if (runner instanceof CommandLineRunner) {
            this.callRunner((CommandLineRunner)runner, args);
        }
    }
}

 

this.handleRunFailure(context, var12, listeners); 

SpringApplication.run()에서 ApplicationContext를 설정하는 도중 Exception이 발생할 때, Exception을 처리하는 코드입니다. 등록된 listener에게 failed 이벤트를 발행하거나 shutdownHook을 실행하는 등, 애플리케이션이 정상적으로 종료될 수 있도록 처리하는 로직이 포함돼있습니다. 

 

Duration timeTakenReady = Duration.ofNanos(System.nanoTime() - startTime); 

(22) 번에 사용하기 위해서 startTime부터 현재까지 걸린 시간을 측정합니다. 

 

listeners.ready(context, timeTakenToReady) 

SpringApplication.run() 메서드가 종료되기 바로 직전에 호출되는 메서드입니다. 해당 메서드가 호출되는 것은 ApplicationContext가 정상적으로 refresh 됐으며 등록된 모든 CommandLineRunner(s)와 ApplicationRunner(s)가 호출됐음을 의미합니다.

 

Reference 

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/env/ConfigurableEnvironment.html

https://www.baeldung.com/circular-dependencies-in-spring

728x90
복사했습니다!