함께 배포되는 것들 사이에는 계약이 필요 없지만, 따로 배포되는 순간 계약이 생긴다.
이 경계는 무엇을 청구하는가
수명의 경계가 시작됩니다. 앞의 열 편이 시스템이 한 순간에 어떤 모양인지를 다뤘다면, 이제 시스템이 시간 속에서 변해가는 이야기입니다.
배포 경계는 함께 나가는 것들의 묶음입니다. 같은 배포 단위 안에 있으면, 두 코드는 언제나 같은 버전입니다. 함수 시그니처를 바꾸면 호출부도 같이 바뀌고, 둘은 동시에 배포됩니다.
배포 단위를 나누는 순간 이 전제가 깨집니다. A를 배포했는데 B는 아직 옛 버전입니다. 그 사이에 둘은 서로 다른 가정으로 동작합니다.
이 경계가 청구하는 것은 버전 스큐(version skew) 입니다.
- 두 배포 단위가 항상 같은 버전이라고 가정할 수 없습니다.
- 그래서 계약이 필요하고, 계약은 이전 버전을 수용하는 하위 호환이어야 합니다.
- 그리고 계약을 바꾸려면 여러 단계를 거쳐야 합니다.
이 비용을 내지 않으면 다른 비용을 내야 합니다. 한 덩어리로 배포하면 계약은 필요 없지만, 작은 수정 하나에도 전체를 배포해야 합니다. 배포가 무거워지면 배포 빈도가 줄고, 배포 빈도가 줄면 한 번에 나가는 변경이 커지고, 문제가 생겼을 때 원인을 찾기 어려워집니다.
배포 경계가 답하는 질문은 하나입니다. 무엇이 함께 배포되어야 하는가.
이 경계에 붙어 있는 이름들
스펙트럼
배포 단위는 이분법이 아니라 어느 정도 스펙트럼을 띄고 있습니다.
| 형태 | 배포 단위 | 계약 | 적합한 조건 |
|---|---|---|---|
| 모놀리스 | 하나 | 불필요 | 팀이 하나이거나 릴리스 주기가 같음 |
| 모듈러 모놀리스 | 하나 | 코드 내부 규약 | 경계는 필요하지만 독립 배포는 아직 |
| 서비스 몇 개 | 몇 개 | API | 릴리스 주기가 다른 영역이 생김 |
| 마이크로서비스 | 많음 | API + 이벤트 | 팀이 많고 각자 배포해야 함 |
모놀리스는 나쁘고 마이크로서비스는 좋다는 흔한 오해가 있지만, 그렇지 않습니다.
배포 단위를 나눌 이유는 기술이 아니라 조직과 속도입니다.
- 결제팀이 매일 배포하고 싶은데 검색팀의 릴리스를 기다려야 한다 → 나눌 이유가 있습니다.
- 팀이 하나인데 서비스를 열두 개로 나눴다 → 배포 파이프라인 열두 개를 한 팀에서 관리해야 합니다.
ep.01 모듈 경계와 배포 경계는 다릅니다. 모듈은 사람이 읽는 단위이고, 배포 단위는 릴리스가 나가는 단위입니다. 모듈러 모놀리스는 이 둘을 의도적으로 어긋내는 선택입니다. 코드는 깨끗하게 나누되, 배포는 하나로 유지합니다.
모듈러 모놀리스에서 시작하는 것이 대부분의 경우 옳습니다. 경계가 이미 그어져 있으면, 나중에 떼어내는 것은 어렵지 않습니다. 반대로 경계 없이 나누면 분산 모놀리스가 됩니다. 서비스는 열두 개인데 하나를 배포하려면 다른 열한 개를 같이 배포해야 하는 상태입니다.
마이크로 프론트엔드
프론트엔드에도 같은 논의가 있습니다. 하나의 SPA를 여러 팀이 함께 만들면, 배포가 병목이 됩니다.
Module Federation은 런타임에 다른 번들의 모듈을 가져오는 방식입니다. 빌드 시점이 아니라 실행 시점에 조립합니다.
// 호스트 설정 - 원격 모듈을 선언합니다
new ModuleFederationPlugin({
name: 'shell',
remotes: {
checkout: 'checkout@https://checkout.example.com/remoteEntry.js',
search: 'search@https://search.example.com/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
})
// 사용하는 쪽 - 빌드 시점에는 이 코드가 없습니다
const CheckoutWidget = lazy(() => import('checkout/Widget'))
shared가 핵심이자 함정입니다. React를 두 벌 로드하면 훅이 깨집니다. 그래서 singleton: true로 하나만 쓰게 합니다. 그런데 결제팀이 React 19로 올리고 검색팀이 18에 남아 있으면, 둘 중 하나는 자기가 테스트하지 않은 버전에서 돌게 됩니다.
공유 의존성은 배포 경계를 다시 이어붙이는 지점입니다. 독립 배포하겠다고 나눴는데, React 버전 때문에 다시 조율해야 합니다.
더 단순한 방법도 있습니다.
- 라우트 단위 분리:
/checkout/*은 통째로 다른 앱입니다. 페이지 이동 시 전체 로드가 일어나지만, 완전히 독립적입니다. - iframe: 격리가 가장 확실합니다. 대신 통신이 번거롭고 접근성·라우팅 처리가 까다롭습니다.
- 웹 컴포넌트: 프레임워크 독립적입니다. 대신 상태 공유와 스타일링에 제약이 있습니다.
마이크로 프론트엔드가 필요한 경우는 생각보다 적습니다. 팀이 서넛이고 릴리스를 기다리는 것이 실제 병목일 때 고려합니다. 그 전에는 ep.01 모듈 경계와 코드 스플리팅으로 충분합니다.
코드 스플리팅과 청크 경계
ep.01 모듈 경계에서 예고했던 이야기입니다. 소스의 모듈 경계와 브라우저가 내려받는 청크 경계는 다릅니다.
청크를 나누는 기준은 모듈 구조가 아니라 함께 필요해지는 시점입니다.
// 라우트 단위 - 가장 자연스러운 경계
const CheckoutPage = lazy(() => import('./pages/checkout'))
// 조건부 기능 - 쓰는 사람만 받습니다
async function openEditor() {
const { RichTextEditor } = await import('./features/editor')
// 무거운 에디터를 이 시점에 받습니다
}
// 무거운 라이브러리 - 필요할 때만 받습니다
async function exportToPdf(data: Report) {
const { jsPDF } = await import('jspdf')
}
청크가 너무 잘아도 문제입니다. 요청이 많아지고, 각 요청에 왕복 비용이 붙습니다. 대략적인 기준은 이렇습니다.
- 라우트 단위로 먼저 나눕니다.
- 소수만 쓰는 무거운 기능을 따로 뗍니다.
- 여러 라우트가 함께 쓰는 것은 공통 청크로 묶습니다.
- 그 이상은 측정한 뒤에 나눕니다.
Feature Flag
배포와 출시를 분리하는 도구입니다. 코드는 배포되어 있지만 켜져 있지 않은 상태를 만듭니다.
// 배포는 되었고, 켜기는 별도
if (await flags.isEnabled('new-checkout-flow', { userId })) {
return <NewCheckoutFlow />
}
return <LegacyCheckoutFlow />
플래그를 도입하면 롤백 속도가 비약적으로 상승합니다. 문제가 생기면 재배포 없이 플래그를 끕니다. 재배포에 10분이 걸려도 플래그는 10초만에 바꿀 수 있습니다.
플래그에는 종류가 있고, 수명이 다릅니다.
| 종류 | 목적 | 수명 |
|---|---|---|
| 릴리스 플래그 | 점진 출시 | 며칠~몇 주: 반드시 제거 |
| 실험 플래그 | A/B 테스트 | 실험 기간: 반드시 제거 |
| 운영 플래그 | 부하 시 기능 차단 | 영구 |
| 권한 플래그 | 플랜별 기능 | 영구 |
앞의 두 종류를 제거하지 않는 것이 가장 흔한 부채입니다. 플래그가 서른 개 쌓이면 조합이 10억 가지가 되고, 어떤 조합이 실제로 테스트되었는지 아무도 모릅니다.
// 플래그에 만료일을 붙여 둡니다
export const FLAGS = {
'new-checkout-flow': { owner: 'checkout-team', expiresAt: '2026-09-30' },
'search-v2': { owner: 'search-team', expiresAt: '2026-08-31' },
} as const
만료일이 지난 플래그를 CI가 경고하게 만들면, 정리가 일정에 들어옵니다.
배포 전략
배포 단위를 나누면 배포 방식도 선택지가 생깁니다.
블루/그린: 두 개의 완전한 환경을 두고 트래픽을 한 번에 전환합니다. 롤백이 즉시 가능합니다. 대신 자원이 두 배입니다.
카나리: 새 버전에 트래픽 일부만 보내고 관찰합니다. 문제가 보이면 되돌립니다. 자원 효율이 좋고, 실사용 데이터로 판단할 수 있습니다.
롤링: 인스턴스를 하나씩 교체합니다. 자원이 적게 들지만, 교체 중에는 두 버전이 동시에 돕니다.
세 방식 모두 두 버전이 동시에 존재하는 구간이 있습니다. 블루/그린조차 전환 순간에 진행 중이던 요청이 있습니다. 그래서 데이터베이스 스키마 변경은 배포와 분리되어야 합니다. 이 문제는 ep.12 변경 경계에서 Expand-Contract로 다룹니다.
안티패턴
분산 모놀리스: 서비스는 나뉘었는데 함께 배포해야 하는 상태입니다. 나눈 비용은 다 내고 얻는 이득은 없습니다.
공유 데이터베이스: 두 서비스가 같은 테이블을 직접 읽고 쓰면, 스키마를 바꿀 때 양쪽을 조율해야 합니다. 배포 경계가 데이터베이스 앞에서 무너집니다.
정리되지 않는 플래그: 릴리스 플래그와 실험 플래그가 역할을 끝낸 뒤에도 코드에 남아 있는 상태입니다. 서른 개가 쌓이면 조합이 10억 가지가 되고, 그중 어떤 조합이 실제로 테스트되었는지 아무도 모릅니다.
빌드 타임 공유 라이브러리 강제 버전: 마이크로 프론트엔드를 하면서 모든 앱이 같은 디자인 시스템 버전을 쓰도록 강제하면, 디자인 시스템 릴리스가 모든 앱의 배포를 막습니다.
이 경계가 화면에 드러나는 자리
배포 경계는 사용자에게 일관성의 균열로 나타납니다.
한 화면에 두 버전이 섞일 때
마이크로 프론트엔드에서 결제 위젯은 디자인 시스템 v2를, 검색 위젯은 v1을 쓸 수 있습니다. 두 위젯이 한 화면에 나란히 있으면 사용자는 버튼 모서리 반경이 다르고, 파란색 톤이 미묘하게 다른 화면을 봅니다.
명시적으로 지적하지 못해도 사용자는 느낍니다. “뭔가 어설프다”는 인상으로 남습니다.
대응 방법이 몇 가지 있습니다.
토큰만 공유합니다. 컴포넌트는 각자 버전을 쓰되, CSS 변수로 내려오는 토큰은 셸이 하나만 제공합니다. 색과 간격이 통일되면 균열이 크게 줄어듭니다.
/* 셸이 제공하는 토큰 - 모든 위젯이 이것을 씁니다 */
:root {
--color-brand-primary: #4A5FD9;
--radius-md: 8px;
--spacing-4: 16px;
}
시각적으로 분리합니다. 서로 다른 팀의 위젯을 나란히 두지 않습니다. 카드로 감싸거나 섹션을 나누면, 미세한 차이가 “다른 영역”으로 읽힙니다.
버전 차이의 상한을 정합니다. “디자인 시스템 메이저 버전 차이는 최대 1까지”처럼 합의를 두면, 균열의 크기에 천장이 생깁니다.
점진 롤아웃의 UX
카나리와 feature flag는 일부 사용자만 새 화면을 보는 상황을 만듭니다. 여기에는 UX 결정이 따라옵니다.
첫째, 같은 사용자는 언제나 같은 쪽에 있어야 합니다. 새로고침할 때마다 화면이 바뀌면 사용자가 혼란스럽습니다. 플래그 판정에 사용자 ID를 넣어 고정합니다.
// 사용자 ID 해시로 고정 - 같은 사람은 항상 같은 버킷
function isInBucket(userId: string, percentage: number): boolean {
const hash = hashString(userId)
return (hash % 100) < percentage
}
둘째, 새 화면을 본 사용자에게 의견을 들을 수 있어야 합니다. “새로운 결제 화면을 사용 중입니다”라는 표시와 피드백 경로가 있으면, 문제를 일찍 발견합니다.
셋째, 되돌아갈 수 있는지 정합니다. 베타 기능이라면 “이전 버전으로 돌아가기”가 필요합니다. 되돌아갈 수 없다면 그렇게 말해야 합니다.
넷째, 도움말과 실제 화면이 어긋나지 않게 합니다. 절반의 사용자가 새 화면을 보는 동안, 고객센터 문서는 어느 쪽을 설명해야 할까요. 이 조율은 배포 결정에 딸려 오는 실무 비용입니다.
플래그가 감춘 것이 DOM에 남을 때
클라이언트에서 판정하는 플래그는 꺼진 기능의 코드까지 브라우저에 내려보냅니다.
// 플래그가 꺼져 있어도 이 컴포넌트 코드는 번들에 있습니다
{flags.newPricing && <NewPricingTable prices={secretPrices} />}
아직 공개하지 않은 가격표가 번들에 들어 있으면, 개발자 도구로 볼 수 있습니다. 미공개 정보가 걸린 플래그는 서버에서 판정하고, 꺼진 쪽의 데이터를 아예 내려보내지 않아야 합니다. ep.10 신뢰 경계의 원칙이 여기서도 적용됩니다.
참고 자료
- Fowler, M. (2015). MicroservicePremium. https://martinfowler.com/bliki/MicroservicePremium.html
- Jackson, C. (2019). Micro Frontends. https://martinfowler.com/articles/micro-frontends.html
- Webpack. Module Federation. https://webpack.js.org/concepts/module-federation/
- Hodgson, P. (2017). Feature Toggles (aka Feature Flags). https://martinfowler.com/articles/feature-toggles.html
- Humble, J., & Farley, D. (2010). Continuous Delivery. Addison-Wesley.
다음 편 예고
배포 단위를 나누면 계약이 생깁니다. 그리고 계약은 바꾸기 어렵습니다. 어느 부분이 약속이고 어느 부분이 구현인지, 그 선을 잘못 그으면 영원히 유지해야 할 것이 늘어납니다.