회사에서 FastAPI 기반으로 MSA를 구축하고 있다. 현재 service-backend와 service-sms라는 두 개의 리포지토리가 있고, 둘 다 같은 데이터베이스의 machine 테이블을 사용한다.
문제는 여기서 시작됐다.
project-backend/ (리포지토리 1)
└── models/
└── machine.py
project-sms/ (리포지토리 2)
└── models/
└── machine.py
두 리포지토리 모두 동일한 Machine 모델을 가지고 있었다:
class Machine(Base, table=True):
__tablename__ = "machines"
email_enabled: bool = Field(default=False, description="이메일 알림 활성화 여부")
어느 날 service-backend에서 email_enabled 필드가 필요해서 추가했다. 당연히 service-sms에는 없으니까 또 추가해서 push했다.
그런데 오늘은 service-sms에 다른 enabled 필드들을 4개 더 넣었다:
class Machine(Base, table=True):
__tablename__ = "machines"
email_enabled: bool = Field(default=False, description="이메일 알림 활성화 여부")
data_alarm_enabled: bool = Field(default=True)
status_alarm_enabled: bool = Field(default=True)
similarity_alarm_enabled: bool = Field(default=True)
parts_alarm_enabled: bool = Field(default=True)
이번엔 service-backend에 없어서 애플리케이션이 깨져버렸다.
이런 식으로 계속 왔다 갔다 하면서 동기화하는 게 너무 짜증났다.
처음 생각한 건 공통 라이브러리를 만드는 것이었다.
project-common/ (별도 리포지토리)
├── models/machine.py
└── setup.py
project-backend/ (별도 리포지토리)
project-sms/ (별도 리포지토리)
하지만 스타트업에서 이렇게 할 경우,
득보다 실이 많은 방법이다. 스타트업은 속도가 생명이다.
결론적으로 모노레포가 가장 현실적인 해결책이었다.
그리고 복잡한 공통 라이브러리나 Git Submodule 같은 방법들은 오히려 오버엔지니어링이 될 수 있다.
모노레포 + MSA 조합으로 코드는 한 곳에서 관리하되, 런타임에는 독립적인 서비스로 동작하게 만드는 것이 제일 낫다고 판단했다.
많은 사람들이 착각하는 부분인데, 모노레포와 모놀리식은 완전히 다른 개념이다.
platform/ # 하나의 리포지토리
├── services/ # 각각은 독립적인 마이크로서비스
│ ├── backend-service/ # 독립 배포 가능
│ │ ├── Dockerfile
│ │ ├── main.py
│ │ └── requirements.txt
│ └── sms-service/ # 독립 배포 가능
│ │ ├── Dockerfile
│ │ ├── main.py
│ │ └── requirements.txt
├── shared/ # 공통 코드
│ ├── models/ # DB 모델 (핵심!)
│ │ └── machine.py
│ ├── auth/ # 인증 로직
│ └── utils/ # 공통 유틸리티
├── migrations/ # DB 스키마 관리
└── docker-compose.yml # 로컬 개발용
이제 각 서비스에서 공통 모델을 import해서 사용한다.
# services/backend-service/main.py
from shared.models.machine import Machine
# services/sms-service/main.py
from shared.models.machine import Machine
모델을 변경해야 할 때는 shared/models/machine.py 한 곳에서만 수정하면 된다.
# docker-compose.yml
version: '3.8'
services:
backend:
build: ./services/backend-service
ports:
- "8000:8000"
environment:
- DATABASE_URL=${DATABASE_URL}
sms:
build: ./services/sms-service
ports:
- "8001:8001"
environment:
- DATABASE_URL=${DATABASE_URL}
현재 사내에서 쓰이는 두 서비스service-backend와 service-sms라는 두 개의 리포지토리는 향후 플랫폼화가 진행되면서 재구축할 예정이다. 지금 두 서비스는 너무 specific한 기능이 많아서 general하게 바꿀 필요가 있어서다.
그래서 기존처럼 기능은 작동하되,빠르게 변화하는 스타트업 환경에 맞게 재구축할 필요가 있다. 기능의 추상화도 당연히 필요하고... 구조도 안정적이면서 빨리 빨리 바뀔 수 있어야 하고 ㅎㅎ
그래서 현재 구조의 짜증남을 미리 미리 기록하고 있다 ^^;; 그래야 개선을 하지...
먼저, 핵심 차이는 배포 단위다.
모노레포의 경우, 다음과 같다.
platform/ (하나의 git 리포지토리)
├── shared/models/machine.py ← 이건 그냥 폴더
├── services/backend/
└── services/sms/
shared/models/machine.py 수정하면 그냥 git commit/push 끝import그리고 모노레포는 모든 의존성을 하나에서 관리하기 때문에 편리하다.
platform/
├── requirements.txt ← 모든 의존성을 여기서 통합 관리
├── shared/
├── services/backend/
└── services/sms/
반면, 공통 라이브러리는 다음과 같다.
service-common/ (별도 리포지토리) ← 독립적인 패키지
├── models/machine.py
└── setup.py
service-backend/ (별도 리포지토리)
└── requirements.txt: service-common==1.2.3
service-sms/ (별도 리포지토리)
└── requirements.txt: service-common==1.2.3
Machine 모델을 수정하려면 다음 절차를 거쳐야한다.
의존성 관리도 까다롭다.
service-common v1.2.3 → FastAPI 0.95.0 의존
service-backend → FastAPI 0.100.0 사용 중
service-sms → FastAPI 0.98.0 사용 중
service-common 업데이트하면
→ FastAPI 버전 충돌 발생 가능
# 새 필드 추가 작업
1. shared/models/machine.py 수정
2. git add . && git commit -m "Add alarm fields"
3. 끝! 모든 서비스가 자동으로 새 모델 사용
# 새 필드 추가 작업
1. service-common 리포지토리로 이동
2. models/machine.py 수정
3. setup.py 버전 수정 (1.2.3 → 1.2.4)
4. python setup.py bdist_wheel
5. twine upload dist/* (패키지 업로드)
6. service-backend 리포지토리로 이동
7. requirements.txt 수정 (service-common==1.2.4)
8. pip install -U service-common
9. service-sms 리포지토리로 이동
10. requirements.txt 수정 (service-common==1.2.4)
11. pip install -U service-common
12. 각각 테스트/배포