Back-end/Spring

[Springboot] 스프링 부트 자동구성의 동작 원리 파헤쳐보기 (@SpringBootApplication, @EnableAutoConfiguratioin, @Import, AutoConfigurationImportSelector)

Nellie Kim 2024. 9. 28. 17:16
728x90

어째서 스프링부트는 우리가 원하는 많은 Bean 들을 자동으로 등록해주는 것일까?  스프링부트가 해주었던 마법같은 동작을 하나하나 들여다보자. 

사용한 기술 및 버전 

  • 스프링 부트 : 3.3.1
  • 자바 : 17
  • IDE : IntelliJ Community

@SpringBootApplication 을 따라 들어가 보기

인텔리제이 프로젝트의 main 메서드가 있는 실행파일을 먼저 들어가보자.

 

순서대로 다음 어노테이션들을

@SpringBootApplication ⇒ @EnableAutoConfiguratioin ⇒ @Import({AutoConfigurationImportSelector.class}) 로 따라들어가보자.

 

 

 

 

@EnableAutoConfiguratioin 파일에 보면, @Import 어노테이션이 보인다. 

@Import 어노테이션은 다른 클래스나 구성 요소를 스프링 애플리케이션 컨텍스트에 추가하는 역할을 한다.

이 어노테이션에 AutoConfigurationImportSelector 가 등록이 되어있는 것을 볼 수 있다. 

 

 

AutoConfigurationImportSelector 를 따라 들어가 보자. 

 

AutoConfigurationImportSelector 

AutoConfigurationImportSelector 에 들어가면 getCandidateConfigurations 메서드가 있다. 이 메서드가 핵심이다. 

 

이 메서드에서는 Assert.notEmpty ~ 로 시작하는 메서드에

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 를 매개변수로 받고 있다.

 

 

이 매개변수가 뭐길래 얘를 참조하는 것일까?

 

이 매개변수의 구조를 따라 프로젝트 경로에서 

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일을 를 찾아가보자.

 

프로젝트의 외부 라이브러리 ⇒ Maven: ~ : spring-boot-autoconfigure:버전 ⇒ .jar ⇒ META-INF ⇒ spring ⇒ org.springframework.boot.autoconfigure.AutoConfiguration.imports

 

 

 

org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일을 열어보면 ,

아래와 같은 152줄의 설정 클래스들이 미리 들어있다.

 

AutoConfigurationImportSelector의  getCandidateConfigurations 메서드가 이 설정 클래스들을 참조하고 있고, 

이 AutoConfigurationImportSelector를 스프링부트는 @Import 어노테이션으로 주입하고 있었던 것이다.

 

 

이 많은 자동 설정 클래스들의 진짜 모습을 보기 위해 이 중에서 레디스 설정 클래스인 RedisAutoConfiguration를 들어가보자.

 

 

RedisAutoConfiguration

드디어 자동구성 클래스의 적나라한 모습을 볼 수 있다. 

 

 

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.boot.autoconfigure.data.redis;

import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;

@AutoConfiguration
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
    public RedisAutoConfiguration() {
    }

    @Bean
    @ConditionalOnMissingBean({RedisConnectionDetails.class})
    PropertiesRedisConnectionDetails redisConnectionDetails(RedisProperties properties) {
        return new PropertiesRedisConnectionDetails(properties);
    }

    @Bean
    @ConditionalOnMissingBean(
        name = {"redisTemplate"}
    )
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return new StringRedisTemplate(redisConnectionFactory);
    }
}

 

여기서 낯선 어노테이션이 눈에 띈다.

@ConditionalOnMissingBean, @ConditionalOnSingleCandidate 이다.

 

@ConditionalOnMissingBean 

@ConditionalOnMissingBean은 스프링 프레임워크에서 사용하는 어노테이션으로, 특정 타입의 빈(Bean)이 스프링 컨텍스트에 존재하지 않을 때만 새로 빈을 생성하도록 조건을 설정하는 어노테이션이다.

 

만약 개발자가 이미 해당 타입의 빈을 명시적으로 정의해 두었다면, 스프링이 자동으로 새로 생성하지 않고 기존의 빈을 사용하게 된다.

 

이는 스프링의 자동 구성 기능을 효율적으로 처리하기 위해 많이 사용된다. 따라서 이 어노테이션이 없을 경우, 같은 타입의 빈이 여러 번 생성될 수 있으며, 이것은 애플리케이션 동작에 예상치 못한 영향을 줄 수 있다.

 

이 코드에서 @ConditionalOnMissingBean이 사용된 부분을 보면

  1. @ConditionalOnMissingBean({RedisConnectionDetails.class}): 이 어노테이션이 붙은 redisConnectionDetails 메서드는, 스프링 컨텍스트에 RedisConnectionDetails 타입의 빈이 없을 때만 이 메서드가 호출되어 PropertiesRedisConnectionDetails 타입의 빈을 생성한다.
  2. @ConditionalOnMissingBean(name = {"redisTemplate"}): 여기서는 이름이 "redisTemplate"인 빈이 존재하지 않을 때만 redisTemplate 메서드가 실행되어 RedisTemplate<Object, Object> 타입의 빈을 생성한다.
  3. @ConditionalOnMissingBean: 여기서는 타입을 명시하지 않고 기본적으로 동일한 타입의 빈이 없을 때만 메서드가 실행되어 빈을 생성한다.
  4. @ConditionalOnSingleCandidate: 특정 타입의 빈이 스프링 컨텍스트에 하나만 있을 때, 그 빈을 사용할 수 있도록 조건을 설정하는 어노테이션이다. 이를 통해 의존성 주입 시 모호성을 해결하고, 다수의 후보가 있을 때는 해당 빈을 생성하지 않도록 제어할 수 있다.

 

그렇다면 이제 아래 RedisAutoConfiguration 코드의 일부를 자세히 분석해보자.

    @Bean
    @ConditionalOnMissingBean(
        name = {"redisTemplate"}
    )
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

 

따라서, 위 코드가 의미하는 것은 다음과 같다.

 

redisTemplate이라는 이름의 빈이 없고, RedisConnectionFactory 타입의 빈이 하나만 존재할 경우에만 RedisTemplate<Object, Object> 빈을 생성한다.

 

위 코드를 보고 예상할 수 있는 스프링부트의 실행 과정은 ,

  1. 먼저, 스프링 컨텍스트에 redisTemplate 빈이 있는지 확인한다. 만약 이미 있다면, 이 메서드는 실행되지 않고 건너뛴다.
  2. redisTemplate 빈이 없으면, RedisConnectionFactory 타입의 빈이 하나만 있는지 확인한다. 이 타입의 빈이 여러 개 존재하면, 이 메서드도 실행되지 않고 건너뛴다.
  3. 위의 두 조건이 모두 만족하면 드디어 RedisTemplate<Object, Object> 빈을 생성하여 등록한다.

 

결론

스프링 부트는 @EnableAutoConfiguration을 통해 다양한 자동 설정 클래스를 스프링 컨텍스트에 주입하여, 개발자가 따로 설정하지 않아도 필요한 빈들을 자동으로 등록해주는 기능을 제공한다.

 

이 과정에서 AutoConfigurationImportSelector가

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일에 정의된 여러 설정 클래스들을 로드하고, 각종 조건부 어노테이션(@ConditionalOnMissingBean, @ConditionalOnSingleCandidate)을 활용해 이미 등록된 빈이 있거나 특정 조건이 만족되지 않으면 새로운 빈을 생성하지 않도록 제어한다. 

이를 통해 중복 생성 문제를 방지하고, 애플리케이션의 성능과 효율성을 높인다.

 

예를 들어, RedisAutoConfiguration 클래스는 RedisTemplate이나 StringRedisTemplate 빈을 자동으로 생성하지만, 같은 타입의 빈이 이미 있거나 RedisConnectionFactory 빈이 여러 개 존재할 경우 자동 생성을 건너뛰게 된다.

이러한 방식으로 스프링 부트는 유연하고 효율적인 빈 관리를 가능하게 한다.