'기능 단위로 쪼갠다'는 마이크로서비스가 아니다

Microservice Boundary — 도메인 + 독립 배포

ArchitectureMicroservice

난이도: ★★★☆☆
연관 노트: BFF와 API Gateway, 3조건은 같고 축이 다르다


핵심 요약

도메인 기반으로 독립적으로 배포 / 데이터 관리 / 단일 책임으로 구분할 수 있게 나누는 구조

이 문장에 도달하기까지 3번 정정이 필요했다. 각 정정이 이 개념의 경계선이다.

  1. “보험/뱅크/증권” → 그건 마이크로서비스가 아니라 도메인 그룹이다. 실제 서비스 단위는 그 안의 “계좌조회”, “이체”, “알림”처럼 더 잘게 쪼개진 것들.
  2. “기능 단위” → 너무 느슨하다. “로그인 버튼 기능”처럼 UI 조작 단위까지 포함되고, 모놀리스 안의 모듈화와 구분이 안 된다.
  3. “배포 가능하게” → 모놀리스도 배포는 된다. 핵심 단어는 “독립적으로” — 다른 서비스에 영향 없이 나 혼자 배포·장애·확장될 수 있는가.

시나리오 판정 (레포 분리 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 APIAPIO명시적 계약
분석 배치 → 웨어하우스 테이블테이블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 — 같은 원리를 프론트엔드에 적용. 독립 배포 조건이 프론트에서 훨씬 비싼 이유

↔ 같은 원리가 적용되는 곳


부록: 왜 “마이크로”가 오해를 부르는가

접두사가 크기를 가리키는 것처럼 들려서 “작으면 마이크로서비스”라는 오해가 생긴다. 실제 기준은 크기가 아니라 독립성이다.

코드 3만 줄짜리 서비스도 독립 배포·독립 DB·단일 책임을 만족하면 마이크로서비스고, 200줄짜리라도 남의 DB를 직접 읽으면 아니다. Sam Newman이 “microservice”보다 “independently deployable service”라는 표현을 선호하는 이유다.


참고