저장소 관리방안
1. 기본 원칙
-
소프트웨어의 모든 구성요소가 같은 속도로 변경되거나, 같은 장애 위험과 리소스 특성을 갖는 것은 아니다.
-
따라서 저장소 구조와 배포 단위는 특정 아키텍처를 먼저 선택한 뒤 맞추는 것이 아니라, 각 구성요소의 변경·운영 특성을 확인한 뒤 결정해야 한다.
-
또한 저장소 단위와 배포 단위는 서로 다른 개념이다.
-
하나의 모노레포에서 여러 서비스를 독립 배포할 수 있고, 여러 저장소로 나뉘어 있어도 항상 함께 배포된다면 실질적으로는 하나의 시스템과 다르지 않다. 저장소를 나누는 것만으로 서비스의 독립성이 생기지는 않는다.
-
분리 여부를 판단할 때는 다음 세 가지를 우선 확인한다.
| 판단 기준 | 확인 질문 | 분리를 검토할 신호 |
|---|
| 변경 빈도 | 구성요소별 변경·배포 주기가 얼마나 다른가 | 한쪽의 잦은 변경 때문에 안정적인 구성요소까지 반복해서 빌드·배포해야 함 |
| 장애 반경 | 한 구성요소의 실패가 어디까지 영향을 미쳐야 하는가 | 장애를 격리해야 하는 기능이 같은 프로세스나 배포 단위에 묶여 있음 |
| 리소스 프로파일 | CPU, 메모리, I/O, GPU 사용 특성이 어떻게 다른가 | 일부 워크로드 때문에 전체 시스템의 인스턴스 유형이나 확장 정책이 결정됨 |
여기에 데이터 일관성 요구와 운영 역량을 함께 고려해야 한다. 분리된 배포 단위마다 빌드 파이프라인, 시크릿, 모니터링, 알림, 런북, 장애 대응 책임이 추가된다. 기술적으로 분리할 수 있더라도 팀이 이 운영 비용을 감당하지 못하면 적절한 선택이 아니다.
2. Monolith
정의
Monolith는 주요 구성요소를 하나의 애플리케이션으로 빌드하고 배포하는 구조다. 하나의 저장소와 데이터베이스를 사용하는 경우가 많지만, 핵심은 배포 단위가 하나라는 점이다.
- 구성요소의 변경 주기가 비슷하고, 강한 트랜잭션 일관성이 중요하며, 운영 인력이 제한적인 경우에 적합하다.
- 구조가 단순하다는 장점은 있지만, 내부 경계를 관리하지 않으면 코드 결합도가 빠르게 높아질 수 있다.
저장소 및 코드 관리
- 기본적으로 단일 저장소와 trunk-based development를 사용하고, 기능 브랜치는 짧게 유지한다.
- 디렉터리나 모듈별 책임자를
CODEOWNERS로 지정한다.
- 의존성 방향과 순환 의존을 CI에서 검사한다. TypeScript의
dependency-cruiser·eslint-plugin-boundaries, Java의 ArchUnit, Go의 internal 패키지처럼 언어와 도구에 맞는 강제 수단을 사용한다.
common, shared, utils, core 같은 공용 영역에는 범용 코드만 둔다. 도메인 규칙이 공용 모듈로 이동하면 모듈 간 결합을 추적하기 어려워진다.
- 테이블 또는 데이터 집합의 쓰기 책임을 특정 모듈에 귀속한다. 여러 모듈이 같은 데이터를 임의로 수정하지 않도록 한다.
- 전체 경로를 한 프로세스에서 검증하기 쉬운 장점을 활용해 모듈 통합 테스트와 핵심 사용자 흐름의 종단 간 테스트를 운영한다.
- 빌드와 테스트 시간이 개발 흐름을 방해하기 시작하면 증분 빌드와 캐시를 도입한다. Nx, Turborepo, Bazel 등은 선택지이며, 도구 자체보다 변경된 영역만 검증할 수 있는지가 중요하다.
다음과 같은 현상은 내부 경계가 약해졌다는 신호다.
- 공용 모듈이 대부분의 도메인 모듈에 의존하거나, 반대로 대부분의 모듈이 공용 모듈의 내부 구현에 의존함
- 둘 이상의 모듈이 같은 테이블에 직접 쓰기 작업을 수행함
- 특정 함수나 데이터 변경의 영향 범위를 정적 분석이나 검색으로 확인하기 어려움
- 작은 변경도 전체 회귀 테스트와 수동 확인을 요구함
배포 및 데이터 관리
- 하나의 산출물을 함께 배포하므로 릴리스 절차와 운영 구성이 단순하다. 코드 롤백도 비교적 일관된 단위로 수행할 수 있다.
- 다만 데이터베이스 스키마와 이미 저장된 상태는 이미지 태그만 되돌린다고 복구되지 않으므로, 롤백 가능성은 코드와 데이터 변경을 함께 설계해야 한다.
스키마 변경은 다음과 같은 expand-contract 절차를 기본으로 삼는 편이 안전하다.
- 하위 호환되는 필드나 테이블을 추가한다.
- 필요한 경우 구버전과 신버전 형식에 함께 기록한다.
- 기존 데이터를 백필한다.
- 읽기 경로를 새 구조로 전환한다.
- 충분한 검증과 유예 기간 뒤 기존 구조를 제거한다.
카나리 배포와 피처 플래그도 사용할 수 있지만, 배포되는 산출물 자체는 전체 애플리케이션이다. 따라서 기능 수준의 노출 제어와 배포 단위의 장애 격리를 혼동해서는 안 된다.
AI 에이전트 활용
- 하나의 저장소 안에서 타입과 호출 관계를 추적할 수 있으므로, 정적 분석이 잘 구성된 코드베이스에서는 에이전트가 변경 영향 범위를 파악하기 쉽다.
- 반면 코드베이스가 커질수록 전체 맥락을 한 번에 제공하기 어려워지고, 내부 규칙이 문서화되지 않았다면 기존 구현을 중복하거나 잘못된 위치에 코드를 추가할 가능성이 높아진다.
따라서 다음을 함께 운영한다.
- 모듈 경계를 린트와 빌드 규칙으로 강제한다.
- 각 모듈 루트에 책임, 소유 데이터, 허용된 의존 대상을 짧게 기록한다.
- 에이전트가 수정할 범위를 디렉터리나 모듈 단위로 제한한다.
- 변경 영역에 대응하는 테스트를 자동 실행하고, 위험도가 높은 영역은 사람의 승인을 요구한다.
3. Modular Monolith
정의
Modular Monolith는 배포 단위는 하나로 유지하되, 모듈 경계를 빌드와 테스트 단계에서 명시적으로 강제하는 구조다.
런타임에서 프로세스나 커넥션 풀이 분리되어 있는지가 본질은 아니다. 다른 모듈의 내부 구현에 직접 접근하지 못하도록 코드와 데이터 경계를 유지하는 것이 핵심이다.
- 독립 배포가 아직 필요하지 않지만, 도메인별 책임과 변경 영향 범위를 분명히 관리해야 하는 경우에 적합하다.
저장소 및 코드 관리
- 일반적으로 모노레포를 사용하고, 모듈별 소유자를
CODEOWNERS에 지정한다.
- 모듈은 공개 API와 내부 구현을 구분한다. 다른 모듈은 공개 API를 통해서만 접근한다.
- 언어의 가시성 기능, 패키지 구조, 프로젝트 참조, 의존성 린트를 조합해 경계를 강제한다.
- 순환 의존은 CI 실패로 처리하고, 의존성 그래프를 정기적으로 확인한다.
- 각 모듈에 책임 범위, 소유 데이터, 외부에 제공하는 계약, 의존 가능한 모듈을 기록한다.
- 변경 영향 기반 빌드와 테스트를 적용해 모듈 수가 늘어나더라도 CI 시간이 전체 코드 크기에 비례해 증가하지 않도록 한다.
모듈 간 통신 방식은 요구사항에 따라 선택한다.
- 즉시 응답과 단일 트랜잭션이 필요하면 공개 인터페이스를 통한 동기 호출을 사용한다.
- 비동기 처리, 여러 후속 작업, 결합도 완화가 필요한 경우에는 인프로세스 이벤트를 사용할 수 있다.
향후 서비스 분리 가능성만을 이유로 모든 통신을 이벤트로 바꾸는 것은 오히려 흐름 추적과 오류 처리를 복잡하게 만들 수 있다. 현재 필요한 일관성, 지연 시간, 실패 처리 방식에 맞춰 선택해야 한다.
데이터 관리
하나의 데이터베이스를 사용하더라도 데이터의 소유권은 모듈별로 구분한다. 모듈별 스키마를 사용하는 방법은 경계를 명확히 하는 데 도움이 되지만 필수 조건은 아니다.
- 다른 모듈이 소유한 테이블에 직접 쓰지 않는다.
- 모듈 간 조회는 공개 쿼리 인터페이스나 명시적으로 승인된 읽기 모델을 사용한다.
- 스키마를 넘는 조인과 외래 키는 추후 분리 비용을 높이므로, 장기적으로 독립시킬 가능성이 높은 경계에서는 신중하게 사용한다.
- 모듈 간 데이터 변경이 필요하면 호출 계약과 트랜잭션 범위를 문서화한다.
배포 수명 주기
산출물과 배포 단위는 하나이므로 릴리스와 롤백 절차는 Monolith와 유사하다. 다만 경계가 유지되면 변경 영향 범위와 테스트 범위를 모듈 단위로 좁힐 수 있다.
모듈을 나중에 서비스로 분리할 수 있는 가능성도 높아지지만, 분리가 자동으로 쉬워지는 것은 아니다. 데이터 소유권, 트랜잭션 범위, 동기 호출 관계, 운영 책임이 얽혀 있다면 별도의 재설계가 필요하다. Modular Monolith의 가치는 MSA 전환을 보장하는 데 있지 않고, 현재 구조의 결합도를 확인하고 관리할 수 있게 만드는 데 있다.
AI 에이전트 활용
-
모듈 경계는 에이전트의 작업 범위와 자연스럽게 맞출 수 있다. 에이전트에게 특정 모듈과 그 모듈이 의존하는 공개 API만 제공하고, 경계 위반은 빌드 단계에서 차단할 수 있다.
-
주의할 점은 새 코드의 배치 다.
-
모듈의 책임과 분리 기준이 명확하지 않으면 에이전트가 기능을 기술 계층이나 파일 이름만 보고 잘못된 모듈에 배치할 수 있다.
-
이를 방지하려면 모듈별 책임 정의, 예시 경로, 금지된 의존 관계를 간결하게 유지하고 , 변경 영향 기반 테스트를 자동화해야 한다.
4. MSA
정의
- MSA는 시스템을 여러 서비스로 나누고 각 서비스를 독립적으로 배포·운영하는 구조다.
- 저장소를 몇 개 사용하는지는 구현 선택이며 MSA의 정의가 아니다.
- 하나의 모노레포에서 여러 서비스를 독립 배포할 수도 있고, 여러 저장소를 사용하면서도 항상 함께 배포한다면 서비스 독립성이 확보되었다고 보기 어렵다. (일반적으로는 여러 레포로 나누는게 쉽다)
서비스 경계는 다음 조건을 중심으로 평가한다.
- 다른 서비스의 동시 배포 없이 독립적으로 릴리스할 수 있는가
- 자신이 소유한 데이터에 대한 변경 권한이 분명한가
- 장애와 확장 정책을 다른 서비스와 구분해 운영할 수 있는가
- 팀 또는 담당자가 서비스의 개발과 운영 책임을 실질적으로 보유하는가
서비스 A를 배포할 때마다 서비스 B를 정해진 순서로 함께 배포해야 하거나, A가 B의 데이터베이스를 직접 수정한다면 저장소 수와 관계없이 독립성은 낮다.
저장소 및 코드 관리
모노레포와 폴리레포 중 어느 하나가 항상 우월한 것은 아니다.
모노레포가 적합한 경우
- 팀 규모가 작고 공통 도구와 CI 정책을 중앙에서 관리함
- 여러 서비스에 걸친 변경이 자주 발생함
- 통합 검색, 원자적 변경, 일관된 개발 환경이 중요함
- 서비스별 빌드·테스트·배포 경계를 저장소 내부 도구로 확실히 분리할 수 있음
폴리레포가 적합한 경우
- 서비스별 팀과 릴리스 주기가 실제로 독립적임
- 접근 권한, 보안, 규제 또는 외부 협업 경계를 저장소 단위로 나눠야 함
- 각 팀이 언어, 도구, 버전 정책을 독립적으로 운영할 필요가 있음
- 공통 정책을 템플릿과 자동화로 배포할 운영 역량이 있음
어느 방식을 선택하더라도 다음 원칙을 유지한다.
- 서비스별 빌드, 테스트, 배포 파이프라인을 독립적으로 실행할 수 있어야 한다.
- API와 이벤트 계약은 OpenAPI, Protocol Buffers, AsyncAPI, Avro 등 기계가 읽을 수 있는 형식으로 관리한다.
- 계약 변경에는 하위 호환성 검사와 컨슈머 계약 테스트를 적용한다.
- 공통 라이브러리는 로깅, 인증 클라이언트, 계약에서 생성한 코드처럼 안정적인 횡단 관심사에 제한한다.
- 여러 서비스의 도메인 규칙을 하나의 공유 라이브러리에 묶지 않는다. 공통 라이브러리 변경이 다수 서비스의 동시 업그레이드와 배포를 요구하면 독립 배포가 약해진다.
- 브레이킹 체인지는 즉시 교체하지 않고 추가, 병행 운영, 사용 중단 공지, 제거 순서로 진행한다.
데이터 및 통신 관리
- 각 서비스는 자신의 데이터에 대한 쓰기 권한을 보유한다. 다른 서비스가 데이터베이스에 직접 쓰지 않는다.
- 다른 서비스의 데이터가 필요하면 API, 이벤트, 읽기 모델, 데이터 복제 중 요구사항에 맞는 방식을 사용한다.
- 여러 서비스에 걸친 상태 변경은 가능한 한 단일 서비스의 책임으로 재설계한다. 불가피한 경우에는 사가, 아웃박스, 멱등 처리, 재시도와 보상 작업을 조합한다.
- 동기 호출 체인이 길어질수록 지연 시간과 장애 전파 가능성이 커진다. 타임아웃, 제한된 재시도, 회로 차단, 부하 격리를 적용하고, 즉시 응답이 필요하지 않은 작업은 비동기 처리를 검토한다.
- 로그, 메트릭, 분산 추적, 상관관계 ID를 서비스 전반에서 일관되게 수집한다. 서비스 간 요청과 이벤트 흐름을 운영자가 추적할 수 있어야 한다.
배포 수명 주기
MSA의 주요 이점은 서비스별로 배포, 확장, 장애 격리 정책을 달리할 수 있다는 점이다. 대신 코드와 데이터의 호환성을 여러 버전에 걸쳐 유지해야 한다.
- 이전 버전과 새 버전이 일정 기간 공존할 수 있도록 API와 이벤트를 하위 호환되게 변경한다.
- 이미 저장되었거나 발행된 데이터는 단순한 코드 롤백으로 되돌아가지 않으므로, 상태 변경에는 복구·보상·순방향 수정 절차를 마련한다.
- 데이터 마이그레이션은 확장 후 전환하고 마지막에 제거하는 순서로 수행한다.
- 서비스별 파이프라인, 시크릿, 대시보드, 알림, 런북, 의존성 업데이트 비용을 운영 지표로 관리한다.
독립 배포, 독립 확장, 장애 격리, 팀 자율성 중 실제로 필요한 이점이 운영 비용보다 클 때 MSA를 선택해야 한다. 저장소를 나누거나 기술 스택을 다양화하는 것만으로는 충분한 도입 근거가 되지 않는다.
AI 에이전트 활용
서비스 단위가 작고 책임이 명확하면 에이전트가 로컬 코드와 테스트를 이해하기 쉽다. 반면 여러 서비스에 걸친 변경은 하나의 저장소 안에서 정적 분석하는 것보다 영향 범위를 파악하기 어렵다.
이를 보완하려면 다음이 필요하다.
- 기계가 읽을 수 있는 API·이벤트 계약
- 서비스 카탈로그와 소유권 정보
- 계약 테스트와 통합 테스트
- 서비스 간 의존성 그래프
- 여러 저장소 또는 서비스 변경 순서를 관리하는 명시적 작업 계획
에이전트에는 서비스 단위 작업을 우선 맡기고, 크로스 서비스 변경은 계약 변경과 배포 순서를 별도로 검토하는 편이 안전하다.
5. 선택 기준
Monolith가 적합한 경우
- 팀 규모와 운영 역량이 제한적임
- 구성요소의 변경 주기와 확장 방식이 대체로 비슷함
- 하나의 트랜잭션으로 처리해야 하는 업무가 많음
- 독립 배포보다 단순한 릴리스와 장애 대응이 중요함
Modular Monolith가 적합한 경우
- 배포 단위는 하나로 유지해도 되지만 코드와 데이터의 책임을 분리해야 함
- 도메인별 변경 영향 범위를 줄이고 싶음
- 향후 일부 영역을 분리할 가능성은 있으나 현재 MSA 운영 비용을 감당할 이유가 부족함
- 모듈 경계를 빌드, 테스트, 코드 리뷰에서 강제할 수 있음
MSA가 적합한 경우
- 특정 서비스의 배포 빈도, 장애 허용 수준, 확장 방식이 다른 영역과 명확히 다름
- 서비스별 팀과 운영 책임이 실제로 분리되어 있음
- 데이터 소유권과 계약을 독립적으로 관리할 수 있음
- 분산 시스템의 관측 가능성, 호환성 관리, 장애 대응 비용을 감당할 수 있음
명확한 독립 배포 요구가 확인되지 않았다면, 단일 배포 단위를 유지하면서 모듈 경계를 강제하는 Modular Monolith가 보수적인 출발점이 될 수 있다. 이후 변경 빈도, 장애 반경, 리소스 프로파일 또는 팀 소유권의 차이가 실제 운영 지표로 확인되면 해당 모듈을 서비스로 분리한다.
반대로 이미 분리된 서비스라도 독립 배포와 데이터 소유권이 성립하지 않고 운영 비용만 증가한다면 통합을 검토할 수 있다. 아키텍처의 방향을 미리 고정하기보다, 현재의 결합도와 운영 비용을 측정해 조정하는 것이 중요하다.
6. 비교 요약
| 항목 | Monolith | Modular Monolith | MSA |
|---|
| 배포 단위 | 하나 | 하나 | 서비스별 복수 |
| 일반적인 저장소 구성 | 주로 모노레포 | 주로 모노레포 | 모노레포 또는 폴리레포 |
| 경계 강제 방식 | 디렉터리 규칙, 린트, 테스트 | 공개 API, 빌드 경계, 데이터 소유권 | 네트워크 계약, 데이터 소유권, 독립 파이프라인 |
| 주요 장점 | 단순한 운영과 트랜잭션 | 단일 배포의 단순함과 명확한 모듈 경계 | 독립 배포, 확장, 장애 격리, 팀 자율성 |
| 주요 비용 | 내부 결합도 증가 가능성 | 경계 설계와 지속적인 규칙 관리 | 분산 통신, 호환성, 관측 가능성, 운영 고정비 |
| AI 에이전트의 장점 | 저장소 안에서 정적 관계를 넓게 추적 가능 | 모듈을 작업 컨텍스트로 사용 가능 | 서비스 단위의 로컬 맥락이 비교적 명확함 |
| AI 에이전트의 위험 | 코드베이스가 크면 전체 맥락 제공이 어려움 | 새 코드가 잘못된 모듈에 배치될 수 있음 | 크로스 서비스 변경의 영향 추적이 어려움 |
| 필요한 투자 | 경계 린트, 로컬 문서, 통합 테스트 | 모듈 책임 명세, affected 빌드·테스트 | 기계 판독 계약, 서비스 카탈로그, 계약·통합 테스트 |
세 구조 모두에서 중요한 것은 문서의 분량이 아니라 경계를 위반했을 때 자동으로 검출되는 체계다. 저장소 구조, 빌드 규칙, 테스트, 데이터 소유권, 배포 정책이 같은 경계를 가리키도록 유지해야 한다.