관리 메뉴

나구리의 개발공부기록

자동 구성(Auto Configuration), 순수 라이브러리 만들기, 순수 라이브러리 사용하기, 자동 구성 라이브러리 만들기, 자동 구성 라이브러리 사용하기, 자동 구성 이해(스프링 부트의 동작/ImportSelector) 본문

인프런 - 스프링 완전정복 코스 로드맵/스프링 부트 - 핵심 원리와 활용

자동 구성(Auto Configuration), 순수 라이브러리 만들기, 순수 라이브러리 사용하기, 자동 구성 라이브러리 만들기, 자동 구성 라이브러리 사용하기, 자동 구성 이해(스프링 부트의 동작/ImportSelector)

소소한나구리 2024. 12. 2. 19:09

출처 : 인프런 - 스프링 부트 - 핵심 원리와 활용(유료) / 김영한님  
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용


1. 순수 라이브러리 만들기

1) 예제

  • @AutoConfiguration을 이해하기 위해서는 먼저 라이브러리가 어떻게 사용되는지 이해하는 것이 필요함
  • 이전 강의에서 만든 실시간 자바 Memory 조회 기능이 좋다고 소문이 나서 여러 프로젝트에서 사용하고 싶어하여 이 기능을 여러곳에서 사용할 수 있도록 라이브러리로 만든다고 가정
  • 참고로 라이브러리를 만들 때는 스프링 부트 플러그인 기능을 사용하지 않고 진행

(1) 프로젝트 설정 - build.gradle

  • 제공된 프로젝트를 사용
  • 스프링 부트 플러그인을 사용하게 되면 앞에서 설명한 실행 가능한 Jar 구조를 기본으로 만들게 되는데, 여기서는 실행 가능한 Jar가 아니라 다른곳에 포함되어서 사용할 순수 라이브러리 Jar를 만드는 것이 목적이기에 스프링 부트 플러그인을 사용하지 않음
  • 스프링 컨트롤러가 필요하여 스프링 웹라이브러리를 사용하고 스프링 부트 플러그인을 사용하지 않았으므로 버전을 직접 명시함
plugins {
    id 'java'
}

group = 'memory'
sourceCompatibility = '17'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web:3.0.2'
    compileOnly 'org.projectlombok:lombok:1.18.24'
    annotationProcessor 'org.projectlombok:lombok:1.18.24'
    testImplementation 'org.springframework.boot:spring-boot-starter-test:3.0.2'
}

test {
    useJUnitPlatform()
}

 

(2) Memory, MemoryFinder, MemoryController

  • 기존에 작성한 코드와 동일하므로 코드 생략

(3) MemoryFinderTest

  • 데이터가 조회되는지 간단히 테스트코드를 작성하여 검증
package memory;

class MemoryFinderTest {

    @Test
    void get() {
        MemoryFinder memoryFinder = new MemoryFinder();
        Memory memory = memoryFinder.get();
        System.out.println("memory = " + memory);
        assertThat(memory).isNotNull();
    }
}

2) 라이브러리 생성

(1) 빌드

  • 해당 프로젝트의 위치의 터미널에서 ./gradlew clean build를 입력하여 빌드
  • build/libs의 경로에 memory-v1.jar가 생성된 것을 확인이 됨

(2) JAR를 푼 결과

  • jar -xvf memory-v1.jar로 압축을 풀어서 내용을 확인해보면 아래의 이미지의 구조를 확인할 수 있음
  • memory-v1.jar는 스스로 동작하지 못하고 다른 곳에 포함되어서 동작되는 라이브러리(클래스들의 모음)이기 때문에 MANIFEST.MF에는 별다른 내용이 없음


2. 순수 라이브러리 사용하기

1) 프로젝트 생성

  • 위에서 생성한 라이브러리를 사용하기위한 프로젝트를 생성

(1) 프로젝트 설정 - build.gradle

  • 제공되는 프로젝트를 사용
  • 스프링 부트로 생성된 프로젝트로 Lombok과 스프링 웹 라이브러리를 사용함
plugins {
    id 'org.springframework.boot' version '3.0.2'
    id 'io.spring.dependency-management' version '1.1.0'
    id 'java'
}

group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    useJUnitPlatform()
}

 

(2) HelloController

  • 프로젝트가 동작하는지 확인하기위한 간단한 컨트롤러 생성
package hello.controller;

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

2) 직접 만든 memory-v1.jar 라이브러리를 적용

(1) 라이브러리 추가

  • project-v1에 libs 폴더를 생성
  • 해당 폴더에 memory-v1.jar를 복사 및 붙여넣기를 진행
  • build.gradle에 아래의 코드를 추가후 gradle을 리로드
dependencies {
    implementation files('libs/memory-v1.jar')  // 추가
    // ... 기존 종속성은 유지
}
  • 적용이 되면 아래의 이미지처럼 적용이 됨

적용된 결과 모습

** 주의

  • 추가한 폴더 이름이 lib가 아니라 libs인 점을 주의해야함
  • 파일로 추가한 라이브러리를 IntelliJ가 잘 인식하지 못하는 경우에는 프로젝트를 다시 임포트하면 됨

(2) MemoryConfig

  • hello.config 패키지를 만들어서 작성
  • 해당 라이브러리를 스프링 빈으로 등록해서 동작하도록 작성
  • 스프링 부트 자동 구성을 사용하는 것이 아니기 때문에 빈을 직접 하나하나 수동으로 등록해주어야 함
package hello.config;

@Configuration
public class MemoryConfig {

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

 

(3) 동작 확인

  • 실행 후 localhost:8080/memory에 접속해보면 직접만든 메모리 라이브러리가 정상동작하여 메모리 사용량이 출력됨
  • 로그에서도 MemoryFinder가 실행된 것과 메모리의 사용량이 출력되는 것을 확인할 수 있음

(4) 정리

  • 외부 라이브러리를 직접 만들고 또 그것을 프로젝트에 라이브러리로 불러서 적용해 보았는데, 라이브러리를 사용하는 클라이언트 개발자의 입장을 생각해보면 라이브러리 내부에 있는 어떤 빈을 등록해야하는지 알아야하고 그것을 하나하나 빈으로 등록해야함
  • 그래서 보통 라이브러리를 만들면 메뉴얼이 정말 잘 되어있어야 하지만 잘 되어있다고 해도 지금처럼 간단한 라이브러리가 아니라 초기 설정이 복잡한 라이브러리라면 사용자 입장에서는 상당히 번거로운 작업이 될 수 있음
  • 이것을 자동으로 처리해주는 것이 바로 스프링 부트 자동 구성(Auto Configuration)임

3. 자동 구성 라이브러리 만들기

1) 예제

  • 직접 만든 라이브러리를 편리하게 사용할 수 있도록 프로젝트에 라이브러리를 추가만 하면 모든 구성이 자동으로 처리 되도록 변경
  • 즉, 스프링 빈들이 자동으로 등록되며 여기에 추가로 memory=on 옵션도 적용할 수 있도록 변경

(1) 프로젝트 추가

  • 기존 프로젝트 유지를 위해 memory-v1 프로젝트를 복사하여 memory-v2를 만들어서 사용
  • settings.gradle에서 rootProject.name = 'memory-v2' 로 수정, v1 -> v2로 수정해서 프로젝트 이름을 변경해주어야 함

(2) MemoryAutoConfig

  • @AutoConfiguration: 스프링 부트가 제공하는 자동 구성 기능을 적용할 때 사용하는 애노테이션
  • @ConditionalOnProperty: memory=on이라는 환경 정보가 있을 때 라이브러리를 적용(스프링 빈을 등록)하도록 하여 해당 라이브러리를 가지고 있더라도 상황에 따라서 해당 기능을 켜고 끌 수 있게 유연한 기능을 제공
package memory;

@AutoConfiguration
@ConditionalOnProperty(name = "memory", havingValue = "on")
public class MemoryAutoConfig {
    @Bean MemoryController memoryController() {
        return new MemoryController(memoryFinder());
    }
    
    @Bean
    public MemoryFinder memoryFinder() {
        return new MemoryFinder();
    }
}

 

(3) 자동 구성 대상 지정

  • 이 부분이 중요한데 스프링 부트 자동 구성을 적용하려면 해당 경로의 파일에 자동 구성 대상을 꼭 지정해 주어야 함
  • 폴더 위치와 파일 이름이 길기 때문에 주의해야함
  • 폴더 경로: src/main/resources/META-INF/spring/
  • 파일 이름: org.springframework.boot.autoconfigure.AutoConfiguration.imports
  • 파일 내용: memory.MemoryAutoConfig
  • 앞서 만든 자동 구성인 memory.MemoryAutoConfig를 패키지 포함해서 지정
  • 스프링 부트는 시작 시점에 위의 폴더 경로의 파일의 정보를 읽어서 자동 구성으로 사용함

(4) 빌드하기

  • ./gradlew clean build로 빌드

4. 자동 구성 라이브러리 사용하기

1) 프로젝트 생성

(1) 프로젝트 설정

  • 제공된 프로젝트를 사용
  • 기존에 만들었던 project-v1과 동일한 설정을 가지고 있음

(2) HelloController

  • project-v1과 완전히 동일한 코드이므로 생략

2) 직접 만든 memory-v2.jar 라이브러리 적용

(1) 라이브러리 추가

  • project-v1과 동일함
  • libs폴더를 생성하여 memory-v2.jar파일을 복사, 붙여넣기를 실행
  • build.gradle에 설정 추가 후 gradle을 리로드하면 적용이 됨
implementation files('libs/memory-v2.jar') // 추가

 

(2) 라이브러리 설정

  • project-v2에서 사용하는 memory-v2라이브러리에는 스프링 부트 자동 구성이 적용되어 있기 때문에 빈을 등록하는 별도의 설정을 하지 않아도 됨
  • 즉, memory=on 조건만 만족하면 라이브러리가 정상적으로 실행 됨
  • VM 옵션에 -Dmemory=on을 입력하고 애플리케이션을 실행 후 localhost:8080/memory에 접속해보면 정상적으로 라이브러리가 적용된 것을 확인할 수 있음

(3) 정리

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

5. 자동 구성 이해(스프링 부트의 동작/ImportSelector)

1) 스프링 부트의 동작

(1) 스프링 부트 자동 구성

  • 스프링 부트는 위에서 해보았듯이 resources/META-INF/spring/의 경로에 있는 org.springframework.boot.autoconfigure.AutoConfiguration.imports파일을 읽어서 스프링 부트 자동 구성으로 사용함
  • 스프링 부트를 사용하면 기본적으로 제공되는 spring-boot-autoconfigure라이브러리의 org.springframework.boot.autoconfigure.AutoConfiguration.imports파일을 확인해보면 수많은 자동 구성들이 작성되어있는 것을 확인할 수 있음
  • 스프링 부트 자동 구성이 동작하는 원리는 @SpringBootApplication -> @EnableAutoConfiguration -> @Import(AutoConfigurationImportSelector.class) 순서로 확인할 수 있음

(2) AutoConfigApplication

  • 기존에 작성하였던 autoconfig프로젝트로 구조로 확인
  • run()에 보면 AutoConfigApplication.class를 넘겨주는데 이 클래스를 설정 정보로 사용한다는 뜻임
  • 해당 클래스에는 @SpringBootApplication 애노테이션이 있는데 여기 내부를 보면 중요한 설정 정보들이 들어있음
@SpringBootApplication
public class AutoConfigApplication {

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

 

(2) @SpringBootApplication 일부 구조

  • 여기서 주목해야할 애노테이션은 @EnableAutoConfiguration인데, 이름 그대로 자동 구성을 활성화 하는 기능을 제공함
// 기타 애노테이션 생략
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
    // ... 여러 코드들 생략
}

 

(3) @EnableAutoConfiguration

  • @Import는 주로 스프링 설정 정보(@Configuration)를 포함할 때 사용함
  • 그런데 AutoConfigurationImportSelector를 열어보면 @Configuration은 아닌데 해당 클래스가 구현하고 있는 인터페이스의 최 상단을 보면 ImportSelector가 있는데, 이 기능을 이해하려면 ImportSelector를 알아야 함
// 기타 애노테이션 생략
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    // ... 여러 코드들 생략
}

2) ImportSelector - 설명

  • @Import에 설정 정보를 추가하는 방법은 2가지가 있음

(1) 정적인 방법

  • @Import(클래스)로 작성하며, 코드에 대상이 딱 박혀있어 설정으로 사용할 대상을 동적으로 변경할 수 없음
  • 아래 처럼 @Import를 적용하면 스프링에서 다른 설정 정보를 추가할 수 있음
  • 그러나 AConfig, BConfig가 코드에 딱 정해진 것이 아니라 특정 조건에 따라서 설정 정보를 선택해야 하는 경우에는 동적인 방법을 사용해야함
@Configuration
@Import({AConfig.class, BConfig.class})
public class AppConfig {...}

 

(2) 동적인 방법 - ImportSelector사용

  • 스프링은 설정 정보 대상을 동적으로 선택할 수 있는 ImportSelector인터페이스를 제공함
public interface ImportSelector {
    String[] selectImports(AnnotationMetadata importingClassMetadata);

    @Nullable
    default Predicate<String> getExclusionFilter() {
        return null;
    }
}

3) ImportSelector - 예제

  • 이해를 돕기위해 ImportSelector를 사용하는 예제를 작성
  • 모든 코드는 autoconfig프로젝트의 test하위에 hello.selector의 패키지를 생성하여 작성

(1) HelloBean

  • 빈으로 등록할 대상 클래스, 아무것도 정의한 것이 없음
package hello.selector;

public class HelloBean {
}

 

(2) HelloConfig 

  • HelloBean을 스프링 빈으로 등록하는 설정 정보
package hello.selector;

@Configuration
public class HelloConfig {
    
    @Bean
    public HelloBean helloBean() {
        return new HelloBean();
    }
}

 

(3) HelloImportSelector

  • 설정 정보를 동적으로 선택할 수 있게 해주는 ImportSelector 인터페이스를 구현함
  • 여기서는 단순히 hello.selector.HelloConfig 설정 정보를 반환하며 반환된 설정 정보는 선택되어서 사용됨
  • 여기에 설정 정보로 사용할 클래스를 동적으로 프로그래밍 하면됨
package hello.selector;

public class HelloImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"hello.selector.HelloConfig"};
    }
}

 

(4) ImportSelectorTest

  • staticConfig(): 스프링 컨테이너를 만들고 StaticConfig.class를 초기 설정 정보로 사용하여 HelloBean이 스프링 컨테이너에 잘 등록된 것을 확인할 수 있음
  • selectConfig(): SelectorConfig를 초기 설정 정보로 사용하는데, SelectorConfig는 @Import(HelloImportSelector.class)에서 ImportSelector의 구현체인 HelloImportSelector를 사용함
  • 스프링은 HelloImportSelector를 실행하고 "hello.selector.HelloConfig"라는 문자를 반환 받고 이 문자에 맞는 대상을 설정 정보로 사용하여 hello.selector.HelloConfig가 설정 정보로 사용됨
  • 그 결과 HelloBean이 스프링 컨테이너에 잘 등록된 것을 확인할 수 있음
package hello.selector;

public class ImportSelectorTest {

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

    @Test
    void selectorConfig() {
        AnnotationConfigApplicationContext appContext = new AnnotationConfigApplicationContext(SelectorConfig.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 SelectorConfig {
    }

}

4) @EnableAutoConfiguration 동작 방식

  • ImportSelect를 이해했으니 project-v2에 있는 다음 코드들을 이해할 수 있음

(1) @EnableAutoConfiguration

  • AutoConfigurationImportSelector는 ImportSelector의 구현체이며 설정 정보를 동적으로 선택할 수 있음
  • 실제로 이 코드는 모든 라이브러리에 있는 META-INF/spring/ 경로에 있는 org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일을 확인하고 내용을 읽어서 설정 정보로 선택함
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {...}

 

(2) 스프링 부트 자동 구성이 동작하는 순서 정리

  • @SpringBootApplication -> @EnableAutoConfiguration -> @Import(AutoConfigurationImportSelector.class)
  • resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일을 열어서 설정 정보 선택
  • 해당 파일의 설정 정보가 스프링 컨테이너에 등록되고 사용됨
  • AutoConfigurationImportSelector클래스의 getAutoConfigurationEntry()메서드에 디버그 모드를 걸고 디버깅을 해보면configurations = getCandidateConfigurations()가 실행된 결과에 List로 내가 만든 설정정보와, 스프링 부트가 작성해둔 설정정보가 모두 들어가있음
  • 자세히 알필요는 없으며 이렇게 구동된다 정도만 알고 있으면 됨

6. 자동 구성 정리

1) 스프링 부트의 자동 구성을 직접만들어서 사용할 때

(1) 참고사항

  • @AutoConfiguration에 자동 구성의 순서를 지정할 수 있음
  • @AutoConfiguration도 설정 파일이며 내부에 @Configuration이 있는 것을 확인할 수 있음
  • 하지만 일반 스프링 설정과 라이프사이클이 다르기 때문에 컴포넌트 스캔의 대상이 되면 안되며 아래 경로의 파일에 지정해서 사용해야함
  • resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  • 그래서 스프링 부트가 제공하는 컴포넌트 스캔에는 @AutoConfiguration을 제외하는 AutoConfigurationExcludeFilter라는 필터가 포함되어 스프링 부트에서는 자동으로 AutoConfiguration이 컴포넌트 스캔 대상이 되지 않음
  • 자동 구성 내부에서(@AutoConfiguration을 적용한 클래스) 컴포넌트 스캔을 사용하면 안되며 대신에 자동 구성 내부에서 @Import는 사용할 수 있음

(2) 자동 구성을 언제 사용하는지?

  • @AutoConfiguration은 라이브러리를 만들어서 제공할 때 사용하며 그 외에는 사용하는 일은 거의 없는데 보통 필요한 빈들을 컴포넌트 스캔하거나 직접 등록하기 때문임
  • 하지만 라이브러리를 만들어서 제공할 때는 자동 구성이 유용하며 실제로 다양한 외부 라이브러리들이 자동 구성을 함께 제공함
  • 즉, 라이브러리를 직접 만드는 개발자가 아니라면 보통 이미 만들어져있는 라이브러리를 가져다 사용하고 라이브러리를 만들어서 제공하는 경우는 매우 드묾
  • 그러나 자동 구성을 알아야 하는 진짜 이유는 개발을 진행 하다보면 사용하는 특정 빈들이 어떻게 등록된 것인지 확인이 필요할 때가 있는데 이럴 때 스프링 부트의 자동 구성 코드를 읽을 수 있어야 하며 그래야 문제가 발생했을 때 대처가 가능함
  • 자동화는 매우 편리한 기능이지만 자동화만 믿고 있다가 실무에서 문제가 발생했을 때는 파고 들어가서 문제를 확인하는 정도는 이해해야하며 이번에 학습한 정도면 자동 구성 코드를 읽는데에는 큰 어려움을 없을 것임

(3) 남은 문제

  • 이런 방식으로 빈이 자동 등록되면 빈을 등록할 때 사용하는 설정 정보는 어떻게 변경해야 하는지 의문이 들 수 있음
  • 예를 들어서 데이터소스 빈을 등록할 때 DB 접속 URL, ID, PW 같은 정보를 입력해야 하는데 빈이 자동으로 다 등록되어 버린다면 정보를 어떻게 입력할 수 있는지에 대한 의문들인데, 이것은 다음에 배우는 외부 설정과 프로필에서 다룸