2023.02.13 - [Backend/Java] - 공통 lib -> spring-boot-starter로 바꾸기(1)
2023.03.20 - [Backend/Java] - 공통 lib -> spring-boot-starter로 바꾸기(2)
조금 다른, 새로운 프로젝트 등장
프로젝트가 커져가면서, 기능을 분리해 새로운 프로젝트로 관리해야 했다. 기존에 공통 기능 프로젝트(master)와 업무 프로젝트(slave)를 잘 분리했기에 손쉽게 프로젝트를 구성할 수 있었다.
각 업무 프로젝트(slave)마다 필요한 설정이 조금씩 다를 것이다. 특별하게 필요한 설정은 각 프로젝트에서 설정해 사용하면 되지만, 문제는 master 프로젝트에서 제공하는 공통 기능이 특정 프로젝트에서는 필요 없는 상황이 생길 수도 있다.
예를 들어, 기존에 공통으로 처리하던 어떤 웹에 의존적인 설정(필터나 로그인 인터셉터 Bean)들은 배치프로젝트에서는 필요 없다.
master 프로젝트의 Bean을 선택적으로 적용할 수 있어야 한다. 하지만 현재 설정된 방식으로는 Bean을 스캔하는 package를 똑같이 맞춰 놓았기 때문에 특정 Bean을 제외할 좋은 방법이 없다.
현재 설정된 방식은 spring-boot-starter로 바꾸기(1)을 참고하면 된다.
이런 문제를 해결하기 위해서는 프로젝트 설정을 spring-autoconfigure 방식으로 변경해야 한다.
spring-boot-starter
프로젝트 구조
- master-spring-boot-starter: 2개의 하위 모듈을 묶어주는 root 프로젝트
- master-spring-boot-autoconfigure: 스프링 Context에 자동으로 Bean 등록하는 설정 프로젝트
- worker: 공통 라이브러리 기능의 프로젝트
- slave: starter 프로젝트를 의존 주입받아 사용하는 프로젝트
프로젝트 명은 유니크한 gav 가지기 위해 스프링에서 {module-name}-spring-boot-{module-type}을 권장하고 있다.
https://docs.spring.io/spring-boot/docs/1.5.11.RELEASE/reference/html/boot-features-developing-auto-configuration.html#boot-features-custom-starter-naming
worker project
외부에서 인사말을 주입받아 인사하는 로봇 클래스를 하나 만들었다. 이 로봇이 대부분의 프로젝트에서 필요한 객체라고 가정하자.
package me.bbbicb.worker;
public class HelloBot {
private final String helloComment;
public HelloBot(String helloComment) {
this.helloComment = helloComment;
}
public String hello() {
return this.helloComment;
}
}
master-spring-boot-autoconfigure project
package me.bbbicb.masterspringbootautoconfigure;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "master")
public class MasterProperty {
private String hello = "hi"; // 프로퍼티 설정이 없을 경우 적용될 default값
public String getHello() {
return hello;
}
public void setHello(String hello) {
this.hello = hello;
}
}
package me.bbbicb.masterspringbootautoconfigure;
import me.bbbicb.worker.HelloBot;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
@AutoConfiguration
@EnableConfigurationProperties(MasterProperty.class)
public class HelloAutoConfigure {
@Bean
@ConditionalOnMissingBean
public HelloBot helloBot(MasterProperty masterProperty) {
return new HelloBot(masterProperty.getHello());
}
}
package me.bbbicb.masterspringbootautoconfigure;
import me.bbbicb.worker.HelloBot;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
class MasterSpringBootAutoconfigureApplicationTests {
private final ApplicationContextRunner acr = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(HelloAutoConfigure.class));
@Test
@DisplayName("hello함수 실행시 'MasterProperty' hello값을 반환한다.")
void whenDefaultHelloReturnMasterPropertyValue() {
acr.run(ct -> {
assertThat(ct).hasSingleBean(HelloBot.class);
assertThat(ct).hasSingleBean(MasterProperty.class);
MasterProperty masterProperety = ct.getBean(MasterProperty.class);
assertThat(ct.getBean(HelloBot.class).hello()).isEqualTo(masterProperety.getHello());
assertThat(masterProperety.getHello()).isEqualTo("hi");
});
}
}
autoconfigure 프로젝트에서 worker에서 정의된 `HelloBot` 빈을 프로퍼티 설정 값으로 자동 등록한다.
여기서 proxyBeanMethods는 싱글톤을 보장하기 위한 설정으로 기본값이 true인데, 성능을 위해 대부분의 자동설정에서는 false로 설정한다.
`@ConditionalOn` 어노테이션으로 같은 타입의 Bean이 Context에 없을 경우에만, 정의한 `HelloBot` Bean이 자동 등록되도록 했다. autoconfigure 프로젝트에 등록된 Conditional Bean은 마지막 순서에 로드하는 것을 스프링에서 보장하므로, 혹여나 Bean 로드 순서가 잘못될 일은 없다.
마지막으로 classpath:/resources/META-INF/spring/ 경로에 org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일을 만들고, 정의한 AutoConfigure 클래스를 정의하면 된다. 스프링부트가 실행될 때, 파일에 명시된 클래스들을 추가적으로 스캔할 것이다.
// resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
me.bbbicb.masterspringbootautoconfigure.HelloAutoConfigure
기존에는 META-INF/spring.factories였지만 스프링부트 3 버전부터 위의 경로로 변경됐다.
https://docs.spring.io/spring-boot/docs/3.0.0/reference/html/features.html#features.developing-auto-configuration.locating-auto-configuration-candidates
slave project
slave1
package me.bbbicb.slave;
import me.bbbicb.worker.HelloBot;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SlaveApplicationTests {
@Autowired
HelloBot helloBot;
@Test
@DisplayName("기본으로, `HelloBot`을 등록하고 'hi'라고 인사한다.")
void default_registerHelloBotBean() {
Assertions.assertThat(helloBot.hello()).isEqualTo("hi");
}
}
slave1 프로젝트에서는 HelloBot이 필요해 별다른 설정을 하지 않았고, 성공적으로 빈이 등록되고 동작하는 것을 확인할 수 있다.
slave2
package me.bbbicb.slave2;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class Slave2ApplicationTests {
@Autowired
BeanFactory factory;
@Test
@DisplayName("`HelloBot` Bean을 관리하지 않는다.")
void doseNotRegisterHelloBotBean() {
Assertions.assertThat(factory.containsBean("helloBot")).isFalse();
}
}
slave2 프로젝트에서는 HelloBot이 필요하지 않으므로, 제외하는 설정이 추가적으로 필요하다.
package me.bbbicb.slave2;
import me.bbbicb.masterspringbootautoconfigure.HelloAutoConfigure;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(exclude = HelloAutoConfigure.class)
public class Slave2Application {
public static void main(String[] args) {
SpringApplication.run(Slave2Application.class, args);
}
}
`@SpringBootApplication` 어노테이션 속성으로 제외할 `AutoConfiguration` 클래스를 정의할 수 있다.
scanBasePackage는 나쁜 걸까?
spring-boot-starter로 바꾸기(1)에서 문제점으로 언급했던 빈 등록 방식은 scanBasePackage 방식이다. 모듈 사용자가 필요 없는 Bean까지 등록되기 때문에 일종의 꼼수(나쁜 코드)라고 생각했고, 3개의 포스팅에 걸쳐 AutoConfigure방식으로 변경했다.
글을 작성하면서 몇몇 글을 참고하다 scanBasePackage 방식을 장려하는 글을 발견했고, 이내 내가 잘못생각하고 있다는 것을 깨달았다.
문제는 모듈 간의 의존성이다. 모듈이 점점 거대해지면 당연히 각 프로젝트마다 불필요한 설정이 생기기 마련이다(내가 계속 예시로 들었던, 배치프로젝트에서 웹과 관련된 Bean들이 필요 없는 것).
문제의 핵심 원인은 모듈을 분리하지 않은 것이고, 목적에 따른 별개의 모듈 프로젝트로 분리함으로써 해결했어야 했다. 공통 모듈이라는 것은 정말 모든 프로젝트에서 사용될만한 것들만 정의하고, 웬만하면 사용을 지양해야 한다.
모듈이 목적에 맞게 잘 분리돼있으면, 어플리케이션 프로젝트에서 모듈을 사용하겠다고 의존성을 명시한다는 것은 모듈의 대부분의 Bean들이 필요하다는 뜻이다.
따라서 모듈의 Bean들은 편의를 위해 scanBasePackage 방식으로 등록한다. 단, 모듈의 시스템 설정과 관련된 Bean들만 AutoConfigure로 관리하고, 사용자가 확장할 수 있도록 `@Conditional` 혹은 `Customizer`방식을 제공한다.
상세한 코드는 아래 깃허브에서 확인할 수 있다.
'Backend > Java' 카테고리의 다른 글
오라클 - 마이바티스 날짜형 데이터 맵핑 (0) | 2023.07.12 |
---|---|
Java에서 Null을 다루는 방식 (0) | 2023.05.22 |
Stream에 Decorator 패턴 써먹기 (2) | 2023.04.03 |
공통 lib -> spring-boot-starter로 바꾸기(2) (0) | 2023.03.20 |
공통 lib -> spring-boot-starter로 바꾸기(1) (0) | 2023.02.13 |