모놀리스가 커지면 흔히 MSA 를 고려한다. 빌드가 느려지고, 한 부분의 변경이 다른 부분의 배포를 막으며, 트래픽 부하가 한 군데에서 전체로 번진다. 통념적 처방은 “작게 쪼개라” 다. 정작 어려운 부분은 어디서 자를지 정하는 일이다.

MSA 는 시스템을 어느 기준으로 가를지 정하는 결정이다. 도메인 경계, 데이터 소유권, 스케일 패턴, 장애 격리 중 어느 것을 우선하느냐가 서비스 경계를 만들고, 그 경계가 통신·데이터 일관성·운영 비용을 차례로 결정한다. 기준이 잘못 잡히면 후속 결정도 같이 어긋나고 한 번 그어진 경계는 되돌리기 어렵다.

flowchart LR
  A[분해 기준
도메인 · 데이터 · 스케일 · 장애] --> B[서비스 경계] B --> C[통신 패턴
동기 / 비동기] B --> D[데이터 일관성] C --> E[운영 비용] D --> E

분해 기준

서비스를 분할한다는 것은 어떤 차이를 경계 삼느냐의 문제다. 같은 코드라도 도메인을 기준으로 분할하면 한 모양이 나오고, 데이터 소유권으로 분할하면 다른 모양이, 스케일 특성으로 분할하면 또 다른 모양이 나온다. 어느 기준이 옳다고 말할 수는 없다. 이 시스템에서 어느 기준이 지배적인지를 먼저 본다.

도메인 경계

DDD 의 Bounded Context 가 그대로 부합한다. “주문”, “결제”, “추천” 같은 업무 의미 단위가 서비스 경계와 일치한다. 변경의 응집성이 좋다. 주문 로직이 바뀌면 주문 서비스만 바뀐다.

약점은 도메인이 흐릿할 때 드러난다. 모델이 충분히 안정되지 않은 상태에서 경계를 그으면 잘못된 모델이 그대로 서비스 경계가 된다. 한 도메인이 두 서비스에 분산되거나, 두 도메인이 한 서비스에 묶이거나. 그 후로는 대부분의 변경이 두 서비스를 동시에 수정하게 된다.

단일 코드베이스 안의 수직 분할을 서비스 경계까지 확장한 형태로 보면 된다.

데이터 소유권

누가 어떤 테이블의 쓰기 권한을 가지느냐. 공유 DB 를 허용하는 순간 MSA 의 핵심 이점인 독립 배포와 독립 스키마 진화가 사라진다. 한 서비스의 스키마 변경이 다른 서비스의 코드를 깨뜨릴 수 있기 때문이다.

이 기준은 도메인 경계와 흔히 겹친다. 한 도메인이 자기 데이터를 소유하는 게 자연 흐름이다. 그런데 같은 도메인 안에서도 쓰기 패턴이 다르면 데이터 소유권이 별도의 분해 기준이 된다. 주문 도메인 안에서도 트랜잭션 처리와 통계 집계는 부하와 일관성 요구가 다르다.

스케일 패턴

워크로드 특성으로 분할한다. 같은 도메인 안에서도 읽기와 쓰기, CPU 와 IO, 폭증형과 평탄형의 비중이 갈리면 분할이 적합하다.

채팅 워크로드를 예로 들면, 메시지 발행은 쓰기 무거움 + 폭증형이고 메시지 검색은 읽기 무거움 + IO 무거움이다. 한 서비스로 묶으면 두 패턴 중 어느 쪽에 맞춰도 다른 쪽이 비효율이 된다. 분할하면 각자 자기에게 맞는 방식으로 스케일할 수 있다. 발행은 큐로 흡수하고, 검색은 인덱스와 캐시로 처리한다.

장애 경계

한 서비스의 장애가 다른 서비스에 번지지 않게 분할한다. 핵심 경로와 비핵심 경로를 분리한다.

광고 서버 워크로드를 예로 들면, 메인 추천이 장애를 일으키면 fallback 콘텐츠가 나가야 매출이 보호된다. 메인과 fallback 을 한 서비스에 두면 메인의 장애가 fallback 까지 함께 중단시킨다. 분리하면 fallback 은 자기 경로로 살아남는다. 안정성 관점의 분해 기준이다.

기준이 충돌할 때

도메인 경계로 분할하려는데 스케일 패턴이 다른 분할을 요구하는 경우가 흔하다. 한 도메인이 폭증형 워크로드와 평탄형 워크로드를 모두 가질 때 도메인 통합과 스케일 분리가 충돌한다.

이 시스템에서 지배적인 기준을 정하고 그것으로 분할한 뒤, 다른 기준은 서비스 내부의 모듈이나 큐 분리로 처리한다. 모든 기준을 서비스 경계로 끌어올리면 서비스 수가 폭증하고 운영 비용이 통제 불가능해진다.

서비스 간 통신

서비스 경계가 정해지면 다음 결정은 어떻게 통신할 것인가다. 동기 또는 비동기다.

동기 — gRPC

명령형, 즉시 응답이 필요한 경우. 결제 요청, 인증 확인 같은 호출이 여기에 해당한다. 호출자는 결과를 기다리고, 실패하면 즉시 안다.

gRPC 는 HTTP/2 기반으로 ProtoBuf 라는 IDL 로 양방향 계약을 정의한다. unary, server streaming, client streaming, bidirectional streaming 의 4가지 모드를 지원하고, ProtoBuf 의 이진 직렬화 덕에 JSON 보다 가볍다. HTTP/1.1 의 head-of-line blocking 을 HTTP/2 의 다중화로 해결한다.

동기의 비용은 호출 체인이 길어질수록 누적되는 지연이다. 한 곳이 중단되면 호출 체인 전체가 영향받는다. 그래서 동기 통신은 호출 체인을 짧게 유지하고 핵심 경로에 한정하는 편이 권장된다.

비동기 — Kafka

이벤트 발행, 결과적 일관성, 트래픽 흡수가 필요한 경우. 주문이 생기면 추천 서비스가 그 이벤트를 받아 모델을 업데이트하거나, 사용자 행동 로그가 큐에 쌓이면 분석 서비스가 자기 페이스로 처리하는 식이다.

Kafka 는 분산 로그다. producer 가 topic 에 이벤트를 적고, consumer 가 자기 offset 으로 읽는다. 같은 이벤트를 여러 consumer 가 다른 목적으로 소비할 수 있다(fan-out). 일시적인 트래픽 폭증을 큐가 흡수하므로 downstream 의 부하가 평탄해진다.

비동기의 비용은 즉시가 아닌 일관성이다. 이벤트가 도착할 때까지 짧지만 빈 시간이 있고, consumer 가 중단되거나 느려지면 lag 이 쌓인다.

어느 길을 택할 것인가

분해 기준이 답을 준다.

  • 도메인 경계로 갈랐는데 두 도메인이 서로의 즉시 결과에 의존한다면 동기다.
  • 데이터 소유권으로 갈랐고 한 서비스의 변경이 다른 서비스의 캐시나 뷰를 갱신해야 한다면 비동기 이벤트가 자연스럽다.
  • 스케일 패턴으로 갈랐고 폭증 트래픽을 흡수해야 한다면 비동기 큐다.
  • 장애 격리로 갈랐고 핵심 경로와 비핵심 경로를 분리했다면, 핵심은 동기로 두고 비핵심은 비동기로 빼서 fallback 을 가능하게 한다.

대부분의 실제 시스템은 두 가지를 섞는다. 통신 방식 하나만 고집하는 시스템은 보통 분해 기준을 한 가지로만 보고 있을 가능성이 크다.

데이터 일관성과 운영 도구

단일 트랜잭션의 상실

모놀리스에서 익숙했던 단일 ACID 트랜잭션이 사라진다. “결제와 재고 차감을 하나로 묶는다” 가 서비스 경계 너머에서는 자연스럽지 않다.

분산 환경의 트랜잭션은 두 방향으로 나뉜다. 동기적으로 분산 트랜잭션을 시도하는 길(2PC, TCC) 과 결과적 일관성을 받아들이고 보상 트랜잭션을 설계하는 길(Saga, Outbox) 이다. 어느 쪽도 단일 트랜잭션의 단순함을 회복하지 못한다.

서비스 경계를 그을 때 단일 트랜잭션이 깨지는 자리를 미리 의식해야 한다. 가장 자주 함께 변하는 데이터를 경계 너머로 분산시키면 모든 쓰기가 분산 트랜잭션이 되고, 그 비용은 무시할 수 없다. 분해 기준이 통신뿐 아니라 데이터 일관성 모델까지 결정하는 셈이다.

운영 도구가 필요해지는 자리

서비스 수가 늘면 새로운 도구가 필요해지는 자리가 생긴다.

  • Service Mesh: 서비스 간 통신 정책(retry, timeout, circuit breaker, mTLS) 을 코드 밖으로 빼고 싶을 때
  • API Gateway / BFF: 외부 진입점에서 인증, rate limiting, 응답 조립을 한곳에 모으고 싶을 때
  • 분산 추적: 호출 체인이 길어져 한 요청이 어디서 느려졌는지 추적하기 어려울 때
  • 컨테이너 오케스트레이션: 서비스 단위가 많아져 배포와 스케일링을 자동화하고 싶을 때

이 도구들은 결과로 뒤따르지 전제는 아니다. 분해 기준이 분명하고 서비스 경계가 단순하면 도구 의존을 늦출 수 있다. 기준이 흐릿한 상태에서 도구부터 만들면 복잡도를 가시화하지 못한 채 누적시킨다.

잘못된 분해의 비용

분리는 되돌리기 어렵다. 한 번 다른 서비스로 분리되면 그 코드는 자기 데이터, 자기 배포 파이프라인, 자기 모니터링, 자기 팀 의존성을 따로 갖게 된다. 다시 합치려면 이 모든 것을 거꾸로 풀어야 한다.

분해 기준이 잘못 잡힌 시스템에는 흔히 두 가지 신호가 보인다. 대부분의 변경이 여러 서비스의 동시 배포를 요구하면 도메인 경계가 잘못 그어졌다는 뜻이고, 대부분의 호출이 동기 체인 끝까지 이어지면 통신 결정이 분해 기준이 아니라 관성에 따라 내려졌다는 뜻이다.

단일 코드베이스 안의 분할 결정도 같은 원리지만, MSA 는 그 결정을 되돌리기 어려운 형태로 고정한다.

그래서 의심스러우면 자르지 않는 편이 낫다고 본다. 모놀리스 안에서 모듈 경계를 먼저 분명히 그어보고, 그 경계가 충분히 안정되었을 때 자른다. 도메인이 안정되고 데이터 소유권이 분명해지고 스케일 차이가 운영을 어렵게 만드는 시점이다. 자르는 것이 목적이 되면 분해 기준은 사후 정당화가 된다.

참고