brunch

You can make anything
by writing

C.S.Lewis

by anonymDev Oct 11. 2022

AutoConfiguration은 어떻게 동작하는 거야

spring-boot-autoconfigure 코드 뒤집기

Spring Boot AutoConfiguration 이란

Spring Boot의 대표적인 기능은 누가 뭐래도 AutoConfiguration이다. Spring Boot와 연동되는 라이브러리의 디펜던시(대표적으로 *-starter)를 추가해주면 빈 설정과 생성을 자동으로 해주는 편의 기능이다.


예를 들어 redis에 연결된 스프링 애플리케이션을 만들고 싶을 때 spring-boot-starter-data-redis를 의존성에 추가해주면 편리하게 redisTemplate 빈과 연결 설정이 가능하다 .

<demo/build.gradle>

dependencies {
   implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}


별도 설정 없이 구성자나 필드 Autowire를 통해서 RedisTemplate 타입의 빈을 주입받아 사용할 수 있다.

@Service
public class CachingService {
    private final RedisTemplate<Object, Object> redisTemplate;
    @Autowired
    public CachingService(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
}


이때 별도의 설정 없는 경우 127.0.0.1:6379로 연결을 시도한다. host와 port는 application.yml (또는 application.properties)에 다음과 같이 설정하면 된다.

<demo/resources/application.yml>

spring:
  redis:
    host: your-remote-redis-host
    port: 16379


Spring Boot의 AutoConfiguration의 마법 같은 기능은 어떻게 동작하는 것일까?


AutoConfiguration은 spring-boot-project 하위 모듈 spring-boot-autoconfigure에서 구현했다. 기능만큼 구현은 마법 같진 않으니 코드를 보며 살펴보자. 핵심이 되는 코드를 편집된 형태로 첨부할 예정이니 전체 코드를 보고 싶다면 함께 코드 링크를 눌러 확인해보자.


spring-boot-autoconfigure에는 data, jdbc, web, jackson, batch, r2dbc, mongo 등등의 하위 디렉터리가 존재하고 각각의 AutoConfiguration 설정이 포함돼 있다. 이번 글에서는 spring-boot-starter-data-redis의 AutoConfiguration을 구현한 /data/redis/ 하위에 위치한 RedisAutoConfiguration.java (코드)를 살펴보도록 한다.


@AutoConfiguration // (1)
@ConditionalOnClass(RedisOperations.class) // (2)
@EnableConfigurationProperties(RedisProperties.class) // (3)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class }) // (4)
public class RedisAutoConfiguration {

...

}


클래스 상단에 AutoConfiguration 어노테이션이 붙어있다(1). @AutoConfiguration의 역할은 다음과 같이 문서에 기술돼있다.

Indicates that a class provides configuration that can be automatically applied by
Spring Boot
출처: docs.spring.io


Spring Boot가 자동으로 적용하는 설정 클래스로 지정하는 어노테이션이라고 한다. 즉, Redis의 AutoConfiguration 설정 클래스로 RedisAutoConfiguration 클래스를 지정한 것이다.

@ConditionalOnClass 어노테이션이 그 아래에 위치하고 있다. OnClassCondition 어노테이션의 확장한 어노테이션으로 RedisAutoConfiguration의 컴포넌트 등록 조건을 명시하고 있다.


@ConditionalOnClass({ RedisOperation.class })(2)는 classpath에 RedisOperation.class가 존재하는 조건을 의미한다.  RedisOperation.class는 spring-data-redis 의존성에 포함돼있는데 spring-data-redis는 spring-autoconfigure에 옵셔널 의존성으로 추가돼있다.

<spring-boot-autoconfigure/build.gradle>

...

optional("org.springframework:spring-data-redis")

...


이 경우 spring-data-redis 의존성을 직접 추가해주거나 spring-data-redis을 의존성으로 갖고 있는 상위 의존성을 추가해줘야 한다. 이번 예제에서 우리는 spring-boot-starter-data-redis를 추가했기 때문에 후자에 해당하므로 RedisOperation.class가 classpath에 존재한다.

<spring-boot-starter-data-redis/build.gradle>

dependencies {
   api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter"))
   api("org.springframework.data:spring-data-redis") <----여기에 추가돼있다.
   api("io.lettuce:lettuce-core")
}


@EnableConfigurationProperties(RedisProperties.class) (3)는 @ConfigurationProperties 어노테이션이 붙은 RedisProperties 빈을 활성화한다(spring.redis Prefix 설정을 로드).


RedisAutoConfiguration은 RedisTemplate과 StringRedisTemplate 두 개의 빈을 생성하고 있다.

두 빈은 redisTemplate 이름의 빈과 StringRdisTemplate 타입의 빈이 없는 경우(5), (6)에만 조건부적으로 생성된다. ConditionalOnMissingBean은 BeanFactory에 특정 빈이 포함되지 않은 경우 조건을 충족함을 명시한다. 만약 해당 이름 또는 타입의 빈을 별도로 생성했다면 RedisAutoConfiguration에 default로 정의된 두 빈은 생성되지 않는다.

 public class RedisAutoConfiguration {


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

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

}


(4) @Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class }) 는 Lettuce와 Jedis Connection Configuration 컴포넌트를 임포트 한다. 두개의 Redis Connection Configuration 컴포넌트 역시 조건부로 등록된다.


@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisClient.class) // (7)
@ConditionalOnProperty(name = "spring.redis.client-type", havingValue = "lettuce", matchIfMissing = true) //(8)
class LettuceConnectionConfiguration extends RedisConnectionConfiguration {

...

@Bean
@ConditionalOnMissingBean(RedisConnectionFactory.class)
LettuceConnectionFactory redisConnectionFactory(
      ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers,
      ClientResources clientResources) {
   LettuceClientConfiguration clientConfig = getLettuceClientConfiguration(builderCustomizers, clientResources,
         getProperties().getLettuce().getPool());
   return createLettuceConnectionFactory(clientConfig);
}

...(코드)


LettuceConnectionConfiguration은 RedisClient.class가 classpath에 존재하는 경우 활성화된다(7). RedisClient는 lettuce-core에 정의된 클래스이다. lettuce-core는 spring-boot-starter-data-redis가포함하는 의존성이므로 LettuceConnectionConfiguration이 기본 컴포넌트로서 로드될 것이다. 또는 @ConditionalOnProperty 속성에 의해 'spring.redis.client-type' 프로퍼티의 값이 'lettuce'인 경우에도 로드된다. LettuceConnectionFactory 타입의 RedisConnectionFactory 빈을 생성한다.


반면 JedisConnectionConfiguration의 경우 JedisConnection.class를 제외하고 GenericObjectPool.class, Jedis.client는 spring-boot-starter-data-redis에 포함된 의존성이 아니다. 따라서 옵셔널 하게 로드하는 ConnectionConfiguration 컴포넌트이다(9). 또는 'spring.redis.client-type' 설정 프로퍼티 값이 'jedis'인 경우에 로드된다(11). JedisConnectionFactory 타입의 Connection Factory 빈을 생성한다. 만약 JedisConnectionFactory 사용을 원한다면 관련 의존성을 추가해주면 된다.


@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ GenericObjectPool.class, JedisConnection.class, Jedis.class }) // (9)
@ConditionalOnMissingBean(RedisConnectionFactory.class) // (10)
@ConditionalOnProperty(name = "spring.redis.client-type", havingValue = "jedis", matchIfMissing = true) // (11)
class JedisConnectionConfiguration extends RedisConnectionConfiguration {

...

@Bean
JedisConnectionFactory redisConnectionFactory(
      ObjectProvider<JedisClientConfigurationBuilderCustomizer> builderCustomizers) {
   return createJedisConnectionFactory(builderCustomizers);
}

...(코드)


RedisConnectionFactory가 빈으로 생성되면 앞서 언급했던 redisTemplate을 빈으로 생성할 때 주입된다.


public class 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 빈이 생성되면 이름/또는 타입으로 빈을 불러와 다음과 같이 사용할 수 있다.

@SpringBootApplication
public class DemoApplication {

   public static void main(String[] args) {
      ConfigurableApplicationContext run = SpringApplication.run(DemoApplication.class, args);
      RedisTemplate<String, Object> redisTemplate = (RedisTemplate<String, Object>) run.getBean("redisTemplate");
      redisTemplate.opsForValue().set("test-key",  "test-value");
   }
}


지금까지 Redis의 AutoConfiguration 관련 코드를 살펴봤다. Redis의 AutoConfiguration 기능을 사용하지 않길 원하는 경우도 있다. 이때는 @ConditionalOnMissingBean에 명시된 대로 RedisConnectionFactory와 RedisTemplate 빈을 정의해서 설정을 오버라이드 하면 된다.







브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari