사내 모노레포 도입을 고민 중이다

dasd412·2025년 12월 13일

실무 문제 해결

목록 보기
21/27

상황

두 개의 마이크로서비스, 하나의 DB, 끝없는 동기화 지옥

회사에서 FastAPI 기반으로 MSA를 구축하고 있다. 현재 service-backendservice-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 한 곳에서만 수정하면 된다.

결과 > MSA의 장점과 모노레포의 장점을 모두

MSA의 장점 유지

  • 각 서비스는 독립적으로 배포 가능
  • 다른 포트로 실행 (backend: 8000, sms: 8001)
  • 필요시 다른 기술 스택 사용 가능
  • API Gateway를 통한 라우팅

모노레포의 장점 추가

  • 모델 변경 시 한 번에 모든 서비스에 반영
  • 통합된 CI/CD 파이프라인 구축 가능
  • 의존성 관리 단순화
  • 리팩토링이나 전체적인 변경사항 추적이 쉬움

배포 예시

# 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-backendservice-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 모델을 수정하려면 다음 절차를 거쳐야한다.

  1. service-common 리포지토리에서 수정
  2. setup.py 버전 업 (1.2.3 → 1.2.4)
  3. 패키지 빌드 (python setup.py bdist_wheel)
  4. PyPI나 사내 패키지 저장소에 업로드
  5. service-backend에서 requirements.txt 업데이트
  6. service-sms에서도 requirements.txt 업데이트
  7. 각각 재배포

의존성 관리도 까다롭다.

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. 각각 테스트/배포

profile
아키텍쳐 설계에 관심이 많은 백엔드 개발자입니다.

0개의 댓글