'기능 단위로 쪼갠다'는 마이크로서비스가 아니다
Microservice Boundary — 도메인 + 독립 배포
난이도: ★★★☆☆
연관 노트: BFF와 API Gateway, 3조건은 같고 축이 다르다
핵심 요약
도메인 기반으로 독립적으로 배포 / 데이터 관리 / 단일 책임으로 구분할 수 있게 나누는 구조
이 문장에 도달하기까지 3번 정정이 필요했다. 각 정정이 이 개념의 경계선이다.
- “보험/뱅크/증권” → 그건 마이크로서비스가 아니라 도메인 그룹이다. 실제 서비스 단위는 그 안의 “계좌조회”, “이체”, “알림”처럼 더 잘게 쪼개진 것들.
- “기능 단위” → 너무 느슨하다. “로그인 버튼 기능”처럼 UI 조작 단위까지 포함되고, 모놀리스 안의 모듈화와 구분이 안 된다.
- “배포 가능하게” → 모놀리스도 배포는 된다. 핵심 단어는 “독립적으로” — 다른 서비스에 영향 없이 나 혼자 배포·장애·확장될 수 있는가.
시나리오 판정 (레포 분리 O, 배포 분리 O, DB 공유 O)
“독립 데이터에 걸린다. 레포가 나눠져 있고 별도 서버로 배포되니까 독립 배포는 가능할 것 같고. 다만 order 서비스가 member 서비스에 영향을 주면 안 되는데 지금 같은 스키마라서 영향을 준다.”
판정은 정확하다. 다만 두 가지가 더 있다:
- 영향 차단은 “단일 책임”이 아니라 “독립 데이터·독립 배포”의 결과다. 단일 책임은 “변경 이유가 하나”라는 뜻.
- 독립 배포도 실은 반쪽만 통과다. 배포 행위는 독립이지만, 남의 스키마 변경에 런타임으로 터진다.
왜 헷갈렸나
정정 1 — 도메인 그룹을 서비스로 봤다
핀테크 슈퍼앱의 “보험 / 뱅크 / 증권”을 마이크로서비스라고 생각했다. 이건 사업부·도메인 그룹 단위다. “뱅크” 하나만 열어봐도 내부에 계좌조회·이체·알림·인증이 각각 독립 서비스로 들어있다.
즉 보험/뱅크/증권은 마이크로서비스가 아니라 마이크로서비스들의 묶음이다.
정정 2 — “기능”이라는 단어가 층을 뭉갠다
“기능 단위로 쪼갠다”고 정리했는데, “기능”은 층이 없는 단어다.
"기능"이라는 말이 동시에 가리키는 것들:
아이디 입력 ← UI 조작. 서비스 아님
로그인 버튼 ← UI 조작. 서비스 아님
─────────────────────────────────
인증 처리 ← 업무 단위. 서비스 O
이체 처리 ← 업무 단위. 서비스 O
마이크로서비스는 아래쪽 층만 해당한다. 기준은 비즈니스 능력(business capability) — 그 자체로 완결된 업무 의미가 있는가. UI 조작 단위가 아니라 업무 단위다.
그래서 “도메인 단위”가 “기능 단위”보다 정확하다.
정정 3 — “배포 가능”은 모놀리스도 만족한다
“도메인 단위로, 배포 가능하게”까지 왔는데 여기서 빠진 단어가 **“독립적으로”**다.
이 단어가 없으면 모듈화와 구분이 안 된다. 모놀리스 안에서도 코드를 도메인별 모듈로 나눌 수 있다. 하지만 그건 마이크로서비스가 아니다 — 같이 배포되고, 같은 DB를 쓰니까.
메커니즘
3가지 필수 조건
⚠️ “3조건”이라는 말이 두 곳에서 쓰인다. 여기(마이크로서비스)의 3조건은 아래와 같고, 게이트웨이 3조건(경계 / 단일 진입점 / 검문+변환)과는 다른 것이다. 게이트웨이 3조건은 “서비스 앞단에 무엇을 세울지”를, 이쪽은 “서비스를 어디서 쪼갤지”를 판정한다.
1. 독립 배포 이체 서비스 고친다고 알림 서비스를 재배포하지 않는다
2. 독립 데이터 자기 DB만 쓴다. 남의 DB 직접 조회 금지 → API로만 통신
3. 단일 책임 하나의 명확한 이유로만 변경된다
모듈화 vs 마이크로서비스
같은 도메인 분리인데도 갈리는 지점.
| 모놀리스 + 모듈화 | 마이크로서비스 | |
|---|---|---|
| 코드 분리 | O (도메인별 폴더/패키지) | O |
| 배포 | ✗ 전부 함께 | O 각자 따로 |
| DB | ✗ 하나를 공유 | O 각자 소유 |
| 장애 범위 | ✗ 하나 죽으면 전체 | O 그 서비스만 |
| 통신 | 함수 호출 (인프로세스) | 네트워크 API |
구분선은 배포·DB·장애 범위다. 코드 분리만으로는 판정할 수 없다.
계층 구조
[ 클라이언트 ]
│
┌────────┴────────┐
│ API Gateway │ ← 라우팅·인증. 어디로 갈지 몰라도 됨
└────────┬────────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
[계좌조회] [이체] [알림] ← 마이크로서비스
│ │ │
[DB-A] [DB-B] [DB-C] ← 각자 소유. 남의 DB 접근 금지
└───────── "뱅크" 도메인 그룹 ──────┘
(그룹은 서비스가 아니다)
이 그림에서 왜 API Gateway가 여러 서비스 앞단에 필요한지도 같이 드러난다. 서비스가 잘게 쪼개져 있으니, 클라이언트가 “계좌조회는 어디, 이체는 어디”를 다 알 필요 없게 라우팅을 대신 받아주는 층이 필요하다.
경계를 정하는 질문
“무엇이 함께 있으면 편한가”가 아니다.
✗ "이 코드들 관련 있으니 같이 두자"
✓ "무엇이 독립적으로 변경·배포·장애나야 하는가"
전자로 그으면 경계가 계속 커진다. 후자로 그어야 경계가 서비스 단위로 수렴한다.
함정: “레포도 나눴고 배포도 따로 하는데요”
3조건 중 두 개를 만족한 것처럼 보이지만 실제로는 하나도 온전하지 않은 흔한 상태.
order-service, member-service, payment-service
· 레포 분리 O
· 별도 서버 배포 O
· 같은 RDS, 같은 스키마
· order 가 member 테이블을 직접 SELECT
독립 데이터 — 명확히 위반
남의 테이블을 직접 읽는 순간 경계가 없다. 통신이 API가 아니라 DB를 경유한 암묵적 통신이 된다.
독립 배포 — 반쪽만 통과
member 팀이 member.name → member.full_name 으로 rename
member-service 배포 ✅ (자기 코드도 같이 고쳤으니)
order-service 💥 아무것도 안 했는데 런타임에 터짐
SELECT name FROM member
← 컴파일 타임에 안 잡힌다
독립적으로 배포는 되지만 독립적으로 안전하지는 않다. 공유 스키마는 타입 시스템에 보이지 않는 커플링을 만든다. “배포 파이프라인이 분리됐다”는 독립 배포의 증거가 아니다.
소유(own)와 참조(reference)
“고객 정보는 어느 서비스나 필요한데, 그러면 같은 테이블을 봐야 하지 않나?” — 여기서 갈리는 게 소유와 참조의 구분이다.
❌ 공유 스키마 ✅ 소유 + 참조
┌──────────────┐ ┌────────────┐ ┌────────────┐
│ member 테이블 │◀── SELECT │ order DB │ │ member DB │
└──────────────┘ │ │ │ │
▲ ▲ │ member_id │ │ id (PK) │
│ │ │ ↑ 참조 키 │ │ name │
[order] [payment] │ (값만 보유) │ │ phone │
└────────────┘ └────────────┘
남의 테이블을 읽는다 │ ▲
└─ API 호출 ───┘
"이 id의 이름 뭐야?"
order는 member_id 값만 갖는다. 회원 이름이 필요하면 member API를 호출한다.
의도적 복제는 중복이 아니다
주문에 배송지가 필요할 때 member의 현재 주소를 매번 조회하면 안 된다.
| 회원의 현재 주소 | 주문 당시 배송지 | |
|---|---|---|
| 소유 | member 서비스 | order 서비스 |
| 바뀌는가 | 회원이 이사하면 바뀜 | 절대 안 바뀜 (과거 사실) |
회원이 이사해도 3년 전 주문의 배송지는 그대로여야 한다. 다른 데이터이므로 order가 주문 시점 스냅샷을 자기 DB에 갖는 게 정상이다. 정규화 위반이 아니라 시간 축이 다른 별개 사실이다.
테이블을 계약으로 쓰나, API를 계약으로 쓰나
“참조하는 스키마가 서로 달라야 하는가?” — 아니다. 스키마 모양은 무관하다. 결정적인 건 무엇을 계약(contract)으로 삼는가다.
❌ 테이블이 계약 ✅ API가 계약
[order] ──SELECT name──▶ member 테이블 [order] ──GET /members/{id}──▶ [member]
│
member 팀이 name→full_name rename 내부에서 name→full_name rename │
│ │ │
▼ ▼ │
order 터짐 💥 API 응답은 그대로 { name } ◀────┘
order 영향 없음 ✅
| 테이블이 계약 | API가 계약 | |
|---|---|---|
| 계약이 | 암묵적 — 누가 읽는지 아무도 모름 | 명시적 — 응답 형태가 계약 |
| 소유 팀의 자유 | 컬럼명 못 바꿈 (누가 깨질지 모름) | 내부 컬럼 자유롭게 변경 |
| 깨질 때 발견 | 런타임, 프로덕션 | 계약 테스트 / 스키마 검증 |
API를 끼우면 내부 스키마를 바꿀 자유가 생긴다. 캡슐화를 서비스 단위로 확대한 것과 정확히 같다.
class Member {
private name; // ← DB 컬럼. 자유롭게 rename 가능
public getName() {} // ← API. 이건 계약이라 못 바꿈
}
SELECT name FROM member는 남의 private 필드에 손을 넣는 것이다.
정리: 참조 스키마가 달라야 하는 게 아니라, 스키마를 참조하지 않아야 한다. 참조 대상이 테이블에서 API로 바뀌는 것.
member_id는 의존 아닌가
의존이다. 다만 식별자는 공개 계약의 일부다. ID는 설계상 안 바뀌게 만드는 값이고, 이게 최소한으로 받아들이는 결합이다. 없으면 두 서비스를 연결할 방법 자체가 없다.
물리 분리가 필수인가
조건은 접근 차단이고, 물리 분리는 그걸 강제하는 수단이다.
이상 : 별도 DB 인스턴스 → 물리적으로 불가능
최소선 : 같은 인스턴스, 스키마 분리 → 크로스 스키마 쿼리 금지 (권한으로 강제)
위반 : 같은 스키마, 남의 테이블 SELECT → 위 시나리오
같은 인스턴스를 쓰더라도 DB 유저 권한으로 남의 테이블 SELECT를 막으면 독립 데이터 조건은 성립한다. “같은 인스턴스니까 위반”이 아니라 “읽을 수 있으니까 위반”이다.
판정 기준은 “서버↔DB”가 아니라 “누구의 DB냐”
테이블 계약이 항상 나쁜 것은 아니다. 자기 테이블을 읽는 건 private 필드를 자기가 읽는 것이니 당연히 정상이다.
| 접근 | 계약 형태 | 정상? | 이유 |
|---|---|---|---|
order-service → order 테이블 | 테이블 | O | 자기 소유 |
order-service → member 테이블 | 테이블 | X | 남의 소유 |
order-service → member API | API | O | 명시적 계약 |
| 분석 배치 → 웨어하우스 테이블 | 테이블 | O | 웨어하우스는 읽히려고 존재 |
규칙: 테이블 계약이 나쁜 게 아니라, 소유하지 않은 테이블을 계약으로 삼는 게 나쁘다.
웨어하우스도 대가는 똑같이 낸다
웨어하우스는 여러 소스를 모아 읽히는 것이 설계 목적이므로 SELECT가 위반이 아니다. 하지만 계약이 테이블 스키마라는 사실은 변하지 않으므로, 같은 실패 모드가 재현된다.
데이터팀이 상류 테이블 컬럼 rename
│
└─▶ 발송 배치가 조용히 깨짐 💥
SELECT user_status FROM ...
← 여기도 컴파일 타임에 안 잡힌다
order → member 테이블과 실패 모드가 동일하다. 레이어만 다르다.
실무 대응: view가 API 역할을 한다.
[ 원본 테이블 ] ← 데이터팀이 자유롭게 스키마 변경
│
▼
[ view / 마트 ] ← 이게 계약. 컬럼명·타입 고정
│
▼
[ 소비 배치 ] ← view만 본다. 원본 변경에 영향 없음
view가 서비스 API와 같은 역할이다. 내부는 자유롭게 바꾸고 노출면만 고정하는 동일한 캡슐화 패턴. 원본을 직접 SELECT하는 배치는 남의 private 필드를 읽는 것과 같다.
”payment는 했는데 order는 안 한 고객”은 다른 경계의 질문
크로스 서비스 질의를 운영 DB JOIN으로 풀려고 하면 독립 데이터 조건이 무너진다. 이건 분석 질의이고, 경계가 다르다.
[ 운영 경계 — OLTP ] [ 분석 경계 — OLAP ]
order DB ─┐
member DB ─┼──── 적재 ────▶ [ 데이터 웨어하우스 ]
payment DB ─┘ 여기서 마음껏 JOIN
서비스별로 분리 전부 모아서 통합
"이 주문 처리해" "payment했는데 order 안 한 사람"
축: 서비스 독립성 축: 질의 자유도
경계가 두 개인데 축이 다르다 — 게이트웨이 노트의 “변경 축” 판정과 정확히 같은 구조다. 운영 DB를 직접 JOIN하지 말고 웨어하우스에서 푸는 것이 독립 데이터 조건에서 나오는 결론이다.
다음에 이 상황을 만나면
“이거 마이크로서비스로 쪼개죠”라는 말이 나올 때 → 3조건으로 검증한다.
- 이건 업무 단위인가 UI 조작 단위인가? 후자면 서비스가 아니다
- 이걸 고칠 때 다른 서비스도 재배포해야 하나? 그러면 독립 배포가 아니다
- 자기 DB를 갖는가? 남의 테이블을 직접 SELECT 하고 있으면 경계가 없는 것
- 남이 컬럼 이름을 바꾸면 내가 터지나? 터지면 독립 배포가 아니다 (배포 파이프라인 분리 ≠ 독립 배포)
- 이 데이터를 내가 소유하나 참조하나? 참조면 ID만 갖고 API로 물어본다
- 크로스 서비스 집계가 필요한가? 운영 DB JOIN이 아니라 웨어하우스로 보낸다
- 내가 읽는 테이블은 누구 소유인가? 남의 것이면 API를, 웨어하우스면 view를 끼운다
- 상류가 컬럼을 바꾸면 내 배치가 조용히 깨지나? 계약면(API/view)이 없다는 신호
- 이게 죽으면 어디까지 멈추나? 전체가 멈추면 분리된 게 아니다
- 지금 부르는 단위가 실은 도메인 그룹은 아닌가? 안을 열어보면 더 쪼개져야 할 수도
주의: 3조건을 만족시키는 대가는 공짜가 아니다. 함수 호출이 네트워크 호출로 바뀌면 지연·부분 실패·분산 트랜잭션이 새로 생긴다. 팀 규모가 작으면 모듈화된 모놀리스가 더 합리적이다.
커넥팅 닷
← 선행 개념 (이걸 알아야 이해된다)
- 왜 서버는 이전 요청을 기억하지 않는가 — 서비스가 독립적으로 배포·확장될 수 있는 전제가 stateless. 상태를 프로세스에 들고 있으면 인스턴스를 마음대로 늘리거나 죽일 수 없다
- Bounded Context (DDD) — “도메인 단위”의 출처. 어디까지가 하나의 용어 체계인지 그어야 서비스 경계가 나온다
- 캡슐화 (private/public) — 독립 데이터 조건의 뿌리.
SELECT로 남의 테이블을 읽는 건 남의private필드에 손을 넣는 것과 같다. 이 감각이 없으면 “왜 API를 끼워야 하나”가 번거로움으로만 보인다
→ 확장 개념 (여기서 더 나아가면)
- BFF와 API Gateway, 3조건은 같고 축이 다르다 — 쪼개진 서비스들 앞단에 무엇을 세울지. 마이크로서비스가 있어야 Gateway/BFF 논의가 성립한다
- Saga 패턴 / 결과적 일관성 — 독립 DB의 대가. DB 트랜잭션 하나로 끝났던 일을 여러 서비스에 걸쳐 보상 트랜잭션으로 처리해야 한다
- CDC / 이벤트 기반 복제 (Debezium 등) — 소유는 한 서비스가, 사본은 필요한 서비스가 갖는 구조를 자동화. “의도적 복제”를 손으로 관리하지 않는 방법
- 계약 테스트 (Pact 등) — API를 계약으로 삼았을 때 그 계약이 깨졌는지 배포 전에 잡는 방법. 테이블 계약에는 이런 안전망이 없다
- Conway’s Law — 서비스 경계는 결국 조직 경계를 따라간다. 팀이 하나면 마이크로서비스로 쪼개도 다시 붙는다
- Micro Frontends — 같은 원리를 프론트엔드에 적용. 독립 배포 조건이 프론트에서 훨씬 비싼 이유
↔ 같은 원리가 적용되는 곳
- DB에 JOIN을 맡기면 ‘일을 떠넘기는’ 걸까? — 독립 DB의 직접적 대가. 서비스가 갈리면 JOIN이 불가능해지고 애플리케이션 조합 = N+1 위험
- 상태 없는 서버로 CRM 지표 만들기 — 크로스 서비스 집계를 운영 DB가 아니라 웨어하우스에서 푸는 그 패턴. OLTP/OLAP 경계 분리의 실제 사례
- 대규모 트래픽에서 프론트엔드 설계는 진짜 달라지나? — “스케일이 문제를 어떻게 드러내는가”의 같은 계열. 마이크로서비스는 서버 쪽 스케일 대응의 조직적 형태
- ‘쓰기도 하고 읽기도 하는데’ 죽은 코드다 — 데이터 소유권을 선언하지 않으면 경계가 무너진다는 동일한 문제
부록: 왜 “마이크로”가 오해를 부르는가
접두사가 크기를 가리키는 것처럼 들려서 “작으면 마이크로서비스”라는 오해가 생긴다. 실제 기준은 크기가 아니라 독립성이다.
코드 3만 줄짜리 서비스도 독립 배포·독립 DB·단일 책임을 만족하면 마이크로서비스고, 200줄짜리라도 남의 DB를 직접 읽으면 아니다. Sam Newman이 “microservice”보다 “independently deployable service”라는 표현을 선호하는 이유다.
참고
- Microservices — Martin Fowler — 원 정의. 독립 배포와 분산화된 데이터 관리를 특성으로 명시
- MonolithFirst — Martin Fowler — 경계를 모르는 상태에서 먼저 쪼개면 왜 실패하는지