흰 스타렉스에서 내가 내리지

멀티 모듈 프로젝트 본문

Spring

멀티 모듈 프로젝트

주씨. 2023. 9. 9. 17:27
728x90

# 멀티 모듈 프로젝트 구조가 왜 중요한가?

- 나중에 변경하기 어렵다    - 리스크를 줄이기 위한 시작

 

멀티모듈 프로젝트는 빌드와 배포 프로세스에 밀접한 영향을 미친다.

시스템이 커져갈수록 빌드와 배포 프로세스도 복잡해지면서 나중에 이 프로젝트의 구조를 변경하게 되면, 빌드와 배포 프로세스 모두 변경해야한다.

 

 

우선은 META 라는 하나의 멀티모듈 프로젝트로 구성한다.

META 모듈은 서비스의 기반이 되는 공통 도메인이다. 모든 모듈들에서 필요로 한다. 

Track 이라는 도메인은 MySQL에 적재를 하고, Lyric 이라는 가사 도메인은 MongoDB에 적재를 한다면, META 모듈은 어디에 위치시키고 어떻게 구현을 해야할까?

 

META 모듈은 또 다른 멀티 모듈을 필요로 한다. 

바로 유관 부서 및 업체 연동을 구현한 인프라 모듈이다. 

보면, 음원을 재생하기 위한 AOD 연동 모델, 비디오 재생을 위한 VOD 연동 모듈, 이미지 서비스를 위한 Photo 연동 모듈, 결제 관련된 모듈 등.

 

이처럼 연동 모듈들은 지속적으로 늘어날 수 밖에 없다. 

처음 구현하게 되면 딱히 변경이 없다가, 특정한 시점에 버전업 요청이 오면 코드 변화가 굉장히 크게 생긴다. 

이것이 연동 모듈의 특징.

 

왼쪽의 녹색 서비스 모듈로만 시작됐던 프로젝트가 오른쪽에 빨간색 영역에 모듈들이 추가되면서 멀티모듈 프로젝트가 자연스럽게 2배 이상 커지게 되었다. 

추가로 Java 진영의 Spring Gateway와 Discovery 모듈이 추가되어 있다. 

이 부분은 시스템 성격의 프로젝트 모듈이라 aws나 azure gcp 구성을 위한 설정 파일 모듈로도 생각해 볼 수 있다. 

 

 

이제 점점 늘어나는 이 멀티 모듈들을 무슨 기준으로 나누고 구성해야 할까?

문제의 근원. 무조건 core, common을 삭제하고 시작한다. 

그러면 공통적인 부분은 어떻게 개발을 하나요?

일부 코드에 대해서 어느 정도 중복을 허용한다. 

코드가 일부 중복되는 것보다 core / common 이 잠재적으로 가지고 있는 위험성이 더 큰 문제가 될 수 있다. 

우리가 겪고 있는 지금 시대는 10년, 20년 전에 비해 엄청나게 복잡하고 고도화되어 있다. 

이런 말이 있다. 그 때는 맞고 지금은 틀리다. 

"중복은 제거되어야 한다." 라는 말에 매우 공감하지만, 중복을 제거하기 위해서 모든 것을 한 곳에 구현하는 core / common 이라는 공통 모듈을 운영하는 방식은 옳지 않은 것 같다. 

 

 

멀티 모듈 프로젝트는 무엇을 기준으로 나누어야 할까?

결론은 ddd에서 일명 경계 나누기라고 불리우는 바운디드 컨텍스트다. 

특정한 컨텍스트 문맥 하에서 완전한 의미를 갖는 경계의 기준을 잘 나누는게 멀티모듈 프로젝트 구조를 설계할 때도 무엇보다 중요하다. 

이 기준을 바탕으로 멀티모듈 그룹이라는 4개의 그룹을 일반적으로 정의해서 사용한다. 

기준이 되는 멀티그룹 4개의 구성은 어떻게 되는지 다시 한 번 살펴보자. 

 

1. Boot 서버 그룹

batch, admin, api 같은 서버 어플리케이션은 코드의 변화가 제일 작게 일어난다.

이런 모듈을 하나로 합쳐서 하나의 그룹으로.

 

2. 데이터 그룹

데이터 그룹은 서버 모듈과 밀접한 관계가 있다.

도메인 영역으로 데이터 스토어를 직접 핸들링하게 된다. 

 

3. 인프라 그룹

유관 부서 및 업체 연동을 위한 그룹 모듈.

구현되고 나면 변화는 적지만, 나중에 버전업이 되면 코드에 큰 변화가 일어나게 되는 그룹

 

4. 클라우드 시스템 그룹

서버 관리를 위한 그룹 모델

컨테아너 환경과 트래픽 제어를 위한 시스템 관련 그룹이다.

 

이렇게 4개의 멀티그룹은 각자 보유한 성격과 특성 사이클을 가지고 있고, 그리고 그 기준으로 경계를 나누고 프로젝트를 구성한다. 

 

 

# 실전 멀티 모듈 프로젝트 구현은 어떻게 해야할까?

경계나누기로 정의했던 4개의 멀티그룹을 하이폴더로 생성한다. 

그리고 그 다음 멀티그룹 성격에 맞게 프로젝트를 각각 구성한다.

이렇게 구성하게 되면 플랫하게 나열된 프로젝트 구성보다 가독성이 좋아지고, gradle 과 같은 빌드 도구에서 그룹 폴더명을 기준으로 정의할 기술들을 일괄적으로 선언할 수 있는 장점이 있다.

 

gradle build script에 boot로 시작되는 프로젝트에는 jib라는 도커플러그인을 일괄적으로 한 번에 선언해 주는 모습을 볼 수 있다. 

 

조금 더 자세하게 멀티 그룹내 프로젝트 구조에 대해서도 살펴보자 

첫번째로, boot 멀티그룹이다. 웹 어플리케이션 그룹이다. 

admin, batch, api 이런 어플리케이션 프로젝트들이 존재할 수 있다. 

 

두번쨰는, data 모듈이다. 뮤직서비스에서는 메타라는 데이타 영역이 있고, 또 유저들을 위한 유저 데이터, 차트 데이터, 보관함 같은 라이브러리 데이터가 데이터 레이어 프로젝트로 편성될 수 있다. 

 

세번째, 인프라 프로젝트는 유관 부서나 업체 연동을 위한 모듈. 

VOD, AOD, Photo, Billing 과 같은 프로젝트가 존재할 수 있다. 

 

네번쨰, 클라우드 시스템 관련 프로젝트다. 

지금은 스프링 클라우드 제품인 Gateway와 Discovery, config라는 제품이 있다. 

 

 

여기서 문제점을 또 맞이하게 된다. 

프로젝트가 늘어나는만큼 빌드 시간도 늘어나면서 개발 생산성에 영향을 미치게 되었다. 

또 클라우드 멀티그룹 내에 remote config 서버 프로젝트라는 설정 파일을 수정하게 되면 깃허브에 webhook이 발생하게 되서, 여러 어플리케이션에 이벤트를 전달해서 설정 데이터가 리플래시가 되는 처리를 하고 있는데,  해당 프로젝트의 변경만이 아닌 다른 프로젝트에 코드 변화에도 이벤트에 반응하고 있어서 관리적이 이슈가 발생하였다. 

 

프로젝트를 멀티그룹의 특성에 맞게 다시 한 번 나눈다. 

먼저, 변화가 가장 잦은 서비스 기능 레포지토리의 부트 데이터 멀티그룹은 특성이 같기 때문에 한 곳으로 그대로 유지한다.

 

그리고 그 다음은 클라우드 시스템 멀티 그룹을 별도의 2개의 저장소로 다시 분류한다. 

하나는 시스템적인 프로젝트, 또 다른 하나는 webhook 이벤트를 필요로 하는 리모트 컨피그 서버 프로젝트로 분리한다. 

 

마지막으로, 인프라 멀티 그룹을 다시 별도의 인프라 라이브러리라는 리포지토리로 분리한다. 

 

 

다시 말하자면, 이렇게 각자 특성에 맞게 프로젝트를 분리하게 되면, 빌드 시간도 줄어드는 이점이 있지만, 더욱 중요한 점은 프로젝트 경계가 명확해짐으로써 실제 인터페이스 구현 설계도 어떤 내용을 주고 받아야 하는지 명확해진다. 

 

이제 분리되어 있는 각 저장소별로 상호 구현을 어떻게 해야 하는지 이야기 해보자. 

 

첫번쨰로, 데이터 프로젝트와 인프라 라이브러리 프로젝트 간에 관계 구현에 대한 내용이다.

뮤직 도메인에는 Track 하위에 Lyric과 Playback이라는 도메인이 존재하는데, 

Playback은 AOD라는 인프라 라이브러리의 모듈을 통해서 외국에 존재하는 AOD 서버의 재생 관련 데이터를 요청하고 응답받게 됩니다. 

AOD 인프라 라이브러리를 사용하는 방식은 일반적으로 중앙의 Artifactory라는 저장소에 빌드 타임에 라이브러리를 업로드하게 되고, 사용하고자 하는 프로젝트에서 의존성을 선언한 후 구현하게 된다. 

 

이 관계에 대해서 어떤 방식으로 실제 구현을 해야할까?

클래스 다이어그램을 통해서 자세히 살펴보자. 

 

data-meta 모듈에 playbackService 라는 인터페이스를 하나 선언한다. 

인터페이스를 만든 이유는, VOD 서비스도 재생을 위한 구현체가 필요하고, Track 을 위한 구현체도 필요하기 때문 

외부의 AOD 서버는 인프라 AOD 라이브러리 프로젝트 내에 AOD API 클라이언트 구현체를 통해서 요청과 응답을 받게 된다. 

그 다음은 playbackService 라는 인터페이스를 상속받아 TrackPlaybackService를 구현하고 AOD API 클라이언트를 추출하게 된다. 

이 때 일반적으로 외부 서비스 연동을 위해서는 아마도 내부적인 trackId와 aodId와 같은 키를 매핑해 둔 데이터 저장소를 사용하게 된다. 

 

이렇게 하면 좋을 듯 했지만 문제가 다시 발생하게 된다. 

만약 해당 라이브러리에서 사용하는 batch job의 에이전트 수가 50개 100개 200개 라면, 역시나 "Too many connections" 문제를 만나게 된다. 

그럼 이 문제를 해결하기 위해서는 어떻게 코드를 다시 변경해야 할까?

 

Too many connection 오류를 피하기 위해서는 어쩔 수 없이 DB 접근을 데이터 모듈 쪽으로 이동해서 구현을 해야 한다. 

이제 TrackPlaybackService 구현체는 어디로 이동을 해야할까?

 

당연히 DB 호출을 해야하기 때문에 data-meta 모듈로 이동해서 구현을 해야 한다. 

이제 마지막으로는 인프라 AOD 모듈에서 AodPlaybackService를 구현해서 요청과 응답에 대해서 책임을 지면 된다. 

 

결국 모두를 나누는 경계로 생각해보면 인프라 라이브러리 프로젝트는 호출하는 프로젝트와 관계없이 자신의 역할과 책임인 AOD 서버를 올바르게 호출하기 위해 구현이 되어야 한다는 것이다. 

협력 관계에서 주고받는 메시지가 객체의 책임을 결정하고, 그 책임을 자율적으로 만드는 것이 어플리케이션의 품질을 결정하는 좋은 방법론이다. 

 

지금까지 데이터 모듈과 인프라 라이브러리 모듈 관계에 대해서 알아보았다. 

 

다음은, BOOT 서버 프로젝트와 데이터 프로젝트 관계 구현에 대해 알아보자.

여기서 가장 많이 나오는 질문이 서비스 구현체는 어디에 두어야 하는가?

 

자바의 스프링 프레임워크 기반으로 개발을 진행하게 되면, 일반적으로 요청받게 되는 컨트롤러에 이어서 서비스를 구현하고 서비스에서 데이터를 질의하는 레포지토리를 구현하게 되는데, 이 때 서비스 구현체는 어느 모듈에 위치를 해야할까?

 

 

 

#서비스 레이어 구현은 어디에? (일반가사(text)는 있는데 싱크가사(text, timestamp)가 없는 경우 후처리로 생성(timestamp)하기)

런타임의 이벤트를 전달해서 후처리로 생성해주고자 하는데, 어떻게 구현할까?

 

일반적으로, 요청이 오게되면 boot-meta 프로젝트에 TrackLyricController에서 데이터 메타 프로젝트의 TrackLyricRepository를 통해 가사 데이터를 응답받게 된다. 

이 때, 가사 데이터의 timestamp 값이 없다면, 생성되도록 요청이 되고, 그럼 이 때 서비스는 어디에 구현을 해야할까?

 

양쪽 모두에 구현을 하면 어떨까?

싱크 가사를 생성하는 이벤트는 boot-meta 모듈의 CreateLyricService 클래스를 통해서 발생되어야 하고, timestamp 값이 만들어지게 되면, 이 데이터를 data-meta 모듈의 TrackLyricService를 통해서 저장되야 할 것 같다

 

이처럼 서비스 구현체는 프로젝트 별로 역할에 맞도록 각자 각각 구현되어야 한다. 

그리고 boot-meta와 data-meta 프로젝트 간에는 상호간의 규칙이 한가지 필요하다.

이 규칙은, data-meta 프로젝트에서는 반드시 servlet request와 같은 웹 서버에 의존적인 객체를 전달하지 않는 것이다. 

ServletRequest 객체가 말단 layer 까지 내려가는 응용 서비스를 많이 보았는데, 이런 경우는 역할과 책임 그리고 협력관계에 올바르지 않다고 생각한다. 

 

data-meta 모듈은 배치에도 사용될 수 있고, 웹 어플리케이션에도 사용될 수 있고, 또 다른 원타임 어플리케이션에서도 사용될 수 있다. 

모드가 웹서버 기반으로 동작되지 않을 수 있기 때문에 data-meta module은 ServletRequest 같은 의존적인, 웹과 의존적인 클래스를 전달하지말라.

쿠키나 세션도 마찬가지다. 해당 객체가 넘어가는 순간 data-meta 프로젝트에서는 웹 기반의 의존성들이 강하게 주입된다. 

그럼 어떤 이슈가 생기는가? test 코드에서 웹 서버 관련된 라이브러리가 필요하게 되고, 라이브러리가 의존성을 갖게 되는 순간 많은 일들이 일어난다. 

그렇게 되면 data-meta 라는 모듈에 의미가 상실되기 시작한다. 

 

이제 마지막으로 클라우드 시스템 프로젝트와 부트서버 프로젝트 간의 관계 구현에 대해서 이야기해 보자. 

 

Discovery와 같은 제품을 사용한다면 한 가지 의문이 들기 시작한다. 

Discovery 제품에 버전업이 발생하게 되면 모든 서버 프로젝트가 의존성을 함께 갖기 때문에 다시 빌드 한 후 배포해야 하는 불편함을 겪게 된다. 

문제는 웹 어플리케이션의 변화가 없었음에도 시스템적인 버전업으로 다시 전체 빌드 배포가 되는것은, 어쩌면 둘 간의 의존성을 더욱더 명확하게 분리해서 정리시켜야 하지 않을까 라고 생각할 수 있다. 

 

Istio는 쿠버네티스 기반의 서비스 메시 플랫폼으로 인보이 프록시를 통해서 트래픽을 관리제어하고 있다. 

사진에서 보이듯이 오토바이 옆에 붙어있는것을 사이드카라고 한다. 

Istio 역시 별도로 인보이라는 프록시 제품이 사이드카 패턴 방식으로 자신의 어플리케이션을 관리 제어하게 된다. 

만약 쿠버네티스 기반의 어플리케이션을 구축한다고 고민해보면, 한 번 쯤 도입을 고민해보자. 

 

 

 


#정리

1. "왜" 멀티 모듈 프로젝트 구조가 중요할까?

- 잘못 구성되면 나중에 변경하기 어렵다.

   - 컴파일 랭귀지라는 프로젝트를 운영을 해도 런타임에 가서 확인을 할 수 밖에 없는 상황들이 반드시 존재한다.

- 프로젝트 초기에 이루어져야 하는 일련의 설계 과정이다.

- 개발 생산성에 막대한 영향을 미친다. 

    - 코드 한 줄을 수정했지만 전체 빌드가 되어야 하고 배포가 되는 상황을 맞이하면 ..

 

2. "무엇"을 기준으로 멀티 모듈 프로젝트 구조를 나누어야 할까?

- 경계 안에서 의미를 갖는 그룹을 정의하는 것이 가장 중요하다. (Bounded Context)

- 역할, 책임, 협력 관계가 올바른지 다시 한 번 생각한다. 

- BOOT(Server), INFRA, DATA(Domain), SYSTEM(Cloud)

 

3. "어떻게" 실전 멀티 모듈 프로젝트 구현을 해야 할까?

- 프로젝트가 커지고 있다면 다시 경계를 나누고 그 기준으로 소스 저장소를 분리한다. 

- INFRA(외부) 라이브러리에는 DATA 관련 구현을 지향한다. 

- 서비스 구현은 각자 역할에 맞게 각각 구현될 수 있다.

- 시스템 레벨 구현이 실제 서비스 애플리케이션과 밀접하게 연관되지 않도록 격리하거나 전환(Istio)한다. 

'Spring' 카테고리의 다른 글

멀티 모듈 생성 2  (0) 2023.09.10
멀티 모듈 생성 1  (0) 2023.09.10
Builder Pattern :: 빌더 패턴  (0) 2023.08.03
DTO  (0) 2023.08.03
Transaction 롤백?  (0) 2023.07.13