MSA 프로젝트를 하면서, 굉장히 불편했던것이 공통되는 코드를 계속해서 다시 작성해야한다는 것이었다.
예를 들어, 예외의 계층 구조라던가, 공통 예외 응답 형식이라던가, JwtAuthenticationFilter라던가
그것보다 더 문제가 되는것은, 나의 경우, 각 예외 상황마다 Account0001
과 같이 유니크한 키를 박아놨는데, 이것이 프로젝트간에 공유가 안되다 보니, 문제가 있었다.
지금까지는 그냥 복붙하며 2개의 마이크로 서비스와, 게이트 웨이 ... 을 만들었지만, 이번에 Optimization Api 를 제공하는 새로운 서버를 만들면서 더이상 이렇게는 못하겠다는 생각이 들었다.
불편은 둘째치더라도, 저런 예외 코드가 수정되면, 각각의 서버를 다 수정해줘야하고, 이게 또 일치한다는 보장도 없기 때문에 큰 문제였다.
그래서 Gradle을 기반으로 멀티 모듈을 도입하기로 하였다.
위와 같이, 모듈을 나누고, 서로 다른 모듈을 참조할 수 있는 구조를 말한다.
나는 처음에 굉장한 혼란이 왔었는데, 이게 의존성이 있으면 통째로 jar로 빌드되어야 하나? 같은 거였다.
그런데 그건 아니고, 각각의 모듈을 빌드 할 수 있고, 빌드 시점에 저 공통 모듈을 끌어와서 같이 빌드하는 식이었다.(세상 편하네,,,)
나의 경우, 저런 예외 코드라던지, 공통적으로 쓰이는 필터 같은 것을 공통 모듈에 저장하고, 핵심 비즈니스 로직만 각각의 모듈에 저장하면 좋을 것 같다고 생각했다.
그래서 감도 잡을겸 데모 프로젝트를 만들어 보았다. 다음 블로그를 많이 참조했다
나의 프로젝트를 보면
이러한 구조로 만들어 보았는데, common에는 main 메서드는 없고, pojo랑 bean 등록만 존재한다.
SeviceA와 ServiceB가 Common을 참조하는 구조이다.
루트 프로젝트의 build.gradle은 다음과 같이 작성했다.
common은 bootJar가 안되도록 막아놨다.
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.2'
id 'io.spring.dependency-management' version '1.1.4'
}
bootJar {
enabled = false
}
repositories {
mavenCentral()
}
subprojects {
group = 'click.porito'
version = '1.0-SNAPSHOT'
sourceCompatibility = '17'
apply {
plugin 'java'
plugin 'java-library'
plugin 'org.springframework.boot'
plugin 'io.spring.dependency-management'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
// 공통 의존성
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
test {
useJUnitPlatform()
}
}
project(":serviceA") { // 컴파일 시 common 모듈을 로딩
dependencies {
implementation project(":common")
}
}
project(":serviceB") { // 컴파일 시 common 모듈을 로딩
dependencies {
implementation project(":common")
}
}
project(":common") {
bootJar {enabled = false}
jar {enabled = true}
}
이렇게 하니까 프로젝트 루트에서, 한번에 의존성들을 관리할 수 있게되었다.
settings.gradle은 다음과 같다
rootProject.name = 'multi-module-demo'
include('serviceA')
include('serviceB')
include('common')
Common 쪽에 다음과 같은 코드를 작성하였다
그리고 Service A를 실행시켜보았다.
package click.porito.servicea;
import click.porito.common.CommonObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.util.function.Function;
@SpringBootApplication(scanBasePackages = "click.porito")
public class ServiceAApplication {
@Autowired
private Function<String, Integer> test;
public static void main(String[] args) {
CommonObject commonObject = new CommonObject("name", "description");
System.out.println(commonObject);
SpringApplication.run(ServiceAApplication.class, args);
}
}
예상 대로라면, 두개의 Print가 되어야한다.
둘다 잘 찍혔다.
주의해야할 점은 저 basePackage를 조정을 해야한다.
SpringBoot의 정말 편한 점중 하나인 AutoConfiguration
도 동작하는지 궁금해서 직접 테스트를 해보았다.
매우 잘 작동한다.
이를 통해, 공통 모듈에서 정말 라이브러리 같이 필요한 설정이라던지, 공통 코드들을 모아놓고 안전하게 사용할 수 있을것 같다