부트캠프 팀 프로젝트로 MSA를 진행하게 됐다. 처음에는 도메인마다 GitHub 레포를 나누는 게 MSA의 정석처럼 느껴졌다. 그런데 오늘 팀 회의에서 팀원분이 멀티모듈 모노레포 방식을 가져와 설명해 주셨고, 그 구조를 기준으로 이야기를 나누면서 생각이 정리되기 시작했다.
핵심은 “레포를 많이 나누는 것”이 아니라, 한 레포에서 도메인 서버를 Gradle 멀티모듈로 두고도 독립 배포를 유지할 수 있다는 점이었다. 팀 규모와 일정, Spring 서버 구조를 두고 판단할 때 멀티모듈 모노레포 방식이 유리해 보였다.
오늘 회의에서 공유된 방향을 바탕으로 코드로 옮기기 전 우리가 어떤 구조를 고르고 무엇을 지키면서 MSA로 갈 수 있는지 정리해보려 한다.
Table of contents
Open Table of contents
MSA는 레포 개수가 아니다
MSA의 핵심은 레포를 많이 만드는 일이 아니다. 배포 단위, 소유권, 데이터, 실패 경계를 나누는 일이다.
그래서 저장소 전략은 보통 세 가지로 나뉜다.
| 방식 | 형태 | 잘 맞는 상황 |
|---|---|---|
| Polyrepo | 서비스마다 레포 분리 | 팀/배포 독립성이 최우선일 때 |
| Monorepo | 한 레포에 여러 서비스 | 공통 변경과 협업이 잦을 때 |
| Hybrid | 서비스는 분리, 공통만 공유 | 독립 배포와 계약 공유를 같이 챙길 때 |
큰 조직의 모노레포 사례에서 모노레포를 사용하면서 얻는 이점은 “폴더를 한곳에 모아서”가 아니라 원자적 변경, 공통 코드 공유, 표준화, 영향 범위 파악에서 나온다. 다만 그 이점은 모듈 선택적으로 빌드, 권한, CI 같은 관리 비용을 감수할 때 커진다. Google처럼 자체 VCS까지 쓰는 경우도 있지만, 소규모의 프로젝트에서는 GitHub 한 레포 위에서 Gradle 멀티모듈과 Actions로 같은 방향을 작게 가져갈 수 있다.
반대로 polyrepo가 필요한 이유도 명확하다. 배포와 실패를 강하게 끊고 싶을 때, 팀 권한이 레포 단위로 갈릴 때, 모노레포 관리 비용을 감당할 여유가 없을 때다. 우리 상황은 이런 상황을 고려할 시기는 아니다.
polyrepo가 부담인 이유
부트캠프 팀은 인원이 적고, 일정이 짧고, 도메인을 같이 보며 배운다. 이런 환경에서 서비스마다 레포를 나누면 독립성은 생겨 보이지만, 실제로는 관리 비용이 더 커질 수 있다.
- 공통 예외, 응답 포맷, 유틸을 서비스마다 복사하기 쉽다
- CI/CD 워크플로를 레포마다 복제하고 맞춰야 한다
- 로컬에서 전체 흐름을 보려면 여러 레포를 클론하고 포트를 맞춰야 한다
- 작은 계약 변경에도 PR과 리뷰가 여러 갈래로 쪼개진다
읽어본 여러 블로그 글에서도, 소규모 팀이 레포를 과도하게 나누면 공통 코드 중복과 설정 불일치가 먼저 쌓인다는 내용이 있다. 같은 문제를 처음부터 겪을 필요는 없다고 생각한다.
이번 프로젝트는 레포를 많이 나누는 것이 아니라, 경계를 나누되 협업 비용은 낮추는 것에 초점을 두는게 맞는 판단인 것 같다.
멀티모듈 모노레포
오늘 회의에서 팀원분이 제안해 주신 방향이 바로 이것이다. 각 도메인을 독립 Spring Boot 서버로 두고, 최상위 Gradle에 모듈로 묶어 한 레포에서 관리하는 멀티모듈 모노레포다.
Spring 서버를 기준으로 보면, 도메인을 모듈로 나누는 것과 MSA는 같은 말이 아니다.
- 모듈만 나누고 실행 앱이 하나 == 모듈형 모놀리스
- 각 도메인이 독립 실행 가능한 Spring Boot 앱이 되고, 따로 이미지화+배포 == MSA
project/
settings.gradle
build.gradle # 공통 플러그인/버전
common/ # 라이브러리 모듈
order-api/ # 독립 Spring Boot 서버
payment-api/
user-api/
...
각 도메인 서버를 만들고 최상위 Gradle에 include하는 방식이다. root는 실행하지 않는 aggregator 역할만 하고, 실제 배포 단위는 각 *-api다.
이 구조에서 지킬 원칙은 네 가지 정도로 정리할 수 있다.
- 도메인 서버끼리는 구현체를
implementation으로 직접 의존하지 않는다 - 통신은 HTTP/gRPC/메시지처럼 네트워크 경계로 한다
common에는 로깅, 공통 응답, 유틸처럼 진짜 공통만 둔다- DB와 트랜잭션 경계도 가능하면 서비스 단위로 분리한다
특히 common은 편해서 커지기 쉽다. 공통처럼 보이는 도메인 로직이 들어가면 레포는 하나인데 사실상 모놀리스처럼 결합이 된다. 그래서 common은 최소화하고 애매하면 해당 도메인 모듈에 남기는 쪽을 우선하는게 좋다.
한 레포여도 도메인만 배포할 수 있다
모노레포를 보고 걱정했던 부분이 “그러면 배포도 한꺼번에 해야 하나?”였다.
찾아보니 GitHub Actions라면 도메인별 워크플로우를 만들고 paths로 자기 모듈만 감시해 따로 배포를 진행할 수 있었다. 레포는 공유하고, 배포는 도메인 단위로 나눌 수 있는 것이다.
.github/workflows/
deploy-order-api.yml
deploy-payment-api.yml
deploy-user-api.yml
예를 들어 order-api 워크플로는 이런 식이다.
on:
push:
branches: [main]
paths:
- 'order-api/**'
- 'common/**'
- '.github/workflows/deploy-order-api.yml'
이렇게 하면 order-api만 바뀌었을 때 해당 서버만 빌드하고 이미지만 올려 배포할 수 있다. payment-api만 바뀌면 payment만 배포된다. 둘 다 바뀌면 각 워크플로가 따로 돈다.
다만 common이 바뀌면 영향받는 서비스도 같이 빌드하고 배포해야 한다. 멀티모듈 모노레포에서 독립 배포를 유지하려면, 파일을 고쳤을 때 어떤 서버를 다시 배포할지를 처음부터 문서화해 두는 편이 안전하다.
배포 흐름 자체는 단순하다.
- 변경된 모듈 감지
- 해당 모듈만 빌드
- 서비스별 Docker 이미지 푸시
- Kubernetes나 배포 대상의 해당 Deployment만 갱신
즉, MSA의 독립적인 배포는 CI/CD 설계로 지키는 것이다.
선택에 대한 근거
이번 선택의 근거를 한곳에 모아보면 다음과 같다.
| 고민 | 답 |
|---|---|
| 레포를 도메인마다 나눌까? | 팀 규모와 일정에는 관리 비용이 더 크다 |
| 모듈만 나누면 MSA인가? | 독립 실행/배포 단위가 있어야 MSA다 |
| 한 레포면 같이 배포되나? | Actions paths로 도메인별 배포가 가능하다 |
이번 프로젝트에서 경험하려는 것은 “MSA처럼 보이는 폴더 구조”가 아니다. 서비스 경계를 나누고 그 경계가 코드, 데이터, 배포에 어떻게 남는지를 이해하는 일이다.
나중에 특정 도메인만 배포 주기, 스케일, 팀 책임이 완전히 달라지면 그때 레포 분리를 고민해보면 된다. 반대로 말하면 지금은 레포를 먼저 쪼개지 않아도 MSA로 갈 수 있다는 것이다.
마치며
팀원분이 설명해 주신 멀티모듈 모노레포를 듣고 찾아보며 배운 것은, MSA를 한다고 해서 처음부터 레포를 도메인 수만큼 만들 필요는 없다는 것이다. 오히려 작은 팀일수록 한 레포에서 도메인 서버를 모듈로 키우고, 변경된 서버만 배포하는 편이 학습과 협업 모두에 유리하다.