01. 들어가기 전,
현재 하고있는 프로젝트는 처음에는 하나의 모듈의 모든 기능을 작성 하고 있었습니다!
보시면 domain, global 로 나누고, 도메인 주도 기반으로 해서 구현하고 있는 과정인데용
위를 보시면, 도메인 별로 batch, controller, dto, .. 요렇게 나눠져 있었던 구조였습니당..!
서버 인스턴스 하나만으로 시작할 때는 서비스의 복잡도가 상대적으로 낮고 유지보수도 간편한데,, ! 트래픽이 증가하거나 작업 부하가 늘어나게 된다면? 나중을 위해 지금이 멀티모듈로 구조를 바꿔야 하는 적절한 시기라고 생각했습니당
저희는 ASG와 로드밸런싱 구성을도입을 했는데요,
이런식으로 API 서버는 사용자 요청에 실시간으로 대응을 해야하기 때문에 ASG를 통해 트랙픽에 맞춰 하는 것이 바람직하다고 생각했습니당 하지만, 로드 밸런싱으로 인해 인스턴스가 여러 개로 분산되면, 배치 작업이 각 인스턴스에서 개별적으로 실행될 수 있기 때문에 배치 작업이 의도치 않게 N번 실행되는 문제가 생기지 않을까용
이때, 배치 작업의 중복 실행으로 데이터의 불일치나 리소스 낭비 등등 초래하는 문제들이 있습니다!
-> 배치작업을 API 서버와 분리하여 API 서버는 오토스케일링과 로드밸런싱으로 유동적 확장을 하고, 배치는 지정된 인스턴스에서만 독립적으로 실행하여 중복 실행 없이 안정적으로 작업을 처리할수 있지않을까!!
그렇다면,,,! 배치서버와 API 서버를 분리할때, 공통 엔티티와 로직은 어떻게 할지!
사실 가장 간단한건 copy & paste이겠죠..? 그래도 일관된 데이터 구조와 로직을 위해 멀티모듈로 대공사를 진행하기로 하였습니다!
멀티모듈에서는 역할 분리를 명확하게 하고, 각 모듈의 책임과 목적을 분리하는 것이 가장 중요하다고 생각하는데요. 여러 레퍼런스를 찾아봤는데요, 인프콘 2022 | 실전! 멀티 모듈 프로젝트 구조와 설계 - 네이버 김대성
보고 조금 도움되는 부분들을 정리 해보았습니다!
1. 어떤 기준으로 멀티 모듈 프로젝트 구조로 나눠야할까?
- 역할, 책임, 협력 관계가 올바른가?
메타라는 큰 하나의 멀티모듈로 구성해보자면 모든 모듈들에서 필요로함
→ track: mysql 적재, lyric: mongodb에 적재되는 경우 meta 도메인은 어디에 위치할지에 대해 고민
추가적으로, vod, aod, photo, ai 등등 연동 모듈들이 있는데, 지속적으로 늘어난다!
해결
- core common을 삭제하고 시작하자!
- 중복은 제거되는건 맞지만 core common이 정말 필요한지 고민을 해보자
- bounded context: 경계나누기
- 멀티모듈 프로젝트 구조를 설계해보자
- DDD는 큰 모델을 서로 다른 bounded context로 나누고 상호 관계를 명시하여 처리합니다
1)boot(server)
- 서버 모듈[잦은 변화]
- batch, admin, api
2)data(domain)
- 데이터 모듈(+도메인)
- meta, user, chat
3)cloud(system)
- 클라우드(시스템)모듈[변화적음]
- configm gateway, discovery
- aws, gcp, azure
4)infra
- 연동모듈[큰변화]
- and, vod, photo, billing
DATA(DOMAIN) → INFRA
해당 라이브러리에서 사용하는 배치 잡에 agent 수가 많다면 too many connections!
해결
- db접근을 data모듈로 이동해서 해야하고, trackplaybaackservice 구현체도 → db호출 위로 (data모듈)
- infra aod에 서비스는 호출하는 프로젝트와 관계 없이 자신의 역할과 책임 만을 ! 가진다
- → 즉, 협력관계에서 주고받는 메시지가 객체의 책임을 결정하고, 책임을 자율적으로 만드는 것이 좋다
BOOT(SERVER) → DATA(DOMAIN)
이 사이에 서비스 구현체는 어디에 두어야할까?
controller - service - repository
- 이런식으로 전부 한번에 구현하는것이 아니라 각각의 모듈 안에 역할에 맞도록 구현되어야한다!
- servlet request 메서드에 의존적인 객체를 전달하지 않아야한다!모두 웹서버로 동작하지 않는 경우가 있기 때문임
- → 이 객체가 말단까지 내려오면 역할과 책임이 올바르지않다
또한 문제: 테스트코드 작성시, 의존성을 전부 주입시켜줘야함,, →그렇다면 데이터 메타라는 모듈의 의미가 상실된다
결론
1.왜 멀티모듈 프로젝트 구조가 중요할까
- 잘못 구성되면 나중에 변경하기 고통스럽다.
- 프로젝트 초기에 이루어져야하는 일련의 설계 과정이다.
- 개발 생산성에 막대한 영향을 끼친다.
2.무엇을 기준으로 멀티 모듈 프로젝트 구조를 나눠야할까?
- 경계안에서 의미를 갖을 수 있는 그룹을 정의하는 것이 중요하다.
- 역할, 책임, 협력 관계가 올바른지 다시 생각한다.
- BOOT, INFRA,DATA, SYSTEM
3.어떻게 실전 멀티 모듈 프로젝트 구현을 해야할까?
- 경계를 나누고 그 기준으로 소스 저장소를 분리한다.
- INFRA (외부)라이브러리에는 DATA 관련 구현을 지향한다.
- 서비스 구현은 각자 역할에 맞게 각각 구현할 수 있다.
- 시스템 레벨 구현이 실제 서비스 애플리케이션과 밀접하게 연관되지 않도록 격리하거나 전한다.
02. 프로젝트에 적용해보자!
그전에, BOOT(SERVER) → DATA(DOMAIN) 에서 발생했던 문제들이요!
그 중간에 있는 서비스들은 어떻게 해야하며..? 그리고 각각의 서비스들은 해당 역할만을 가져야하기에 각각의 모듈로 들어가야합니다!
그래서 repository에 의존하는 service class를 구성하고, controller에서 호출하는 service는 바로 repository를 의존하지 않도록 하나를 더 두었습니다! A->B , B-> 와 같은 순환참조를 막기에도 좋다고 생각했습니당
즉, repository -> service -> businessService -> controller로 구성하고,
repository와 service는 core-data 모듈에 두고, businessService와 controller는 boot>external-api에 두었습니당
<FeedCardServcie(service)>
<CardService(businessService)>
<완성된 멀티모듈 구조>
<core - build.gradle>
루트모듈에는 src 내용을 삭제하였습니다!
- 루트 모듈 (bootJar = false, jar = true)
- 루트 모듈에서는 모듈 간 의존성 관리나 공통적인 설정을 정의하는 용도로 사용하여 -> bootJar 생성안함 false로 설정
- 실행가능한 JAR이 아닌, 일반 JAR 파일만 생성 -> jar = true로 설정
bootJar: Spring Boot 애플리케이션에서 사용되면, 애플리케이션을 실행 가능한 JAR 파일로 패키징
jar: 일반 Java 라이브러리나 애플리케이션을 빌드하는데 사용, Java 프로젝트의 컴파일된 클래스 파일과 리소스 파일들을 하나의 JAR 파일에 패키징
<core setting.gradle>
하위모듈을 프로젝트에 포함 시켜줘야합니당
<boot_batch build.gradle>
batch 모듈
- 기본 설정이 적용되고, 즉 공통 설정에 의존하도록 해야한다! -> bootjar, jar 설정 없이 기본 설정!
<boot_external-api>
+dependencies 에는
external-api 모듈 (bootJar = true, jar = false)
- external-api 모듈은 독립적으로 실행될 수 있는 Spring Boot 애플리케이션 -> bootJar = true
- jar = false로 설정함으로써 일반 라이브러
<cloud build.gradle>
- 마찬가지로, bootJar = fasle, jar = true 처리!
- cloud에 필요한 dependencies가 포함되어있습니당
<core-data build.gradle>
- 마찬가지로, bootJar = fasle, jar = true 처리!
- dependencies에는 데이터 관련 의존성들이 모아져 있습니다!