컴퓨터공학 300 주제 시리즈의 199번째 글이다. 전체 지도는 여기.

한 줄 요약

모놀리스는 하나의 배포 단위로 묶인 애플리케이션이고, 마이크로서비스는 업무 능력별로 나뉜 작은 서비스들이 각자 독립적으로 배포되고 네트워크로 협력하는 구조다. 마이크로서비스는 독립 배포와 팀 자율성을 주는 대신 분산 시스템의 복잡성을 청구한다. 어느 쪽이 낫다는 답은 없고, 그 청구서를 감당할 이유가 있느냐가 기준이다.

왜 필요한가

서비스 초기에는 코드 하나, 배포 하나, DB 하나가 가장 빠르다. 그런데 개발자가 수십 명으로 늘면 문제가 생긴다.

  • 결제팀의 작은 수정을 배포하려면 전체 애플리케이션을 다시 빌드·배포해야 하고, 다른 팀의 미완성 코드와 일정이 엉킨다.
  • 검색 기능 하나 때문에 메모리를 늘려야 하는데 애플리케이션 전체를 통째로 키워야 한다.
  • 한 모듈의 메모리 누수가 프로세스 전체를 죽인다.

마이크로서비스는 이런 문제의 해법으로 인기를 얻었다. 하지만 “요즘은 다 마이크로서비스” 라는 이유로 세 명짜리 팀이 서비스 열두 개를 운영하다가 기능 개발보다 서비스 간 통신 디버깅에 시간을 더 쓰는 일도 흔하다. 무엇을 얻고 무엇을 내주는지 정확히 알아야 한다.

핵심 개념

모놀리스

┌──────────────── 하나의 프로세스, 하나의 배포 ────────────────┐
│  주문 모듈   결제 모듈   회원 모듈   검색 모듈   알림 모듈     │
│      └───── 함수 호출 ─────┘                               │
└───────────────────────────┬─────────────────────────────────┘
                         하나의 DB
장점 단점
개발·테스트·디버깅이 단순하다(스택 트레이스 하나) 규모가 커지면 빌드·배포가 느려진다
모듈 간 호출이 빠르고 실패하지 않는다(함수 호출) 한 부분만 따로 확장하기 어렵다
트랜잭션 하나로 일관성을 지킨다 모듈 경계가 무너지기 쉽다(아무나 아무 테이블을 읽는다)
운영 대상이 하나다 기술 스택을 부분적으로 바꾸기 어렵다

모놀리스가 곧 “엉망인 코드” 는 아니다. 내부를 모듈로 잘 나누고 모듈 경계를 지키는 모듈러 모놀리스는 마이크로서비스의 많은 장점을 분산 비용 없이 얻는다. 앞의 레이어드 아키텍처 글에서 본 의존 규칙 검사를 모듈 경계에도 걸면 된다.

마이크로서비스

James Lewis 와 Martin Fowler 의 Microservices(2014)는 마이크로서비스 아키텍처를 작은 서비스들의 모음으로 하나의 애플리케이션을 만드는 방식으로 설명한다. 각 서비스는 자기 프로세스에서 돌고, HTTP API 같은 가벼운 방식으로 통신하며, 업무 능력을 중심으로 만들어지고, 자동화된 배포로 독립적으로 배포된다. 이 글이 꼽은 특징 중 핵심은 다음과 같다.

  • 서비스를 통한 컴포넌트화: 라이브러리가 아니라 독립 배포 가능한 서비스가 부품이다.
  • 업무 능력 중심 조직: 기술 계층(UI 팀, DB 팀)이 아니라 업무 능력(주문, 결제)별로 팀과 서비스를 나눈다.
  • 분산된 데이터 관리: 서비스마다 자기 데이터를 갖는다. 다른 서비스의 DB 를 직접 읽지 않는다.
  • 실패를 전제로 한 설계: 다른 서비스는 언제든 느려지거나 죽을 수 있다.
   [API 게이트웨이]
     │     │      │
     ▼     ▼      ▼
  [주문]─▶[결제]  [검색]       각 서비스: 독립 배포, 자기 DB
     │      │      │
   (DB)   (DB)  (검색 인덱스)
     └──── 이벤트 브로커 ────┘

치르는 값: 분산 시스템의 세금

함수 호출이 네트워크 호출로 바뀌면 다음이 모두 새로 생긴다.

문제 설명 흔한 대응
부분 실패 결제 서비스만 죽을 수 있다 타임아웃, 재시도, 서킷 브레이커
지연 호출마다 네트워크 왕복 호출 수 줄이기, 캐시, 비동기화
데이터 일관성 서비스 간 트랜잭션이 없다 사가(saga), 결과적 일관성, 이벤트
관찰 가능성 요청 하나의 로그가 열 군데에 흩어진다 분산 추적, 상관 ID, 중앙 로그
운영 서비스마다 배포·모니터링·보안 설정 컨테이너 오케스트레이션, 플랫폼 팀
버전 호환 API 를 바꾸면 소비자가 깨진다 하위 호환, 계약 테스트

콘웨이의 법칙

Melvin Conway 는 1968년 글 How Do Committees Invent?에서, 시스템을 설계하는 조직은 그 조직의 의사소통 구조를 닮은 설계를 만들어 낼 수밖에 없다고 썼다. 마이크로서비스 논의에서 이 관찰은 양방향으로 쓰인다. 팀이 하나라면 서비스를 열 개로 나눠도 결국 하나처럼 엮여 움직이고, 서비스 경계를 원한다면 팀 경계를 먼저 그에 맞춰야 한다. 앞 글의 바운디드 컨텍스트가 팀과 서비스를 함께 나누는 좋은 기준이 된다.

모놀리스 먼저

Fowler 는 MonolithFirst에서, 그가 들은 성공적인 마이크로서비스 사례는 거의 모두 너무 커진 모놀리스를 쪼개는 데서 시작했고, 처음부터 마이크로서비스로 시작한 경우는 심각한 어려움을 겪는 경우가 많았다고 관찰한다. 경계를 정확히 긋으려면 도메인을 잘 알아야 하는데, 새 제품 초기에는 그 지식이 없다. 경계를 잘못 그은 마이크로서비스는 고치기가 모놀리스 안의 모듈 경계보다 훨씬 비싸다.

이미 큰 모놀리스를 옮길 때는 스트랭글러 무화과(Strangler Fig) 방식이 흔하다. 기존 시스템 앞에 라우팅 층을 두고, 기능을 하나씩 새 서비스로 옮겨 그쪽으로 트래픽을 돌린다. 한 번에 갈아엎는 대신 점진적으로 교체한다.

비교 요약

기준 모놀리스(모듈러) 마이크로서비스
팀 규모 한두 팀 여러 자율 팀
배포 전체를 함께 서비스별 독립
확장 전체를 함께 서비스별
일관성 트랜잭션으로 쉽게 사가·이벤트로 어렵게
장애 격리 약하다 강하게 만들 수 있다(설계가 필요)
운영 복잡도 낮다 높다(플랫폼 투자 필요)

직접 해 보기

분산의 세금을 숫자로 보자. 두 가지를 계산한다. 첫째, 요청 하나가 여러 서비스를 차례로 거쳐야 할 때의 가용성. 둘째, 요청 하나가 여러 서비스를 병렬로 부르고 모두 기다릴 때 느린 응답이 사용자에게 전파될 확률.

import random

# 1) 직렬 호출 사슬의 가용성: 요청 하나가 N 개 서비스를 차례로 모두 거쳐야 성공한다
per_service = 0.999                      # 각 서비스 가용성 99.9% (가정)
print("거치는 서비스 수  전체 가용성  월 기준 불가 시간(30일)")
for n in (1, 3, 5, 10, 20):
    a = per_service ** n
    print(f"{n:>8}          {a:8.3%}     {(1 - a) * 30 * 24 * 60:6.0f}분")

# 2) 꼬리 지연의 증폭: 한 요청이 N 개 서비스를 병렬로 부르고 모두 기다린다(fan-out)
rng = random.Random(1)
def one_call():
    # 대부분 10ms, 1% 확률로 500ms 걸린다고 가정
    return 500 if rng.random() < 0.01 else 10

def p_slow(fanout, trials=20000):
    slow = sum(max(one_call() for _ in range(fanout)) >= 500 for _ in range(trials))
    return slow / trials

print("\n병렬 호출 수  사용자 요청이 느려질 확률(모의 실험)  이론값 1-(0.99)^N")
for n in (1, 10, 50, 100):
    print(f"{n:>8}      {p_slow(n):8.1%}                       {1 - 0.99 ** n:6.1%}")

실행 결과:

거치는 서비스 수  전체 가용성  월 기준 불가 시간(30일)
       1           99.900%         43분
       3           99.700%        129분
       5           99.501%        216분
      10           99.004%        430분
      20           98.019%        856분

병렬 호출 수  사용자 요청이 느려질 확률(모의 실험)  이론값 1-(0.99)^N
       1          1.1%                         1.0%
      10          9.6%                         9.6%
      50         39.2%                        39.5%
     100         63.1%                        63.4%

각 서비스가 99.9% 로 꽤 안정적이어도, 요청이 10개 서비스를 차례로 거치면 전체 가용성은 약 99.0% 로 떨어진다. 한 달에 43분이던 불가 시간이 7시간 넘게 늘어난다(서비스 장애가 서로 독립이라는 단순한 가정에서). 모놀리스 안의 함수 호출이었다면 이 곱셈은 없다.

두 번째 표는 Jeffrey Dean 과 Luiz André Barroso 의 “The Tail at Scale” 이 지적한 현상이다. 개별 호출이 100번에 한 번만 느려도, 요청 하나가 100개를 병렬로 부르면 사용자 요청의 약 63% 가 느려진다. 그래서 마이크로서비스에서는 평균 지연이 아니라 꼬리 지연(p99) 을 관리하고, 호출 수를 줄이고, 타임아웃과 대체 응답을 설계한다.

현업에서는

  • “분산 모놀리스” 가 최악이다. 서비스는 나눴는데 배포는 항상 같이 해야 하고, 서로의 DB 를 직접 읽는다면 모놀리스의 단점과 분산 시스템의 단점을 동시에 갖는다. 독립 배포가 안 되면 마이크로서비스가 아니다.
  • 플랫폼이 먼저다. 서비스가 늘면 배포 자동화, 중앙 로그, 분산 추적, 서비스 디스커버리가 없이는 운영할 수 없다. 쿠버네티스는 이 중 배포·확장·디스커버리를 표준화해 주는 플랫폼이다. 하지만 쿠버네티스 자체도 운영 대상이라는 점을 잊지 말자.
  • 홈랩은 좋은 실험장이다. 홈랩 k3s 클러스터에서 작은 앱을 서비스 두세 개로 나눠 띄워 보면, 서비스 하나를 일부러 죽였을 때 다른 서비스가 어떻게 반응하는지, 타임아웃이 없으면 요청이 얼마나 오래 매달리는지를 직접 볼 수 있다. 책으로 읽는 분산의 세금이 체감된다.
  • 작게 쪼개는 것이 목표가 아니다. “마이크로” 는 크기 경쟁이 아니다. 한 팀이 독립적으로 이해·변경·배포할 수 있는 크기가 기준이다.

확인 문제

  1. Lewis 와 Fowler 가 꼽은 마이크로서비스의 특징 중 세 가지를 들라.
  2. 모듈러 모놀리스란 무엇이며, 어떤 장점이 있는가?
  3. 콘웨이의 법칙을 한 문장으로 설명하고, 마이크로서비스 설계에 주는 함의를 쓰라.
  4. 각 서비스 가용성이 99.9% 이고 요청이 5개 서비스를 차례로 거칠 때 전체 가용성은 대략 얼마인가?
  5. 스트랭글러 무화과 방식으로 모놀리스를 옮기는 과정을 설명하라.

풀이

  1. 서비스를 통한 컴포넌트화, 업무 능력 중심 조직, 분산된 데이터 관리, 인프라 자동화, 실패를 전제로 한 설계 등에서 셋.
  2. 하나의 배포 단위이지만 내부를 명확한 모듈로 나누고 모듈 경계를 지키는 구조다. 분산 시스템의 비용(네트워크 실패, 분산 트랜잭션) 없이 경계와 독립성의 이점을 상당 부분 얻고, 나중에 경계를 따라 서비스로 떼어 내기도 쉽다.
  3. 시스템 설계는 그것을 만든 조직의 의사소통 구조를 닮는다. 서비스 경계를 원하는 대로 만들려면 팀 경계를 그에 맞춰야 하고, 팀 구조를 바꾸지 않은 채 서비스만 나누면 서비스들이 결국 하나처럼 엮인다.
  4. 0.999^5 ≈ 99.5%.
  5. 기존 시스템 앞에 라우팅 층을 두고, 기능을 하나씩 새 서비스로 구현한 뒤 해당 경로의 트래픽을 새 서비스로 돌린다. 이를 반복해 옛 시스템의 역할을 점점 줄이고 마지막에 제거한다.

더 읽을거리 (References)