SpringBoot - 자동 구성(Auto Configuration)

Kwon Yongho·2023년 7월 1일
0

Spring

목록 보기
32/37
post-thumbnail
  1. 예제 만들기
  2. 자동 구성 확인
  3. 스프링 부트의 자동 구성
  4. 자동 구성 직접 만들기 - 기반 예제
  5. @Conditional
  6. 순수 라이브러리
  7. 자동 구성 라이브러리
  8. 자동 구성 이해

1. 예제 만들기

springboot-autoconfig 프로젝트를 생성하였습니다.

build.gradle

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    //테스트에서 lombok 사용
    testCompileOnly 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'
}

스프링 부트에서 다음 라이브러리를 선택했다.

  • Lombok, Spring Web, H2 Database, JDBC API

JdbcTemplate을 사용해서 회원 데이터를 DB에 저장하고 조회하는 간단한 기능을 만들어보겠습니다.

Member

package hello.member;

import lombok.Data;

@Data
public class Member {

    private String memberId;
    private String name;

    public Member() {
    }

    public Member(String memberId, String name) {
        this.memberId = memberId;
        this.name = name;
    }
}

DbConfig

package hello.config;

import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.JdbcTransactionManager;
import org.springframework.transaction.TransactionManager;

import javax.sql.DataSource;

@Slf4j
@Configuration
public class DbConfig {

    @Bean
    public DataSource dataSource(){
        log.info("datasource 빈 등록");
        HikariDataSource dataSource = new HikariDataSource();

        // H2 데이터 베이스 사용
        dataSource.setDriverClassName("org.h2.Driver");
        dataSource.setJdbcUrl("jdbc:h2:mem:test");
        dataSource.setUsername("sa");
        dataSource.setPassword("");
        return dataSource;
    }

    @Bean
    public TransactionManager transactionManager() {
        log.info("transactionManager 빈 등록");
        return new JdbcTransactionManager(dataSource());
    }
    
    @Bean
    public JdbcTemplate jdbcTemplate() {
        log.info("jdbcTemplate 빈 등록");
        return new JdbcTemplate(dataSource());
    }
}
  • Spring-DB편에서 해봤던 내용이다.
  • DB는 별도의 외부 DB가 아니라 JVM 내부에서 동작하는 메모리 DB를 사용한다.

MemberRepository

package hello.member;

import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public class MemberRepository {

    private JdbcTemplate template;

    public MemberRepository(JdbcTemplate template) {
        this.template = template;
    }

    public void initTable(){
        template.execute("create table member(member_id varchar primary key,name  varchar )");
    }

    public void save(Member member) {
        template.update("insert into member(member_id, name) values(?,?)",
                member.getMemberId(),
                member.getName());
    }
    
    public Member find(String memberId) {
        return template.queryForObject("select member_id, name from member where member_id=?"
                , BeanPropertyRowMapper.newInstance(Member.class), memberId);
    }
    
    public List<Member> findAll() {
        return template.query("select member_id, name from member"
                , BeanPropertyRowMapper.newInstance(Member.class));
    }

}
  • initTable: 보통 리포지토리에 테이블을 생성하는 스크립트를 두지는 않는다. 여기서는 예제를 단순화 하기 위해 이곳에 사용했다.

MemberRepositoryTest

package hello.member;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
public class MemberRepositoryTest {

    @Autowired
    MemberRepository memberRepository;

    @Transactional
    @Test
    void memberTest(){
        Member member = new Member("idKwonyongho", "memberKwonyongho");
        memberRepository.initTable();
        memberRepository.save(member);
        Member findMember = memberRepository.find(member.getMemberId());

        assertThat(findMember.getMemberId()).isEqualTo(member.getMemberId());
        assertThat(findMember.getName()).isEqualTo(member.getName());
    }
}
  • 테이블을 생성하고, 회원 데이터를 저장한 다음 다시 조회해서, 기존 데이터와 같은지 간단히 검증한다.

  • 회원 데이터를 DB에 보관하고 관리하기 위해 앞서 빈으로 등록한 JdbcTemplate, DataSource,
    TransactionManager가 모두 사용되었다.
  • DB에 데이터를 보관하고 관리하기 위해 이런 객체들을 항상 스프링 빈으로 등록해야 하는 번거로움이 있다.
  • 만약 DB를 사용하는 다른 프로젝트를 진행한다면 이러한 객체들을 또 스프링 빈으로 등록해야 할 것이다.

2. 자동 구성 확인

DbConfigTest

package hello.config;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.TransactionManager;

import javax.sql.DataSource;

import static org.assertj.core.api.Assertions.assertThat;

@Slf4j
@SpringBootTest
public class DbConfigTest {

    @Autowired
    DataSource dataSource;

    @Autowired
    TransactionManager transactionManager;

    @Autowired
    JdbcTemplate jdbcTemplate;

    @Test
    void checkBean(){
        log.info("dataSource = {}", dataSource);
        log.info("transactionManager = {}", transactionManager);
        log.info("jdbcTemplate = {}", jdbcTemplate);
        
        assertThat(dataSource).isNotNull();
        assertThat(transactionManager).isNotNull();
        assertThat(jdbcTemplate).isNotNull();
    }
}

JdbcTemplate, DataSource, TransactionManager 스프링 컨테이너 등록 확인

이번에는 DBConfig에서 빈 등록을 제거해보자 (2가지 방법)

  • @Configuration을 주석처리: 이렇게 하면 해당 설정 파일 자체를 스프링이 읽어들이지 않는다. (컴포넌트 스캔의 대상이 아니다.)
  • @Bean 주석처리: @Bean이 없으면 스프링 빈으로 등록하지 않는다.

DbConfig@Configuration를 주석처리했다.

  • JdbcTemplate, DataSource, TransactionManager가 분명히 스프링 빈으로 등록되지 않았다는 것이다.
  • 그런데 테스트는 정상 통과하고 심지어 출력결과에 빈들이 존재하는 것을 확인할 수 있다.
  • 사실 이 빈들은 모두 스프링 부트가 자동으로 등록해 준 것이다.

3. 스프링 부트의 자동 구성

  • 스프링 부트는 자동 구성(Auto Configuration)이라는 기능을 제공하는데, 일반적으로 자주 사용하는 수 많은 빈들을 자동으로 등록해주는 기능이다.
  • 스프링 부트는 spring-boot-autoconfigure라는 프로젝트 안에서 수 많은 자동 구성을 제공한다.

JdbcTemplate을 설정하고 빈으로 등록해주는 자동 구성을 확인해보자.
JdbcTemplateAutoConfiguration

@AutoConfiguration(after = DataSourceAutoConfiguration.class)
@ConditionalOnClass({ DataSource.class, JdbcTemplate.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(JdbcProperties.class)
@Import({ DatabaseInitializationDependencyConfigurer.class,
        JdbcTemplateConfiguration.class,
        NamedParameterJdbcTemplateConfiguration.class })
public class JdbcTemplateAutoConfiguration {
}

- @AutoConfiguration: 자동 구성을 사용하려면 이 애노테이션을 등록해야 한다.

  • @ConditionalOnClass({ DataSource.class, JdbcTemplate.class })
    • IF문과 유사한 기능을 제공한다. 이런 클래스가 있는 경우에만 설정이 동작한다. 만약 없으면 여기 있는 설정들이 모두 무효화 되고, 빈도 등록되지 않는다.
    • @ConditionalXxx시리즈가 있다. 자동 구성의 핵심이므로 뒤에서 자세히 알아본다.
    • JdbcTemplateDataSource, JdbcTemplate라는 클래스가 있어야 동작할 수 있다.
  • @Import: 스프링에서 자바 설정을 추가할 때 사용한다.

JdbcTemplateConfiguration

@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(JdbcOperations.class)
class JdbcTemplateConfiguration {
    @Bean
    @Primary
    JdbcTemplate jdbcTemplate(DataSource dataSource, JdbcProperties properties) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        JdbcProperties.Template template = properties.getTemplate();
        jdbcTemplate.setFetchSize(template.getFetchSize());
        jdbcTemplate.setMaxRows(template.getMaxRows());
        if (template.getQueryTimeout() != null) {
            jdbcTemplate.setQueryTimeout((int)
                    template.getQueryTimeout().getSeconds());
        }
        return jdbcTemplate;
    }
}
  • @Configuration: 자바 설정 파일로 사용된다.

  • @ConditionalOnMissingBean(JdbcOperations.class)

    • JdbcOperations빈이 없을 때 동작한다.
    • JdbcTemplate의 부모 인터페이스가 바로 JdbcOperations이다.
    • 쉽게 이야기해서 JdbcTemplate이 빈으로 등록되어 있지 않은 경우에만 동작한다.
    • 만약 이런 기능이 없으면 내가 등록한 JdbcTemplate과 자동 구성이 등록하는 JdbcTemplate이 중복 등록되는 문제가 발생할 수 있다.
    • 보통 개발자가 직접 빈을 등록하면 개발자가 등록한 빈을 사용하고, 자동 구성은 동작하지 않는다.
  • JdbcTemplate이 몇가지 설정을 거쳐서 빈으로 등록되는 것을 확인할 수 있다

  • 즉 자동 구성 기능들이 Bean으로 등록해 주지 않아도 자동으로 Bean에 등록해주는 것이다.

  • 그래서 개발자가 직접 빈을 등록하지 않아도 JdbcTemplate, DataSource, TransactionManager가 스프링 빈으로 등록된 것이다.

스프링 부트가 제공하는 자동 구성
https://docs.spring.io/spring-boot/docs/current/reference/html/auto-configuration-classes.html

Auto Configuration 용어

자동 설정

  • Configuration은 크게 보면 빈들을 자동으로 등록해서 스프링이 동작하는 환경을 자동으로 설정해주기 때문에 자동 설정이라는 용어도 맞다.

자동 구성

  • 스프링도 스프링 실행에 필요한 빈들을 적절하게 배치해야 한다. 자동 구성은 스프링 실행에 필요한 빈들을 자동으로 배치해주는 것이다.

스프링 부트가 제공하는 자동 구성 기능을 이해하려면 다음 두 가지 개념을 이해해야 한다.

  • @Conditional(설정): 특정 조건에 맞을 때 설정이 동작하도록 한다.
  • @AutoConfiguration(자동 구성): 자동 구성이 어떻게 동작하는지 내부 원리 이해

4. 자동 구성 직접 만들기 - 기반 예제

자동 구성에 대해서 자세히 알아보기 위해 간단한 예제

Memory

package memory;

public class Memory {

    private long used;
    private long max;

    public Memory(long used, long max) {
        this.used = used;
        this.max = max;
    }

    public long getUsed() {
        return used;
    }
    
    public long getMax() {
        return max;
    }

    @Override
    public String toString() {
        return "Memory{" +
                "used=" + used +
                ", max=" + max +
                '}';
    }
}
  • used: 사용중인 메모리
  • max: 최대 메모리
  • usedmax를 넘게 되면 메모리 부족 오류가 발생한다.

MemoryFinder

package memory;

import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class MemoryFinder {

    public Memory get() {
        long max = Runtime.getRuntime().maxMemory();
        long total = Runtime.getRuntime().totalMemory();
        long free = Runtime.getRuntime().freeMemory();
        long used = total - free;
        return new Memory(used, max);
    }
    
    @PostConstruct
    public void init() {
        log.info("init memoryFinder");
    }
    
}
  • JVM에서 메모리 정보를 실시간으로 조회하는 기능이다.
  • max JVM이 사용할 수 있는 최대 메모리, 이 수치를 넘어가면 OOM이 발생한다.
  • total JVM이 확보한 전체 메모리(JVM은 처음부터 max 까지 다 확보하지 않고 필요할 때 마다 조금씩 확보한다.)
  • free total 중에 사용하지 않은 메모리(JVM이 확보한 전체 메모리 중에 사용하지 않은 것)
  • used JVM이 사용중인 메모리이다. (used = total - free)

MemoryController

package memory;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequiredArgsConstructor
public class MemoryController {
    
    private final MemoryFinder memoryFinder;
    
    @GetMapping("/memory")
    public Memory system() {
        Memory memory = memoryFinder.get();
        log.info("memory={}", memory);
        return memory;
    }
}
  • 간단한 컨트롤러

MemoryConfig

package hello.config;

import memory.MemoryController;
import memory.MemoryFinder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MemoryConfig {

    @Bean
    public MemoryController memoryController() {
        return new MemoryController(memoryFinder());
    }

    @Bean
    public MemoryFinder memoryFinder() {
        return new MemoryFinder();
    }
}
  • Bean으로 등록

실행

간단하게 메모리 사용량을 실시간으로 확인할 수 있다.

5. @Conditional

5-1. 설명

  • 앞서 만든 메모리 조회 기능을 항상 사용하는 것이 아니라 특정 조건일 때만 해당 기능이 활성화 되도록해보자.
  • 같은 소스 코드인데 특정 상황일 때만 특정 빈들을 등록해서 사용하도록 도와주는 기능이 바로 @Conditional이다.
  • 여기서 핵심은 소스코드를 고치지 않고 이런 것이 가능해야 한다는 점이다.
  • 참고로 이 기능은 스프링 부트 자동 구성에서 자주 사용한다.

Condition.interface

public interface Condition {
  boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

-matches()메서드가 true를 반환하면 조건에 만족해서 동작하고, false를 반환하면 동작하지 않는다.

  • ConditionContext: 스프링 컨테이너, 환경 정보등을 담고 있다.
  • AnnotatedTypeMetadata: 애노테이션 메타 정보를 담고 있다.

MemoryCondition

package memory;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

@Slf4j
public class MemoryCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        String memory = context.getEnvironment().getProperty("memory");
        log.info("memory={}",memory);
        
        // memory값이 on이면 true를 반환
        return "on".equals(memory);
    }
}

MemoryConfig 추가

@Configuration
@Conditional(MemoryCondition.class) //추가
public class MemoryConfig {}
  • @Conditional(MemoryCondition.class)
    • MemoryConfig의 적용 여부는 @Conditional에 지정한 MemoryCondition의 조건에 따라
      달라진다.
    • MemoryConditionmatches()를 실행해보고 그 결과가 true이면 MemoryConfig는 정상
      동작한다. 따라서 memoryController, memoryFinder가 빈으로 등록된다.
    • false면 빈은 등록되지 않는다.

실행

  • memory=on을 설정하지 않았기 때문에 동작하지 않는다.

memory=on으로 바꾸기

  • VM 옵션을 추가하는 경우 -Dmemory=on 를 사용

실행

5-2. 다양한 기능

Condition인터페이스를 직접 구현해서 MemoryCondition이라는 구현체를 만들었다.

스프링은 이미 대부분의 구현체를 만들어 놨다. 사용해보자

MemoryConfig - 수정

@ConditionalOnProperty(name = "memory", havingValue = "on") // 수정
public class MemoryConfig {}
  • @ConditionalOnProperty(name = "memory", havingValue = "on")를 추가하자
    • 환경 정보가 memory=on이라는 조건에 맞으면 동작하고, 그렇지 않으면 동작하지 않는다.
    • 우리가 앞서 만든 기능과 동일하다.
  • 실행 결과 잘 동작한다.
  • @ConditionalOnProperty도 내부에서는 @Conditional을 사용한다. 그안에도 Condition 인터페이스를 구현한 OnPropertyCondition를 가지고 있다.

@ConditionalOnXxx

  • @ConditionalOnClass , @ConditionalOnMissingClass
    • 클래스가 있는 경우 동작한다. 나머지는 그 반대
  • @ConditionalOnBean , @ConditionalOnMissingBean
    • 빈이 등록되어 있는 경우 동작한다. 나머지는 그 반대
  • @ConditionalOnProperty
    • 환경 정보가 있는 경우 동작한다.

등등등

ConditionalOnXxx 공식 메뉴얼
https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-auto-configuration.condition-annotations

@Conditional: 특정 조건에 맞을 때 설정이 동작하도록 한다.
@AutoConfiguration: 자동 구성이 어떻게 동작하는지 내부 원리 이해

6. 순수 라이브러리

6-1. 만들기

위에서 만든 메모리 조회 기능을 라이브러리로 만들어서 사용해보겠다.

memory-v1 프로젝트를 새로 생성해서 해당 메모리 조회 기능을 추가했다.

MemoryFinderTest

package memory;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.*;

public class MemoryFinderTest {

    @Test
    void get() {
        MemoryFinder memoryFinder = new MemoryFinder();
        Memory memory = memoryFinder.get();
        assertThat(memory).isNotNull();
    }
}

테스트 결과만 확인해보겠다.

빌드하기
gradlew clean build

압축 풀기
jar -xvf memory-v1.jar


memory-v1.jar는 스스로 동작하지는 못하고 다른 곳에 포함되어서 동작하는 라이브러리이다. 이제 이
라이브러리를 다른 곳에서 사용해보자

6-2. 사용하기

project-v1 프로젝트를 새로 생성했다. 이곳에서 memory-v1.jar 라이브러리를 사용해보겠다.

build.gradle

dependencies {
    implementation files('libs/memory-v1.jar') //추가
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

라이브러리를 스프링 빈으로 등록해서 동작하도록 만들어보자.

MemoryConfig

package hello.config;

import memory.MemoryController;
import memory.MemoryFinder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MemoryConfig {
    
    @Bean
    public MemoryFinder memoryFinder() {
        return new MemoryFinder();
    }

    @Bean
    public MemoryController memoryController() {
        return new MemoryController(memoryFinder());
    }
}

실행

라이브러리가 잘 동작 하는 것을 확인 할 수 있다.

정리

  • 라이브러리 내부에 있는 어떤 빈을
    등록해야하는지 알아야 하고, 그것을 또 하나하나 빈으로 등록해야 한다. 지금처럼 간단한 라이브러리가 아니라 초기 설정이 복잡하다면 사용자 입장에서는 상당히 귀찮은 작업이 될 수 있다.
  • 이런 부분을 자동으로 처리해주는 것이 바로 스프링 부트 자동 구성(Auto Configuration)이다.

7. 자동 구성 라이브러리

7-1. 만들기

프로젝트에 라이브러리를 추가만 하면 모든 구성이 자동으로 처리되도록 해보자.

memory-v1프로젝트를 복사해서 memory-v2를 만들었습니다.

MemoryAutoConfig

package memory;

import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;

@AutoConfiguration
@ConditionalOnProperty(name = "memory", havingValue = "on")
public class MemoryAutoConfig {

    @Bean
    public MemoryController memoryController() {

        return new MemoryController(memoryFinder());
    }

    @Bean
    public MemoryFinder memoryFinder() {

        return new MemoryFinder();
    }
}
  • 전에 했던 내용과 비슷하다.

자동 구성 대상 지정
src/main/resources/META-INF/spring/
org.springframework.boot.autoconfigure.AutoConfiguration.imports 생성

memory.MemoryAutoConfig
  • 스프링 부트는 시작 시점에 org.springframework.boot.autoconfigure.AutoConfiguration.imports의 정보를 읽어서 자동 구성으로 사용한다. 따라서 내부에 있는MemoryAutoConfig가 자동으로 실행된다.

빌드해서 build/libs/memory-v2.jar를 생성했습니다.

7-2. 사용하기1

project-v1와 비슷한 project-v2생성
memory-v2.jar 라이브러리를 project-v2에 적용해보자.

build.gradle

dependencies {
    implementation files('libs/memory-v2.jar') // 추가
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

라이브러리 추가

  • project-v2/libs폴더를 생성
  • memory-v2프로젝트에서 빌드한 memory-v2.jar를 이곳에 복사
  • project-v2/build.gradlememory-v2.jar를 추가

memory=on으로 바꾸기

결과

메모리 조회 라이브러리가 잘 동작하는 것을 확인할 수 있다.

  • 스프링 부트가 제공하는 자동 구성 덕분에 복잡한 빈 등록이나 추가 설정 없이 단순하게 라이브러리의 추가만으로 프로젝트를 편리하게 구성할 수 있다.
  • @ConditionalOnXxx덕분에 라이브러리 설정을 유연하게 제공할 수 있다.
  • 스프링 부트는 수 많은 자동 구성을 제공한다. 그 덕분에 스프링 라이브러리를 포함해서 수 많은 라이브러리를 편리하게 사용할 수 있다.

8. 자동 구성 이해

8-1. 스프링 부트의 동작

스프링 부트는 다음 경로에 있는 파일을 읽어서 스프링 부트 자동 구성으로 사용한다.
resources/META-INF/spring/
org.springframework.boot.autoconfigure.AutoConfiguration.imports

동작 순서
1. @SpringBootApplication
2. @EnableAutoConfiguration
3. @Import(AutoConfigurationImportSelector.class)

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

@SpringBootApplication

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes =
        TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes =
                AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {...}
  • 주목할 애노테이션은 @EnableAutoConfiguration이다. 이름 그대로 자동 구성을 활성화 하는 기능을 제공한다.

@EnableAutoConfiguration

@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {}
  • @Import는 주로 스프링 설정 정보(@Configuration)를 포함할 때 사용한다.
  • 그런데 AutoConfigurationImportSelector를 열어보면 @Configuration이 아니다.

8-2. ImportSelector

@Import에 설정 정보를 추가 하는 방법은 2가지
1. 정적 방법

  • @Import(클래스) 이것은 정적이다. 코드에 대상이 딱 박혀 있다. 설정으로 사용할 대상을 동적으로 변경할 수 없다.
@Configuration
@Import({AConfig.class, BConfig.class})
  public class AppConfig {...}

2. 동적 방법

  • @Import(ImportSelector)코드로 프로그래밍해서 설정으로 사용할 대상을 동적으로 선택할 수 있다.
public interface ImportSelector {
  String[] selectImports(AnnotationMetadata importingClassMetadata);
  //...
}

ImportSelector 예제를 만들어보자.

HelloBean

package hello.selector;

public class HelloBean {
}

HelloConfig

package hello.selector;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class HelloConfig {

    @Bean
    public HelloBean helloBean() {
        return new HelloBean();
    }
}
  • HelloBean을 스프링 빈으로 등록

HelloImportSelector

package hello.selector;

import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

public class HelloImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"hello.selector.HelloConfig"};
    }
}
  • 설정 정보를 동적으로 선택할 수 있게 해주는 ImportSelector인터페이스를 구현했다.
  • 여기서는 단순히 hello.selector.HelloConfig설정 정보를 반환한다.
  • 여기에 설정 정보로 사용할 클래스를 동적으로 프로그래밍 하면 된다.

ImportSelectorTest

package hello.selector;

import jdk.dynalink.linker.support.Guards;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

import static org.assertj.core.api.Assertions.assertThat;

public class ImportSelectorTest {

    @Test
    void staticConfig(){
        AnnotationConfigApplicationContext appContext = new AnnotationConfigApplicationContext(StaticConfig.class);
        HelloBean bean = appContext.getBean(HelloBean.class);
        assertThat(bean).isNotNull();
    }

    @Test
    void selectorConfig(){
        // HelloImportSelector에 return값 인식
        AnnotationConfigApplicationContext appContext = new AnnotationConfigApplicationContext(SelectImports.class);
        HelloBean bean = appContext.getBean(HelloBean.class);
        assertThat(bean).isNotNull();
    }

    @Configuration
    @Import(HelloConfig.class)
    public static class StaticConfig{
    }

    @Configuration
    @Import(HelloImportSelector.class)
    public static class SelectImports{
    }
}

selectorConfig()

  • selectorConfig()SelectorConfig를 초기 설정 정보로 사용
  • SelectorConfigImportSelector의 구현체인
    HelloImportSelector를 사용
  • "hello.selector.HelloConfig"라는 문자를 반환
  • 스프링은 이 문자에 맞는 대상을 설정 정보로 사용한다. 따라서 hello.selector.HelloConfig이 설정 정보로 사용

@EnableAutoConfiguration 동작 방식

@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {}
  • AutoConfigurationImportSelectorImportSelector의 구현체이다. 따라서 설정 정보를 동적으로 선택할 수 있다.
  • 위 코드는 실제로 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 경로의 파일을 확인한다.
  • 따라서 아까 자동 구성 라이브러리에 memory.MemoryAutoConfig를 설정해줘서 자동으로 Bean에 등록된 것이다.

0개의 댓글