이제 본격적으로 모놀리식 아키텍쳐 프로젝트를 MSA 아키텍쳐로 전환해보고자 합니다!
이번 포스팅에서는 단일 모듈로 구성되어 있는 프로젝트를 멀티 모듈로 구성하는 과정을 담아보겠습니다.
그 전에 간단하게 멀티 모듈이 무엇인지 살펴보고 적용 과정을 담아보겠습니다!
1. 멀티 모듈이란?
Oracle의 Java Document에서는 모듈을 다음과 같이 정의합니다.
패키지의 한 단계 위의 집합체로, 관련된 패키지와 리소스들을 재사용할 수 있는 그룹
간단히 말하면 모듈은 패키지의 집합을 의미합니다.
일반적으로 간단한 프로젝트는 여러 패키지를 그룹화한 단일의 모듈로 구성되는 경우가 많습니다.
제 개인 프로젝트도 하나의 단일 모듈로, 모든 소스 코드가 하나의 단일 모듈에 존재하는 경우입니다.
멀티 모듈이란, 말그대로 모듈을 여러 개 생성하여 프로젝트를 구성하는 구조입니다.
단일 모듈의 소스 코드를 용도에 맞게 하나씩 모듈화하여 여러 개의 모듈로 만든 것이 멀티 모듈입니다.
멀티 모듈을 구성하게 되면, 다음과 같은 프로젝트 구조가 됩니다.
이름에 Bold 처리가 된 부분이 모두 모듈입니다.
저는 단일 모듈 하나에 존재했던 소스 코드를 멀티 모듈로 역할을 다음과 같이 분리했습니다.
- shboard-common 모듈 : 기존 프로젝트에서 전역으로 공유했었던 소스 코드들 (ex : 인증, 구성, 전역 예외 처리 등)
- member-service 모듈 : 멤버 도메인과 관련한 모든 소스 코드들
- board-service 모듈 : 게시판 도메인과 관련한 모든 소스 코드들
이처럼 하나의 프로젝트 안에서 멀티 모듈 환경을 구성해서 프로젝트를 빌드 및 배포할 수 있습니다.
2. 멀티 모듈을 왜 사용할까?
멀티 모듈을 사용하는 이유에는 크게 2가지가 존재하는 것 같습니다.
- 멀티 프로젝트의 공통된 소스 코드를 묶기 위해 멀티 모듈로 구성
- 단일 프로젝트에서 소스 코드를 어떠한 단위로 분리하기 위해 멀티 모듈로 구성
2-1. 멀티 프로젝트의 공통된 소스 코드를 묶기 위해 멀티 모듈로 구성
위의 그림처럼 A, B, C 프로젝트가 공통적으로 멤버 관련 소스 코드를 사용한다고 해봅시다.
이때, 공통되는 멤버의 코드에 변경이 생기면 어떻게 될까요?
여러 프로젝트에 코드가 존재하므로 IDE에서 3개의 프로젝트를 OPEN하고 변경되는 멤버 코드를 복사 붙여넣기를 해야할 것입니다.
말로만 해도 상당히 번거로운 작업이 될 것 같네요 😂
이러한 번거로움은 '단일 프로젝트의 멀티 모듈로 구성'하면 해결할 수 있습니다.
다음과 같은 그림처럼 말이죠.
이렇게 A, B, C 프로젝트로 존재했던 멀티 프로젝트들의 소스 코드들을 단일 프로젝트인 Common 프로젝트로 구성하고
그 안에서 각각 A, B, C 모듈로 구성한 후에 공통되는 멤버 관련 소스 코드는 개별 모듈로 분리하여 구성하면 해결할 수 있게 됩니다.
이렇게 되면 하나의 프로젝트이므로 하나의 IDE 창으로 관리가 가능하고, 멤버 코드 변경 시 한번만 변경하면 됩니다.
2-2. 단일 프로젝트에서 소스 코드를 어떠한 단위로 분리하기 위해 멀티 모듈로 구성
결론적으로 말하면, MSA를 위해 멀티 모듈을 사용하는 경우는 해당 2번째 이유때문에 멀티 모듈을 사용합니다.
앞서 1번 이유와는 조금 달리 공통의 코드를 추출하는 것이 목적이 아니라 어떠한 단위로 소스 코드를 분리하기 위해 사용합니다.
단위의 개념은 크게 2가지가 있는 것 같습니다.
- 계층(Layer)별
- 도메인별
MSA 전환 시에는 도메인별로 서비스가 나뉘기 때문에 도메인별로 모듈을 분리해서 프로젝트를 구성하면 됩니다.
이렇게 도메인별로 모듈을 분리하고 나면,
하나의 프로젝트 안에서 서비스 별로 모듈을 나누기 때문에 수월하게 MSA 구조를 잡고 빌드 및 배포를 진행할 수 있습니다.
따라서, 이러한 방식으로 멀티 모듈을 적용해보도록 하겠습니다.
3. 멀티 모듈 적용하기
최종적으로 구성할 멀티 모듈은 다음과 같습니다.
(discovery-server 모듈도 현재는 최종적으로 구현했지만, 일단 MSA 관련 모듈이므로 여기서는 제외하겠습니다.)
- shboard-common 모듈 : 기존 프로젝트에서 전역으로 공유했었던 소스 코드들 (ex : 인증, 구성, 전역 예외 처리 등)
- member-service 모듈 : 멤버 도메인과 관련한 모든 소스 코드들
- board-service 모듈 : 게시판 도메인과 관련한 모든 소스 코드들
※ Board-Member 서비스 간 연관관계 처리?
기존 모놀리식 구조에서는 Board가 Member를 참조해서 비즈니스 로직을 처리했습니다.
최종적으로 MSA 구조가 된다면, 하나의 마이크로 서비스에서는 해당 서비스 관련 로직만 존재하도록 할 예정입니다.
(물론, 경우에 따라 관련 엔티티를 가지는 경우도 있는 것 같지만 저는 하나의 마이크로 서비스에 하나의 엔티티만 존재하도록 하겠습니다!)
그래서 Board에서 Member를 참조하지 않을 것이지만, 우선 첫 단계인 멀티 모듈을 구성할 때는
Board-Service 모듈이 Member-Service 모듈을 의존해서 사용하도록 했습니다.
의존성 제거는 이후에 서비스 호출을 다룰 때 다룰 예정입니다.
인텔리제이 사용 시 모듈은 다음과 같이 추가할 수 있습니다.
File -> New -> Module 선택
모듈 생성 창 왼쪽 탭에서 New Module 선택 후 CREATE 클릭
3-1. Root 모듈 구성
기존의 소스 코드들이 존재했던 최상위 경로를 Root 모듈로 구성했습니다.
기존 소스 코드들을 모두 하위의 멀티 모듈로 구성할 것이기 때문에 Root 모듈에서는 다음과 같은 작업만 진행했습니다.
- 기존 소스 코드 삭제(src 폴더 삭제) - 작업 시에는 여러 하위 모듈에 소스코드를 복붙한 이후에 삭제했습니다.
- build.gradle에서 하위 모듈 설정
- settings.gradle에서 하위 모듈 추가
위의 사진과 같은 파일만 존재하도록 했습니다.
settings.gradle
rootProject.name = 'shboard'
include 'shboard-common'
include 'member-service'
include 'board-service'
include 'discovery-server'
settings.gradle에서는 하위 모듈들을 include로 선언해줘서 Root 모듈 설정에서 사용할 수 있도록 지정했습니다.
build.gradle
plugins {
id 'org.springframework.boot' version '3.2.2'
id 'io.spring.dependency-management' version '1.1.4'
id 'java'
}
repositories {
mavenCentral()
}
subprojects {
group = 'com.shboard'
version = '0.0.1'
sourceCompatibility = '17'
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'java-test-fixtures'
repositories {
mavenCentral()
}
ext {
set('springCloudVersion', "2023.0.0")
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
bootJar { enabled = false }
jar { enabled = true }
}
project(':board-service') {
dependencies {
implementation project(':shboard-common')
implementation project(':member-service')
implementation(testFixtures(project(":shboard-common")))
}
}
project(':member-service') {
dependencies {
implementation project(':shboard-common')
implementation(testFixtures(project(":shboard-common")))
}
}
최종적인 Root 모듈의 build.gradle이기 때문에 아직 서술하지 않은 나머지 모듈관련 설정들이 추가되어 있습니다.
중요하게 봐야하는 부분은 subprojects와 project()입니다.
subprojects에서 settings.gradle에서 선언한 하위 모듈들의 설정을 관리할 수 있습니다.
하위 모듈들에 모두 적용되는 설정들을 적용해줬습니다.
(플러그인 중 java-test-fixtures라는 플러그인이 존재하는데 지금은 테스트 관련 플러그인이라는 것만 알아두고 이후에 설명하겠습니다.)
project()에서 하위 모듈을 지정해서 해당 모듈에 들어갈 설정을 커스텀 할 수 있습니다.
board-service 모듈 설정은 shboard-common 모듈과 member-service 모듈을 의존하기 때문에 의존성을 추가해줬고,
member-service 모듈 설정은 shboard-common 모듈을 의존하기 때문에 의존성을 추가해줬습니다.
3-2. common 모듈 구성(공통으로 사용하는 모듈)
기존 소스 코드에서 도메인별로 소스 코드를 분리한다고 했을 때, 공통으로 사용되는 부분이 있었습니다.
저는 기존 소스코드에서 전역적으로 사용되는 코드들의 상위 패키지 명을 'global'로 설정했었습니다.
위의 global 패키지를 보면 인증 관련, 전역 설정(MVC 설정, Redis 설정, Session 설정 등), 전역 예외 처리들을 담당했습니다.
common 모듈에서는 해당 global 패키지의 소스 코드들을 모아서 구성하여 나머지 서비스들이 해당 모듈을 의존하도록 했습니다.
그리고 build.gradle에서는 common 모듈의 소스 코드에서 필요한 의존성들을 추가해줬습니다.
dependencies {
// common 모듈의 소스 코드들에서 사용하는 의존성 추가
}
※ Gradle - test fixtures 기능
common 모듈을 구성 시 특별한 부분은 테스트 관련 부분이었습니다.
기존 소스 코드에서 테스트 부분에도 공통적으로 적용되는 클래스들이 존재했습니다.
계층별 테스트에서 공통되는 초기 설정 코드들을 위처럼 추상 클래스로 생성하여 상속받아서 테스트를 진행했었습니다.
그래서 해당 테스트 코드를 common 모듈에 'test' 하위에 배치하고
이후에 서비스 모듈(member-service, board-service)에서 해당 공통 테스트 클래스를 상속받으려고 했습니다.
그러나, 해당 공통 테스트 클래스가 import되지 않는 것을 확인할 수 있었습니다.
다른 모듈의 Test에 쓰인 클래스를 사용할 수 없는 이유를 찾아보니 다음과 같았습니다.
각 모듈은 jar로 만들어지고 해당 jar를 다른 모듈에서 가져다 쓰는 것이기 때문에 빌드 된 jar에 test와 관련된 부분은 포함이 안되어 있기 때문에 사용할 수 없는 것입니다.
따라서, 테스트 시 클래스를 import하기 위해서는 다른 방법을 찾아야 했습니다.
이때 찾은 것이 바로 Gradle에서 제공하는 test fixtures 기능입니다.
자세한 설명은 공식문서를 참고하면 될 것 같고, 적용한 방법만 살펴보도록 하겠습니다.
https://docs.gradle.org/current/userguide/java_testing.html#sec:java_test_fixtures
1. Root 모듈에서 subprojects에 plugin으로 test fixtures 플러그인 추가
subprojects {
...
apply plugin: 'java-test-fixtures'
...
}
2. Root 모듈의 하위 모듈 개별 설정 projects에서 test fixtures 설정 추가
project(':board-service') {
dependencies {
...
implementation(testFixtures(project(":shboard-common")))
}
}
3. 테스트 픽스쳐를 사용할 모듈(common 모듈)의 build.gradle에서 testFixture에서 사용할 의존성 추가
dependencies {
...
// testFixture 필요 의존성 추가
testFixturesImplementation 'org.springframework.boot:spring-boot-starter-test'
testFixturesImplementation 'io.rest-assured:rest-assured:5.3.2'
testFixturesImplementation 'org.springframework.boot:spring-boot-starter-data-jpa'
}
이렇게 설정하고 나면, 다음과 같이 testFixtures가 생기고 서비스 모듈들에서 공통 테스트 클래스를 import해서 사용할 수 있습니다.
3-3. 서비스 모듈 구성(도메인 관련 모듈)
도메인 관련 모듈(member-service 모듈, board-service 모듈)은 설정이 비슷하기 때문에 함께 설명하겠습니다.
기존 소스 코드 중에서 도메인과 관련한 소스 코드(member 패키지, board 패키지)를 모듈로 분리했습니다.
build.gradle에서는 해당 도메인과 관련한 의존성들을 추가하는 작업만 수행했습니다.
dependencies {
// 각 도메인 모듈의 소스 코드들에서 사용하는 의존성 추가
}
이후에는 테스트 설정만 추가해서 작업을 마무리했습니다.
member-service 모듈의 테스트 관련 설정만 설명해보도록 하겠습니다.
기존 소스 코드의 구조대로 member-service 모듈의 테스트를 실행하면
테스트 스프링 컨테이너에 필요한 스프링 빈이 등록되지 않아서 테스트가 실패하는 것을 확인했습니다.
따라서 필요한 스프링 빈을 스캔하도록 테스트 Configuration 클래스를 만들어서 설정해줬습니다.
MemberTestConfiguration.class
@Configuration
@ComponentScan(basePackages = {"com.example.memberservice", "com.example.shboardcommon"}, basePackageClasses = {H2TruncateUtils.class})
public class MemberTestConfiguration {
}
MemberServiceApplicationTests.class
@SpringBootTest
@ContextConfiguration(classes = MemberTestConfiguration.class)
class MemberServiceApplicationTests {
@Test
void contextLoads() {
}
}
이렇게 Root 모듈, Common 모듈, 도메인(서비스) 모듈로 분리하여 다음과 같이 최종적으로 멀티 모듈을 구성했습니다.
멀티 모듈 소스 코드는 아래 깃헙에서 확인할 수 있습니다.
https://github.com/sh111-coder/sh-board-msa
다음 포스팅에서는 해당 멀티 모듈을 기반으로 MSA를 구성해 나가도록 하겠습니다.
🎯 Github Repository 링크 (전체 코드)
https://github.com/sh111-coder/sh-board-msa
📘 Monolithic to MSA 전체 목차
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (1) MSA란?
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (2) 멀티 모듈 구성하기
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (3) Service Discovery 패턴 적용하기(feat. Spring Cloud Eureka)
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (4) API Gateway 구현(feat. Spring Cloud Gateway)
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (5) 서비스 간 통신하기(feat.Spring Cloud OpenFeign)
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (6) 각 서비스의 설정 파일 관리하기(feat. Spring Cloud Config)
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (7) 서비스 장애 대응 Circuit Breaker 구현(feat. Resilience4J)
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (10) MSA 전환 후 비교 및 회고 + 마무리
Reference
https://jojoldu.tistory.com/123
'아키텍쳐' 카테고리의 다른 글
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (4) API Gateway 구현(feat. Spring Cloud Gateway) (0) | 2024.02.05 |
---|---|
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (3) Service Discovery 패턴 적용하기(feat. Spring Cloud Eureka) (2) | 2024.02.03 |
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (1) MSA란? (0) | 2024.02.01 |
Controller와 Service 레이어 간 요청 & 응답 Dto 사용에 관하여 (2) | 2023.06.19 |
[아키텍쳐] 패키지 구조 : 계층형 VS 도메인형 어떤 것을 선택할까? (4) | 2023.05.06 |