본문 바로가기
Web/Spring

[Spring] 스프링 레퍼런스/소스 구석구석까지 탈탈 털기 - 1

by Geunny 2024. 10. 8.
반응형

개발 경력이 어느덧 5년 차에 접어들었다. 그동안 스프링을 사용해 다양한 프로젝트를 진행해왔고, 이제는 API를 빠르게 만드는 데 어느 정도 익숙해졌다.

 

하지만, 그렇게 만들어내는 내자신이 어느 순간부터 ‘API 공장’처럼 느껴지기 시작했다.

기능은 동작하지만, 그 안에 숨어 있는 더 나은 설계나 최적화 기회들을 놓치고 있는 듯한 기분이었다.

 

이제는 스프링을 단순히 기능을 구현하는 도구로만 쓰기보다는, 그 이면에 숨겨진 원리와 철학을 더 깊이 이해해야 할 시점이 아닐까 하는 생각이 들었다. API의 성능이나 구조적인 문제를 마주할 때마다 “이게 정말 이렇게 만드는 게 맞나?“라는 질문이 끊임없이 떠올랐다.

그럴 때마다 스스로 해답을 찾기 위해 여러 자료들을 뒤져보곤 했지만, 명확한 방향을 찾기가 쉽지 않았다.

 

그래서 이번에는 조금 다른 접근을 해보기로 했다. 스프링이라는 도구를 더 깊이 이해하고, 내가 만든 API의 개선점을 명확히 잡아나갈 수 있는 실마리를 찾아보려고 한다. 이를 위해 스프링 레퍼런스 가이드를 제대로 파헤쳐 보며, 내가 놓친 부분들을 하나씩 점검해볼 생각이다.

 

 

https://docs.spring.io/spring-boot/reference/features/spring-application.html

 

SpringApplication :: Spring Boot

SpringApplication allows an application to be initialized lazily. When lazy initialization is enabled, beans are created as they are needed rather than during application startup. As a result, enabling lazy initialization can reduce the time that it takes

docs.spring.io

 

오늘 파해칠 첫번째 단락이다. 기본 스프링의 경우 문서는 너무 방대하여 SpringBoot 문서를 위주로 다뤄볼 예정이다.

 

1. SpringApplication

 

먼저 Spring의 시작이 되는 클래스이다.

@SpringBootApplication 어노테이션을 통해 스프링이 시작되는 클래스를 선언할 수 있다.

 

 

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MyTestApplication {

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

}

 

1-1. @SpringBootApplication 

 

일반적인 Spring 프레임워크와 Spring Boot의 가장 큰 차이점 중 하나는 자동 설정간편한 구동이다. 그 중심에는 바로 @SpringBootApplication 어노테이션이 있다. 이 어노테이션은 단순히 애플리케이션의 시작점을 표시하는 것 이상으로, Spring Boot의 핵심적인 동작을 결정짓는 중요한 역할을 한다.

 

1-2. @SpringBootApplication의 역할

 

해당 어노테이션의 상단부 옵션을 보면 다음과 같다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
...

 

위 소스의 예시와 같이 @SpringBootApplication은 사실 세 가지 중요한 어노테이션을 결합한 복합 어노테이션이다.

 

1. @SpringBootConfiguration:

이 어노테이션은 스프링 애플리케이션의 설정 클래스를 지정한다. 애플리케이션 구동 시 설정을 위한 빈(Bean)을 정의하고, 이를 통해 애플리케이션의 전반적인 동작을 조율한다. 이는 기존 Spring의 @Configuration과 동일한 역할을 수행하지만, Spring Boot의 설정을 관리하는 특수한 버전이라 할 수 있다.

 

2. @EnableAutoConfiguration:

Spring Boot의 강력한 기능 중 하나인 자동 설정을 활성화한다. Spring Boot는 클래스 경로에 있는 라이브러리를 분석하여 애플리케이션에 적절한 빈들을 자동으로 구성해준다. 예를 들어, spring-boot-starter-web이 있다면 Spring Boot는 이를 감지하고 자동으로 내장 웹 서버와 관련된 설정을 추가해준다. 이 덕분에 우리는 별도의 설정 없이도 바로 애플리케이션을 실행할 수 있다.

 

3. @ComponentScan:

이 어노테이션은 컴포넌트 스캔을 통해, 특정 패키지와 그 하위 패키지에 있는 빈(Bean)들을 자동으로 탐색하여 스프링 컨텍스트에 등록한다. 이를 통해 개발자는 별도로 빈을 수동으로 등록할 필요 없이, 애플리케이션이 자동으로 필요한 빈을 스캔하고 관리할 수 있게 된다.

 

이러한 컴포넌트들은 모두 스프링 컨테이너에 저장되며, 스프링 컨테이너는 애플리케이션의 빈을 관리하고 의존성을 주입하는 핵심 역할을 담당한다. 스프링 컨테이너에 대해서는 추후에 더 자세히 설명할 예정이다.

 

1-3. Spring과 Spring Boot의 차이점에서 @SpringBootApplication의 중요성

 

전통적인 Spring 애플리케이션에서는 구성 파일을 작성하고, 애플리케이션의 모든 빈을 수동으로 설정하는 것이 일반적이었다. 반면에, Spring Boot는 자동 설정을 통해 이러한 반복적인 설정 작업을 대폭 줄여준다. 그 중심에 있는 @SpringBootApplication은 단순한 어노테이션이 아니라, Spring Boot의 철학인 “컨벤션에 의한 설정” 을 실현하는 핵심 도구다.

 

이 어노테이션을 통해 개발자는 복잡한 설정을 최소화하고, 빠르게 애플리케이션을 실행할 수 있다. 특히 @EnableAutoConfiguration 덕분에 필요한 라이브러리만 추가하면, Spring Boot는 해당 라이브러리에 맞는 기본 설정을 자동으로 적용해주어 개발 속도를 크게 향상시킨다.

 

@SpringBootApplication은 스프링 부트를 더 효율적이고 생산적으로 사용할 수 있도록 해주는 핵심적인 요소다. 이 어노테이션 덕분에 개발자는 스프링 부트 애플리케이션을 쉽게 구동할 수 있고, 자동 설정과 컴포넌트 스캔을 통해 빠르고 유연하게 애플리케이션을 개발할 수 있다.

 

1-4. SpringApplication 실행 과정

 

Spring Boot 애플리케이션은 SpringApplication.run()을 통해 실행된다.

이 과정에서 애플리케이션 컨텍스트가 생성되고, 빈(Bean)이 초기화된다. 애플리케이션이 실행되면 컨텍스트가 설정되고, 각종 리스너(listener)들이 애플리케이션의 시작과 종료 이벤트를 처리하게 된다.

 

// SpringApplication.class 내부

public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
    return run(new Class<?>[] { primarySource }, args);
}

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
    return new SpringApplication(primarySources).run(args);
}

public ConfigurableApplicationContext run(String... args) {
    Startup startup = Startup.create();
    if (this.registerShutdownHook) {
        SpringApplication.shutdownHook.enableShutdownHookAddition();
    }
    DefaultBootstrapContext bootstrapContext = createBootstrapContext();
    ConfigurableApplicationContext context = null;
    configureHeadlessProperty();
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting(bootstrapContext, this.mainApplicationClass);
    try {
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
        Banner printedBanner = printBanner(environment);
        context = createApplicationContext();
        context.setApplicationStartup(this.applicationStartup);
        prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
        refreshContext(context);
        afterRefresh(context, applicationArguments);
        startup.started();
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), startup);
        }
        listeners.started(context, startup.timeTakenToStarted());
        callRunners(context, applicationArguments);
    }
    catch (Throwable ex) {
        throw handleRunFailure(context, ex, listeners);
    }
    try {
        if (context.isRunning()) {
            listeners.ready(context, startup.ready());
        }
    }
    catch (Throwable ex) {
        throw handleRunFailure(context, ex, null);
    }
    return context;
}

 

간단히 요약하자면 이 클래스는 Spring Boot 애플리케이션의 실행 과정을 처리하는 핵심 역할을 한다.

실행은 환경 설정, 컨텍스트 초기화, 컨텍스트 리프레시, 그리고 런너 실행 순으로 진행된다. 이 과정에서 다양한 리스너가 실행되어 애플리케이션의 시작과 종료를 관리하며, 덕분에 최소한의 설정으로 쉽게 애플리케이션을 실행할 수 있다.

 

여기서 가장 중요한 동작은 컨텍스트를 생성하고 그 컨텍스트에 빈(Bean)을 등록하고 초기화하는 과정이라고 볼 수 있다.

 

prepareContext() 메서드를 통해 컨텍스트와 빈을 준비하고, Lazy 로딩을 통해 실제로 필요할 때 빈이 로드될 수 있게 한다. 이후, refreshContext()가 호출되면 모든 빈이 초기화되며, 애플리케이션은 본격적으로 실행될 준비가 완료된다.

 

이 모든 과정은 스프링 컨테이너에서 이루어지며, 스프링 컨테이너는 빈을 관리하고, 의존성을 주입하는 핵심 역할을 담당한다. 스프링 컨테이너는 이러한 빈 등록 및 초기화 과정을 통해 애플리케이션이 효율적으로 동작할 수 있도록 지원한다.

private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
        ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
        ApplicationArguments applicationArguments, Banner printedBanner) {
    context.setEnvironment(environment);
    postProcessApplicationContext(context);
    addAotGeneratedInitializerIfNecessary(this.initializers);
    applyInitializers(context);
    listeners.contextPrepared(context);
    bootstrapContext.close(context);
    if (this.logStartupInfo) {
        logStartupInfo(context.getParent() == null);
        logStartupProfileInfo(context);
    }
    // Add boot specific singleton beans
    ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
    beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
    if (printedBanner != null) {
        beanFactory.registerSingleton("springBootBanner", printedBanner);
    }
    if (beanFactory instanceof AbstractAutowireCapableBeanFactory autowireCapableBeanFactory) {
        autowireCapableBeanFactory.setAllowCircularReferences(this.allowCircularReferences);
        if (beanFactory instanceof DefaultListableBeanFactory listableBeanFactory) {
            listableBeanFactory.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
        }
    }
    if (this.lazyInitialization) {
        context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
    }
    if (this.keepAlive) {
        context.addApplicationListener(new KeepAlive());
    }
    context.addBeanFactoryPostProcessor(new PropertySourceOrderingBeanFactoryPostProcessor(context));
    if (!AotDetector.useGeneratedArtifacts()) {
        // Load the sources
        Set<Object> sources = getAllSources();
        Assert.notEmpty(sources, "Sources must not be empty");
        load(context, sources.toArray(new Object[0]));
    }
    listeners.contextLoaded(context);
}

 

listeners.contextLoaded를 통해 스프링의 다음 리스너들이 context 로드가 완료되었다는 이벤트를 받음으로써 빈이 모두 준비된 이후의 동작을 진행할수 있게 해준다.

 

이후 컴포넌트가 존재할때 각 컴포넌트를 Bean으로 생성하여 만들어 주는데 이는 또 깊이가 있음으로 다음에 준비해 보도록 하겠다.

 

최종적으로 따라가게 되면

prepareContext 를 통해 사용할 빈 및 각종 어플리케이션과 컨텍스트를 준비한 후 refresh를 통해 해당 빈들을 초기화 해주면서 스프링에 서 사용할 각 클래스를 Bean 형태로 사용할수 있게 준비해주는 과정이라 보면 된다.

 

이후 callRunners 매서드 호출을 통해 ApplicationRunner/CommandLineRunner 구현체의 클래스들의 run 메서드를 실행시켜 주는 과정으로 스프링은 동작하게 된다.

 

callRunners의 동작이 모두 끝나면, 컨텍스트가 정상적으로 초기화되어 context.isRunning() 상태가 true로 바뀐다. 이 시점에서 리스너(listener)에게 ready 이벤트가 발생해 애플리케이션이 실행 준비가 완료되었음을 알린다.

 

listeners.ready(context, startup.ready())가 호출되면, 애플리케이션이 완전히 준비되었음을 리스너에게 알린다.

이후부터는 웹 요청 처리, 스케줄링 작업, 배치 작업 등의 다양한 스프링 기능들이 활성화된다.

예를 들어, spring-boot-starter-web, spring-boot-starter-batch, 그리고 스케줄링 작업은 spring-boot-starter-task@EnableScheduling을 통해 사용할 수 있다.

 

 

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.3.4)

2024-10-08T01:11:30.943+09:00  INFO 95170 --- [my-test] [           main] com.example.mytest.MyTestApplication     : Starting MyTestApplication using Java 23 with PID 95170 (/Users/choehyegeun/Desktop/java-lab/my-test/build/classes/java/main started by choehyegeun in /Users/choehyegeun/Desktop/java-lab/my-test)
2024-10-08T01:11:30.944+09:00  INFO 95170 --- [my-test] [           main] com.example.mytest.MyTestApplication     : No active profile set, falling back to 1 default profile: "default"
2024-10-08T01:11:31.121+09:00  INFO 95170 --- [my-test] [           main] com.example.mytest.MyTestApplication     : Started MyTestApplication in 0.288 seconds (process running for 0.678)

Process finished with exit code 0

 

마지막으로, 애플리케이션이 모든 작업을 마치고 더 이상 실행할 작업이 없을 경우, 스프링 애플리케이션은 정상적으로 종료되며, 프로세스가 끝난다.

 

정리하자면 스프링 부트 어플리케이션은 

 

1. @SpringBootApplication (@SpringBootConfiguration, @EnableAutoConfiguration, @ComponentScan)을 통해 스프링에서 사용될 빈의 정의를 등록하고, 컴포넌트 스캔을 통해 해당 빈들이 스프링 컨테이너에 등록된다. 이 과정에서는 빈의 클래스 정보와 의존성들이 스프링 컨테이너에 등록되며, 실제 인스턴스화(객체 생성)는 이루어지지 않는다.

 

2. SpringApplication.run(MyTestApplication.class, args) 메서드를 통해 컨텍스트가 초기화되면서, **등록된 빈을 인스턴스화(객체로 생성)**하고 의존성 주입을 완료한 후, 스프링 애플리케이션 로직을 실행시킨다. 이 시점에서 실제 빈 객체가 생성되며, 스프링 애플리케이션의 각종 로직과 의존성 주입이 진행된다.


이처럼 스프링은 수많은 자바 클래스로 이루어진 복잡한 프레임워크다.

물론, 이러한 내부 동작을 깊이 알지 못해도 애플리케이션을 충분히 개발할 수 있다.

하지만 스프링의 원리와 동작 방식을 이해하는 것은 단순한 기능 구현을 넘어, 스프링의 강력한 기능들을 더 효과적으로 활용하는 데 큰 도움이 될 것이다.

 

이번 과정을 통해 스프링을 더 “스프링답게” 사용할 수 있는 방법을 고민하게 되었고, 앞으로도 그 가능성을 탐구하며 성장해 나가야겠다는 생각을 하며 글을 마무리한다.

 

개인의 공부를 정리해가며 작성한 글이며 오타가 있거나 잘못 작성된 내용이 있을수 있습니다. 잘못된 정보나 다른 의견이 있으시다면 댓글로 작성 부탁드립니다.

 

댓글