현재의 프로젝트를 진행할 때에 라이브러리 선택은 필수적이다. 스프링 WEB, 내장 톰켓, JSON 처리기, 로거 등 이러한 기능들을 import해오게 되는데 라이브러리의 존재는 감사하지만 부트 이전의 이러한 라이브러리들을 직접 모두 엮어서 함께 사용하는데에는 까탈스러운 부분이 존재한다.
버전 관리는 대표적인 예시이다.
dependencies {
//1. 라이브러리 직접 지정
//스프링 웹 MVC
implementation 'org.springframework:spring-webmvc:6.0.4'
//내장 톰캣
implementation 'org.apache.tomcat.embed:tomcat-embed-core:10.1.5'
//JSON 처리
implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.1' //스프링 부트 관련
implementation 'org.springframework.boot:spring-boot:3.0.2' implementation 'org.springframework.boot:spring-boot-autoconfigure:3.0.2' //LOG 관련
implementation 'ch.qos.logback:logback-classic:1.4.5'
implementation 'org.apache.logging.log4j:log4j-to-slf4j:2.19.0' implementation 'org.slf4j:jul-to-slf4j:2.0.6'
//YML 관련
implementation 'org.yaml:snakeyaml:1.33'
}
필요한 라이브러리들을 build.gradle에 작성하는 것까지는 약간 귀찮을 수 있지만 충분히 용인가능하다. 하지만 스프링의 버전은 여러가지가 존재하고 이에 호환성을 좋은 라이브러리들마다의 버전을 찾아 입력해야하는 것은 매우 힘든 일이다.
어떤 라이브러리들은 라이브러리들끼리의 호환성을 체크해야할 수도 있을 것이다. 스프링 부트는 이러한 버전관리를 자동으로 처리해준다.
dependencies {
//2. 스프링 부트 라이브러리 버전 관리
//스프링 웹, MVC
implementation 'org.springframework:spring-webmvc'
//내장 톰캣
implementation 'org.apache.tomcat.embed:tomcat-embed-core'
//JSON 처리
implementation 'com.fasterxml.jackson.core:jackson-databind'
//스프링 부트 관련
implementation 'org.springframework.boot:spring-boot' implementation 'org.springframework.boot:spring-boot-autoconfigure' //LOG 관련
implementation 'ch.qos.logback:logback-classic'
implementation 'org.apache.logging.log4j:log4j-to-slf4j' implementation 'org.slf4j:jul-to-slf4j'
//YML 관련
implementation 'org.yaml:snakeyaml'
}
위와 같이 버전을 작성하지 않아도 가장 최적화가 잘된 버전으로 라이브러리들을 꽂아준다.(스프링 부트가 호환성(100% 완벽하진 않지만)을 알아서 체크해준다.) 심지어는 개발자가 꽂아준 특정 라이브러리의 버전을 바꿀 수도 있다.
부트환경이라면 우리는 사실 위의 예시처럼 라이브러리들을 저렇게 늘어놓고 사용하지도 않는다.
dependencies {
//3. 스프링 부트 스타터
implementation 'org.springframework.boot:spring-boot-starter-web'
}
위와 같이 spring-boot-starter-web라이브러리를 가져오게 되면 그 아래로 위에 열거한 라이브러리들 의 조합 그 이상으로 풍부하게 같이 당겨오게 된다.(가장 기본적이고 인기있게 사용되는 라이브러리 조합으로 적용해준다.)
web starter는 web관련 라이브러리들을 당겨오게 되며 starter는 또 다른 starter를 내장하기도 한다. 아래의 링크에서 starter 종류를 확인 가능하며 우리는 항상 그래왔듯 스프링부트 스타터 홈페이지에서 add dependencies를 통해 이러한 스타터 패키지를 포함해서 스프링 프로젝트를 시작할 수 있게 된다.
@SpringBootTest
public class BeanTest {
@Autowired ApplicationContext applicationContext;
@Test
void beanTest() {
String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
System.out.println("beanDefinitionName = " + beanDefinitionName);
}
System.out.println("등록된 빈의 수 : " + applicationContext.getBeanDefinitionCount());
}
}
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'
}
dependency에 starter-web, lombok, starter-test만을 구축해놓고 테스트를 통해 스프링을 띄울때 빈 객체를 콘솔에 찍어본 결과는 다음과 같다.

이 수많은 Bean 객체가 등록될 수 있는 이유는 스프링 부트에서 관리하는 라이브러리들에 스프링 부트가 각 라이브러리들에서 필요한 빈들을 등록할 수 있게끔 라이브러리 자체에 Auto Configuration절차를 작성해두었기 때문이다. 부트는 이를 활용하여 어플리케이션 초기화시점에 빈을 등록한다.
그렇기에 프로젝트 초기에 개발자가 빈을 1~2개만 등록한다 하더라도 실제로는 100개가 넘는 개발자가 모르는 빈들이 등록되어있을 수 있다. 이번 챕터는 딥하게는 아니더라도 부트가 지원하는 이 자동등록을 탐색해보려고 한다.
Memory 사용량을 web 컨트롤러를 통해 아주 간단하게 웹에 json으로 띄우는 라이브러리를 제작하고 이를 적용해보도록 하겠다.(부트를 사용하지 않는다.)
라이브러리 제작 이전에 조건적으로 빈을 등록해보도록 설계하고, 이 내용을 따로 프로젝트화 해서 라이브러리로 제공할 수 있도록 해보겠다.
@Getter
@Data
public class Memory {
private long used;
private long max;
public Memory(long used, long max) {
this.used = used;
this.max = max;
}
@Override
public String toString() {
return "Memory{" +
"used=" + used +
", max=" + max +
'}';
}
}
//
@RestController
@RequiredArgsConstructor
public class MemoryController {
private final MemoryFinder memoryFinder;
@GetMapping("/memory")
public Memory system() {
Memory memory = memoryFinder.get();
log.info("memory={}", memory);
return memory;
}
}
주요 클래스로 위와 같이 세팅하고 웹 서버를 실행해보면, 현재 메모리 사용량을 나타내어주는 아주 간단한 기능을 구현해볼 수 있다.

적용에 대한 가정은 다음과 같다.
/memory의 활성화 유무는 다음과 같이 VM options에 memory = on을 입력해야만 이 컨트롤러가 빈으로 등록되어 해당 경로에 접근해 메모리사용량을 볼 수 있다.

@Configuration
@Conditional(MemoryCondition.class)
public class MemoryConfig {
@Bean
public MemoryFinder memoryFinder() {
return new MemoryFinder();
}
@Bean
public MemoryController memoryController() {
return new MemoryController(memoryFinder());
}
}
///
@Slf4j
public class MemoryCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata
metadata) {
String memory = context.getEnvironment().getProperty("memory");
log.info("memory={}", memory);
return "on".equals(memory);
}
}
Condition을 구현한 객체는 boolean 리턴타입인 matches() 메서드로 하여금 Config 적용, 즉 Config의 내용은 Bean등록에 관한 것이니 Bean 등록의 여부를 결정한다.
정리하면 matches의 결과에 따라 Bean 등록이 결정되는 것이다. 이는 Condition객체를 따로 만들지 않고 다음의 방법으로도 가능하다.
@Configuration
@ConditionalOnProperty(name = "memory", havingValue = "on")
public class MemoryConfig {
@Bean
public MemoryFinder memoryFinder() {
return new MemoryFinder();
}
@Bean
public MemoryController memoryController() {
return new MemoryController(memoryFinder());
}
}
@ConditionalOnProperty(name = "memory", havingValue = "on")을 통해 우리가 위에서 직접 제작한 Condition 구현체를 대신할 수 있다. 스프링은 @Conditional과 관련해서 개발자가 편리하게 사용할 수 있도록 많은 @ConditionalOnXxx를 제공한다.
라이브러리로 만들기 위해 개별적인 프로젝트를 jar로 배포하여 이 라이브러리를 사용하기 위해 이 jar를 받아들여 import해서 사용해야 한다.
단순히 jar파일을 들여와서 이를 build.gradle을 통해 임포트한다면 빈을 직접 하나하나 등록해주어야 한다.
위에서 미리 보았던 starter패키지만 들여왔을 뿐인데 빈이 백개이상 자동등록되었던 것을 기억하자. 이처럼 라이브러리안에 Auto Configuration 기능을 탑재하여 라이브러리 사용자가 최대한 설정없이 임포트만으로 사용할 수 있도록 해야 사용성이 좋아질 것이다.
라이브러리의 Config 클래스에 대해 다음과 같이 애노테이션을 적용해준다.
@AutoConfiguration
@ConditionalOnProperty(name = "memory", havingValue = "on")
public class MemoryAutoConfig {
@Bean
public MemoryFinder memoryFinder() {
return new MemoryFinder();
}
@Bean
public MemoryController memoryController() {
return new MemoryController(memoryFinder());
}
}
그리고 AutoConfiguration에 대한 대상 지정이 필요하다. 이는 라이브러리를 사용하는 클라이언트 프로젝트에서 AutoConfiguration를 인식하기 위함이다.
라이브러리의 src/main/resources/META-INF/spring/에 경로를 생성하고 org.springframework.boot.autoconfigure.AutoConfiguration.imports 이름의 파일을 만들어 내용에 memory.MemoryAutoConfig를 작성해둔다.
이렇게 설계한 후 라이브러리를 jar로 빌드하고 이를 사용할 프로젝트에서 받아(java.libs경로를 생성하고 이에 받도록 하자.) build.gradle에서 임포트만 한다면 필요한 빈 객체가 자동등록되고, 우리가 라이브러리에 적용했던 Conditional에 대한 기능도 그대로 동작하여 VM options에 따라 memory컨트롤러 경로가 활성화된다.
스프링 부트 자동 구성이 동작하는 원리는 다음 순서로 확인 가능하다.
1번은 자주 봤던 main에 존재하는 부트 애노테이션이고 부트 애노테이션을 보면 다음과 같다.
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes =
TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes =
AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {...}
여기서 설정관리 측면에서 중요한 애노테이션은 2.에 해당하는 @EnableAutoConfiguration이다. 말 그대로 AutoConfiguration을 활성화 시킨다.
// @EnableAutoConfiguration
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {…}
@Import를 통해 라이브러리들의 클래스 정보로 설정 클래스를 참고하는 듯 하다.
하지만 보통 @Import는 인자로 AConfig.class와 같이 설정 클래스 파일을 당겨와 설정을 적용하는데 위 경우에는 Selector라는 것 하나만이 전달되고 있다.
윗 문단 설명에서 전자의 경우는 정적인 방식이고 Selector를 사용하는 방식은 동적이다. 즉 스프링 부트는 초기화 시점에 설정을 구축할 때 Selector를 통해 동적으로 각각 라이브러리들의 파편화된 설정정보들을 모은다고 예상할 수 있다.
자동구성에는 순서가 필요하다. A 라이브러리는 B 라이브러리에 종속적일 수 있다. 즉 B 라이브러리가 먼저 설정화되어 여러가지 필요한 객체들이 스프링 컨테이너에 들어있어야 A의 AutoConfiguration이 유효하다면 A의 설정순서는 B 이후여야 하기에 이러한 기준들을 A 라이브러리의 설정파일에 작성되고 Selector가 이 여러가지 라이브러리들의 조건들을 필터링해주는 것이다.
실제 업무에서 자신들의 서비스를 라이브러리를 통해 만들기도 바쁜데 라이브러리를 만들 것도 아니고 라이브러리가 어떻게 만들어졌든 우리가 알아야할까?
물론 라이브러리를 만들 일은 거의 없다. 배우는 입장에서 생각한다면 라이브러리를 만들 목적으로 이 과정을 배우는 것이라면 목적자체가 매우 지엽적이기에 비효율적이다.
하지만 앞서 보았다시피 우리는 편리하게 starter등을 통해 필요한 라이브러리들을 끌어와 쓴다. 그 라이브러리들은 AutoConfiguration을 통해 수많은 빈 객체를 등록시킨다. 이러한 빈들을 활용하는 시점에 디버그가 필요할 수 있고 이 빈들이 어떻게 등록되는지 확인이 필요할 때가 존재할 것이다. 그때 이에 대한 이해도가 없다면 라이브러리들을 파고 들어가서 조사할 힘 자체가 부족해진다.