한 줄 정의
인프라 코드베이스의 구조는 코드의 기술적 분류가 아니라 팀 소유권과 딜리버리 흐름을 따라야 합니다.
쉽게 말하면
이사할 때 짐을 종류별로 싸면 정리한 순간은 깔끔합니다. 나사는 나사 상자에, 전선은 전선 상자에. 그런데 막상 새 집에서 책상 하나를 조립하려면 상자 다섯 개를 열어야 합니다.
반대로 쓰는 단위로 싸면 책상 상자 하나에 상판과 나사와 설명서가 다 들어 있습니다. 여는 순간 그 작업이 끝납니다. 코드도 같습니다. “모든 방화벽 규칙”을 한 파일에 모으면 정리된 것처럼 보이지만, 데이터베이스 하나를 바꾸려면 파일 다섯 개를 열어야 합니다.
그리고 이 상자들을 트럭 몇 대에 나눠 실을지는 짐의 성격이 아니라 누가 어느 집으로 가느냐 로 정해집니다. 저장소를 나누는 기준도 마찬가지입니다. 모노리포냐 마이크로리포냐는 이념 문제가 아니라 팀 소유권과 배송 경로의 결과입니다.
왜 중요한가?
인프라 코드베이스에는 스택 정의, 서버 구성, 모듈, 라이브러리, 테스트, 구성값, 유틸리티 스크립트가 뒤섞여 있습니다. 여기서 답해야 할 질문은 네 가지입니다.
- 프로젝트 안에서 이 코드들을 어떻게 배치할까?
- 저장소에 프로젝트를 어떻게 나눠 담을까?
- 인프라와 애플리케이션 코드는 함께 둘까, 분리할까?
- 여러 부분으로 이뤄진 시스템의 코드는 어떻게 나눌까?
여기서 프로젝트 는 시스템의 개별 컴포넌트를 빌드하는 데 쓰이는 코드 모음입니다. 얼마나 커야 하는지에 대한 엄격한 규칙은 없습니다.
이 질문들이 취향 문제가 아닌 이유는 콘웨이의 법칙 때문입니다. 조직 구조와 그 조직이 만드는 시스템 사이에는 직접적인 관계가 있어서, 팀 구조·시스템 소유권·코드 구조가 서로 어긋나면 그 지점마다 마찰이 생기고 효율이 떨어집니다. 코드 구조는 아키텍처를 반영하는 게 아니라 아키텍처를 강제합니다.
핵심 내용
저장소를 어떻게 나눌 것인가
먼저 상충관계를 봅니다.
| 저장소를 나누면 | 저장소를 합치면 |
|---|---|
| 코드 레벨에서 경계를 유지하기 쉬움 | 여러 팀 작업이 한곳에 섞여 오버헤드·충돌 발생 |
| 저장소에 걸친 변경 작업이 복잡해짐 | 코드가 함께 버저닝·브랜치되어 통합·딜리버리 전략이 단순해짐 |
모든 것을 위한 단일 저장소 (모노리포)
대규모 조직에서도 전체 코드를 한 저장소에 두는 경우가 있습니다. 필요한 프로젝트를 전부 확인할 수 있고 모든 프로젝트의 버전이 일관된다는 점이 보장되기 때문입니다.
대신 소스 제어 시스템이 규모를 감당해야 합니다. 크기·기록·사용자 수·활동량이 늘수록 도구가 버티지 못하므로, 저장소 분할은 성능 관리의 문제 이기도 합니다. 필요한 하위 집합만 받는 스파스 체크아웃으로 완화할 수 있습니다.
여기서 흔한 오해가 하나 있습니다.
단일 저장소 ≠ 단일 빌드
모노리포 전략은 정확히 말하면 “단일 저장소에서 유지보수되는 프로젝트에 빌드 타임 통합 패턴을 쓰는 것”입니다. 저장소를 합쳤다고 해서 모든 프로젝트를 한 번에 빌드할 필요는 없습니다. 시스템의 서로 다른 하위 집합을 빌드하는 여러 빌드를 두고, 그중 일부가 공유 라이브러리 프로젝트를 함께 쓰는 구성이 일반적입니다. 함께 빌드하더라도 애플리케이션 패키지, 인프라 스택, 서버 이미지처럼 여러 아티팩트가 나올 수 있습니다.
모노리포의 진짜 위험은 성능이 아니라 프로젝트 경계가 흐려지는 것 입니다. 다른 프로젝트의 파일을 직접 참조하면서 코드를 쓰면 결합도가 올라가고 의존성이 눈에 보이지 않게 됩니다. 한 프로젝트의 파일을 바꿨을 때 다른 프로젝트에서 예기치 않은 충돌이 나기 시작하면 시간이 갈수록 엉킵니다.
각 프로젝트를 위한 개별 저장소 (마이크로리포)
프로젝트마다 저장소를 하나씩 두는 반대편 극단입니다. 프로젝트 간 분리가 물리적으로 보장되므로, 통합 전에 각 프로젝트를 별도로 빌드·테스트하는 파이프라인이 있을 때 잘 맞습니다. 누군가 두 프로젝트를 체크아웃해서 양쪽 파일을 함께 바꾸면 파이프라인이 실패하면서 문제가 곧바로 드러납니다.
문제는 빌드 타임 통합과 궁합이 나쁘다는 점입니다. 저장소를 전부 체크아웃해서 한 번에 빌드할 수는 있지만, 코드가 함께 버저닝되지 않으므로 일관된 빌드를 만들려면 어떤 저장소의 어떤 버전을 체크아웃할지 판별하는 방법을 따로 만들어야 합니다.
마이크로리포는 딜리버리 타임·어플라이 타임 통합 과 짝일 때 가장 잘 작동합니다. 저장소 하나가 바뀌면 그 프로젝트의 딜리버리 프로세스가 트리거되고, 다른 프로젝트와는 작업 흐름 뒤쪽에서 만납니다.
여러 프로젝트를 위한 멀티 저장소
현실의 대부분입니다. 그룹화는 모노리포·마이크로리포 같은 전략으로 결정되기보다 유기적으로 만들어집니다. 다만 어디에 선을 그을지는 세 가지가 좌우합니다.
| 기준 | 판단 |
|---|---|
| 통합 시점 | 빌드 타임에 통합해야 할 만큼 밀접하면 같은 저장소, 딜리버리 경로가 분리돼 있으면 다른 저장소 |
| 팀 소유권 | 여러 팀이 한 저장소의 서로 다른 프로젝트를 건드리면 커밋 기록이 뒤섞여 지저분해짐. 접근 제어도 보통 저장소 단위 |
| 아키텍처 경계 | 같은 저장소 안의 프로젝트는 파일 의존성으로 쉽게 얽힘. 강한 경계가 필요한 곳은 저장소로 끊음 |
프로젝트 안에는 무엇을 함께 두는가
지원 파일은 프로젝트 옆에
스택 프로젝트의 일반적인 레이아웃입니다.
├── build.sh # 빌드 작업 구현 스크립트
├── src/ # 인프라 스택 코드, 프로젝트의 핵심
├── test/ # 테스트 코드 (오프라인·온라인 등 단계별 하위 폴더)
├── environments/ # 스택 인스턴스별 구성값 파일
└── pipeline/ # 딜리버리 파이프라인 도구 구성
기준은 단순합니다. 프로젝트와 분명한 관계가 있는 파일은 프로젝트와 함께 둡니다. 그래야 누군가 특정 버전을 체크아웃했을 때 인프라 코드·테스트·딜리버리 구성이 전부 같은 버전이라는 것이 보장됩니다. 테스트가 별도 프로젝트에 있으면 코드와 테스트의 버전이 어긋난 채 실행되기 쉽습니다.
크로스 프로젝트 테스트는 소비자 쪽에
프로그레시브 테스트에서는 각 프로젝트를 개별 테스트한 뒤 통합 테스트를 수행합니다. 개별 테스트 코드는 각 프로젝트에 넣으면 되는데, 통합 테스트 코드가 애매합니다.
통합 테스트는 진입점이 되는 프로젝트와 함께 두는 편이 자연스럽습니다. 기능 테스트는 대개 프런트엔드에 연결하고, 테스트 코드가 그 서비스의 호스트 이름·포트에 결합될 가능성이 높기 때문입니다.
여기서 중요한 원칙이 하나 나옵니다. 통합 테스트는 공급자가 아니라 소비자 프로젝트에 둡니다. 애플리케이션 인프라 스택이 공유 네트워킹 스택을 사용한다고 할 때, 통합 테스트를 네트워킹 스택에 넣으면 네트워킹 프로젝트가 자기를 쓰는 애플리케이션 스택을 알아야 합니다. 의존성 방향이 뒤집히고 루프가 생깁니다. 소비자 쪽은 이미 공급자를 알고 있으므로 루프가 없습니다.
전용 통합 테스트 프로젝트
통합 테스트만을 위한 별도 프로젝트를 두는 방법도 있습니다. 통합 테스트 단계마다 하나씩 두기도 하는데, 콘웨이의 법칙이 예측하는 대로 다른 팀이 통합 테스트를 소유할 때 흔한 형태입니다.
대가는 버전 관리입니다. 테스트를 대상 코드와 떼어 놓으면 “지금 이 시스템 버전에 맞는 테스트 버전이 무엇인가”를 사람이 헷갈리기 시작합니다. 코드와 테스트를 같이 작성·변경하고, 딜리버리 타임 통합의 팬인 방식처럼 프로젝트 버전을 연관시키는 장치를 두어야 합니다.
기술이 아니라 도메인 개념으로 나누기
하나의 프로젝트 안에서도 파일을 어떻게 쪼갤지가 남습니다. 대부분의 팀은 기술 요소별로 나눕니다.
└── src/
├── cluster.infra
├── database.infra
├── load_balancer.infra
├── routing.infra
├── firewall_rules.infra
└── policies.infra
깔끔해 보이지만 firewall_rules.infra 하나에 cluster.infra가 만든 가상 머신의 규칙과 database.infra가 정의한 데이터베이스 인스턴스의 규칙이 함께 들어갑니다. 8개 서비스를 위한 30개 방화벽 규칙이 한 파일에 섞여 있는 상태와, 한 서비스에 관련된 3개 규칙만 그 서비스 파일 안에 있는 상태 중 무엇이 이해하고 바꾸기 쉬운지는 분명합니다.
기술적 개념이 아니라 도메인 개념 을 중심으로 나누면 하나를 바꿀 때 열어야 할 파일이 하나로 줄어듭니다.
구성값 파일
환경별 구성값을 어디에 둘지도 같은 질문의 변형입니다. 프로젝트 안 environments/에 두거나, 모든 스택의 환경별 구성만 모은 별도 프로젝트를 두거나입니다.
프로젝트 안 (environments/) | 별도 구성 프로젝트 | |
|---|---|---|
| 코드의 성격 | 재사용 가능한 스택 코드에 특정 인스턴스 정보가 섞임 | 스택 코드가 환경 정보로부터 자유로움 |
| 추적 용이성 | 관련 프로젝트 옆이라 값을 따라가기 쉬움 | 모든 스택의 값이 한곳에 섞여 찾기 어려움 |
| 소유권 | 코드와 구성의 책임 주체가 같음 | 분리되면서 양쪽 모두 책임지지 않는 상태 가 되기 쉬움 |
이상적으로는 환경 구성을 바꿀 때 스택 프로젝트를 수정할 필요가 없어야 합니다. 하지만 완전히 떼어 놓았을 때 생기는 소유권 공백이 더 비쌀 수 있어서, 결정 요인은 결국 팀 소유권입니다.
인프라와 애플리케이션 코드
어디에 둘 것인가
별도 저장소로 나누면 인프라팀과 애플리케이션팀이 나뉜 운영 모델을 그대로 지원합니다. 문제는 애플리케이션팀이 자기 애플리케이션과 연관된 인프라까지 책임지는 경우입니다.
코드를 분리해 두면 책임을 맡겼더라도 의식적인 장벽 이 생깁니다. 평소 쓰지 않는 저장소에 있고, 익숙하지 않은 코드가 다른 팀 인프라 코드와 섞여 있으면 손대기가 부담스러워집니다. 반대로 팀 고유 영역에 있는 인프라 코드는 덜 위협적입니다. 내 변경이 남의 애플리케이션이나 인프라 기반을 망가뜨릴 것 같다는 느낌이 덜하기 때문입니다.
딜리버리는 반드시 함께 흘러야 한다
코드를 어디에 두든 인프라와 애플리케이션은 결국 동일한 시스템에 배포됩니다. 따라서 인프라 코드 변경은 애플리케이션 딜리버리 흐름 전체에 걸쳐 애플리케이션과 통합·테스트되어야 합니다.
문제는 많은 조직이 프로덕션 인프라를 별도 사일로로 본다는 것입니다. 한 팀이 스테이징과 프로덕션 인프라를 소유하지만 개발·테스트 환경은 책임지지 않는 구조에서는, 딜리버리 프로세스가 끝날 때쯤에야 두 부분 간 충돌과 격차가 드러납니다.
flowchart LR subgraph silo["사일로 — 프로덕션 인프라만 별도 소유"] A1["앱 변경"] --> D1["개발"] --> T1["테스트"] --> P1["스테이징·프로덕션"] I1["인프라 변경"] -. 뒤늦게 합류 .-> P1 end subgraph flow["통합 — 모든 환경을 인프라 코드로"] I2["인프라 변경"] --> IT["인프라 전용<br/>테스트 단계"] --> D2["개발"] A2["앱 변경"] --> D2 --> T2["테스트"] --> P2["프로덕션"] end
각 단계에서 인프라 변경을 적용한 뒤 애플리케이션 테스트를 트리거하면, 이미 자동화해 둔 애플리케이션 테스트가 그대로 인프라의 통합 테스트가 됩니다. 앞단에 인프라 전용 테스트 단계와 환경 을 두는 것도 중요합니다. 공유 개발·테스트 환경에 검증되지 않은 인프라 코드를 적용하면 환경이 망가지면서 다른 팀까지 멈추기 때문입니다.
다만 모든 변경을 같은 속도로 흘려보내기는 어렵습니다. 고객용 애플리케이션 변경은 이해관계자 검토가 필요한데, 이때 일상적인 인프라 변경을 함께 묶으면 보안 패치 같은 긴급 변경이 애플리케이션 릴리스 프로세스에 발목을 잡힙니다. 어플라이 타임 통합을 쓰면 진행 중인 애플리케이션 변경과 묶지 않고 인프라 변경만 다운스트림으로 밀어낼 수 있습니다.
인프라 코드로 애플리케이션을 배포하지 않기
인프라 코드는 서버에 무엇이 올라가는지를 정의하고, 애플리케이션 배포도 서버에 무언가를 올리는 일입니다. 그래서 인프라 코드로 배포까지 자동화하는 게 합리적으로 보이지만, 실제로 섞으면 복잡해집니다.
Dropwizard 자바 애플리케이션을 Linux VM에 배포하는 Chef 쿡북 사례가 이를 잘 보여 줍니다. 쿡북이 해야 했던 일은 다운로드와 압축 해제, 이전 프로세스 중지, 구성 파일 갱신, 심볼릭 링크 교체, DB 스키마 마이그레이션, 새 프로세스 시작, 정상 동작 확인이었습니다. 결국 선언형 인프라 코드베이스 안에 들어앉은 절차적 스크립트 였고, 이전 프로세스가 안 죽은 것을 감지하지 못하거나 새 프로세스가 시작된 지 1분 뒤에 충돌한 것을 놓치는 일이 반복됐습니다.
문제는 두 겹이었습니다.
| 문제 | 해결 |
|---|---|
| 배포 절차를 인프라 코드가 직접 구현 | RPM으로 패키징. OS 패키징 시스템은 이미 정의된 배포 인터페이스이므로, 인프라 코드는 패키지 파일만 지정하고 배포는 배포 도구에 맡김 |
| 여러 서버에 걸친 배포 순서 오케스트레이션 | 배포 작업을 서버 구성 코드에서 떼어내 빌드 서버에서 푸시하는 스크립트로 이동. 배포 순서와 로드 밸런서 구성을 관리해 무중단 롤링 업그레이드 구현 |
RPM 이후에도 쿡북은 서버 간 배포 순서를 몰라서 클러스터가 뒤섞인 버전을 실행했고, 한 번만 실행되어야 할 DB 스키마 마이그레이션을 첫 서버에서만 돌리려고 잠금까지 구현해야 했습니다. 오케스트레이션은 서버 구성 코드가 감당할 문제가 아니었던 것입니다.
분산 클라우드 네이티브 환경에서는 이 부담이 더 커집니다. 수십에서 수천 개 인스턴스의 변경을 조정해야 하므로 Helm이나 Octopus Deploy 같은 도구로 애플리케이션 그룹의 배포를 따로 정의하고, 기반 클러스터 프로비저닝은 코드베이스의 다른 부분에 맡깁니다.
결론
가장 강력한 배포 전략은 각 요소를 느슨하게 결합된 상태로 유지하는 것입니다. 각 변경을 다른 변경과 독립적으로 배포하기 쉬울수록 전체 시스템의 안정성이 높아집니다.
비교 / 트레이드오프
| 모노리포 | 마이크로리포 | 멀티 저장소 | |
|---|---|---|---|
| 저장소 단위 | 전체 코드 하나 | 프로젝트 하나당 하나 | 관련 프로젝트 묶음 |
| 잘 맞는 통합 시점 | 빌드 타임 | 딜리버리·어플라이 타임 | 묶음 안은 빌드 타임, 묶음 밖은 그 이후 |
| 버전 일관성 | 자동으로 보장 | 저장소 간 버전 조합을 따로 관리 | 묶음 단위로 보장 |
| 경계 유지 | 파일 참조로 쉽게 무너짐 | 물리적으로 강제 | 묶음 경계에서만 강제 |
| 팀 소유권 | 커밋 기록·접근 제어가 뒤섞임 | 팀별 소유가 명확 | 소유권 단위로 끊을 수 있음 |
| 주된 비용 | 도구 확장성, 경계 침식 | 저장소에 걸친 변경과 빌드 조합 관리 | 어디에 선을 그을지 계속 판단해야 함 |
내 생각
- 모노리포 논쟁의 진짜 변수는 통합 시점입니다. 빌드 타임에 묶어야 하는 코드끼리는 같은 저장소에, 딜리버리 파이프라인에서 만나는 코드는 다른 저장소에 두면 대부분 자연스럽게 정리됩니다. “우리는 모노리포다”부터 정하고 시작하면 순서가 거꾸로입니다.
- 통합 테스트를 소비자 쪽에 두라는 규칙은 코드에서도 그대로 통합니다. 공통 모듈이 자기를 쓰는 서비스의 통합 테스트를 들고 있으면 의존성이 역류하고, 그 모듈은 결국 아무도 못 바꾸는 코드가 됩니다.
- 환경별 구성을 별도 프로젝트로 뺐다가 아무도 안 보게 되는 경우를 자주 봅니다. 소유권 공백이 생기는 쪽이 코드 순수성보다 훨씬 비싸므로, 팀이 나뉘어 있지 않다면 기본값은 프로젝트 안
environments/가 낫습니다. - “인프라 코드 안의 절차적 배포 스크립트”는 Chef 시절 얘기로 끝나지 않았습니다. Terraform
provisioner, Ansible의 애플리케이션 배포 롤, Helm 차트 안의 복잡한 훅이 전부 같은 냄새입니다. 배포는 배포 도구에게, 인프라는 인프라 도구에게 맡기는 경계가 여전히 유효합니다. - 인프라 전용 테스트 단계가 없으면 공유 개발 환경이 인프라팀의 프로덕션이 됩니다. 개발 환경이 인프라 변경으로 깨질 때마다 개발팀 전체가 멈추므로, 이 단계 하나가 조직 내 신뢰를 좌우합니다.
관련 개념
- Ch07 스택 인스턴스 구성하기 — 환경별 구성값을 스택에 전달하는 패턴
- Ch08 코드를 지속적으로 테스트하고 딜리버리한다 — 빌드·딜리버리·어플라이 타임 통합과 딜리버리 단계
- Ch09 인프라 스택 테스트하기 — 프로그레시브 테스트와 테스트 더블
- Ch15 시스템을 작고 간단하게 빌드한다 — 콘웨이의 법칙, 도메인 개념 중심 설계
- Ch16 컴포넌트에서 스택 빌드하기 — 프로젝트 내부를 모듈로 나누는 기준
- Ch17 스택을 컴포넌트로 사용하기 — 공급자·소비자 스택의 의존성 방향