[21-cloud-native] 02. 마이크로 서비스 아키텍처

suhyen·2026년 3월 23일

2026-TIL

목록 보기
5/15
post-thumbnail

마이크로서비스란 무엇인가?

마이크로서비스 아키텍처(MSA)는 하나의 애플리케이션을 작고 느슨하게 결합된(loosely coupled) 독립 서비스들의 집합으로 구성하는 아키텍처 스타일이다.

여기서 "느슨하게 결합"이라는 표현이 핵심이다. 서비스들이 서로를 알긴 하지만, 내부 구현이나 데이터 구조를 직접 공유하지는 않는다. 마치 레고 블록처럼, 각 블록은 독립적으로 존재하면서도 정해진 연결 규격(API/이벤트)을 통해 하나의 완성품을 만든다.

MSA는 컨테이너, Kubernetes, CI/CD, DevOps와 결합되면서 클라우드 네이티브 아키텍처의 핵심 스타일로 자리잡았다. 이 조합이 강력한 이유는, MSA가 제시하는 "서비스를 분리하라"는 설계 원칙이 컨테이너의 "가볍게 패키징하라", Kubernetes의 "자동으로 관리하라", CI/CD의 "빠르게 배포하라"와 자연스럽게 맞닿아 있기 때문이다.


모놀리식 vs 마이크로서비스: 무엇이 다른가?

MSA를 이해하는 가장 좋은 방법은 모놀리식과 비교해 보는 것이다.

구분모놀리식(Monolith)마이크로서비스(MSA)
코드 구조하나의 큰 코드베이스 + 하나의 배포 단위기능 단위로 분리된 여러 서비스, 각각 독립적으로 개발, 배포, 스케일링
초기 장점작은 팀/초기 제품에 단순하고 빠름복잡한 대규모 시스템에 유리
성장 후 문제규모가 커질수록 변경, 배포, 스케일링이 어려워짐운영, 네트워크, 데이터 복잡도 증가
복잡도 이동코드 복잡도 낮음운영 복잡도 높음

모놀리식의 문제는 처음에는 잘 드러나지 않는다. 프로젝트 초반에는 모든 것이 한 곳에 있어서 빠르고 단순하다. 그런데 코드가 수십만 줄이 되고, 팀이 10개 이상으로 늘어나고, 하루에 수백만 명이 접속하게 되면 이야기가 달라진다. 한 팀의 배포가 다른 팀의 서비스에 영향을 주고, 트래픽이 폭발하면 전체를 통째로 스케일 아웃해야 하며, 장애 하나가 전체 시스템을 다운시킨다. MSA는 이 문제에 대한 구조적 해답이다.


마이크로서비스의 4가지 구조적 특징

1. 작은 자율 서비스 (Small, Autonomous Services)

하나의 서비스는 하나의 비즈니스 기능, 즉 하나의 Bounded Context를 담당한다. 주문 서비스는 주문만, 결제 서비스는 결제만 담당하는 식이다. 서비스 크기의 기준은 코드 줄 수가 아니라 "이 서비스가 어떤 비즈니스 문제를 해결하는가" 이다.

2. 느슨한 결합 (Loosely Coupled)

다른 서비스와는 API 또는 이벤트를 통해서만 통신하고, 내부 구현은 완전히 숨긴다. 주문 서비스가 결제 서비스의 내부 DB 구조를 알 필요가 없다. 결제 서비스가 내부적으로 어떤 DB를 쓰든, 어떤 언어로 작성됐든 주문 서비스와는 무관하다. 이 덕분에 각 서비스는 서로 영향 없이 독립적으로 변경될 수 있다.

3. 독립 배포 (Independent Deployment)

서비스별로 버전, 배포 주기, 스케일링 전략이 다를 수 있다. 결제 서비스에 버그가 생겼을 때 결제 서비스만 롤백하면 된다. 모놀리식이라면 전체 시스템을 롤백해야 한다. 독립 배포는 단순히 편의의 문제가 아니라 장애 영향 범위를 제한하는 안전망이다.

4. 기술 이질성 (Polyglot)

서비스별로 다른 언어, 프레임워크, DB를 선택할 수 있다. 실시간 스트리밍 처리가 필요한 서비스는 Go로, ML 추론이 필요한 서비스는 Python으로 작성할 수 있다. 물론 이는 각 팀이 그 기술에 대한 책임을 온전히 지겠다는 전제 하에서다.


마이크로서비스 아키텍처의 구성 요소

실제 MSA 시스템은 다음 컴포넌트들이 계층적으로 결합되어 동작한다.

MSA 아키텍처

  • API 게이트웨이: 외부 세계와 내부 서비스 간의 단일 진입점. 인증 및 인가, Rate Limit, 로깅, 요청 조합을 담당한다.
  • 서비스 레지스트리 & 디스커버리: 동적으로 생성되거나 삭제되는 서비스 인스턴스를 추적하고, 서비스들이 서로를 이름으로 찾을 수 있게 한다.
  • 데이터 저장소(서비스별 DB): 각 서비스는 자신의 DB만 소유한다. 모놀리식의 공유 DB 대신 분산된 데이터 소유권을 가진다.
  • 관측성 인프라: 수십~수백 개의 서비스가 동시에 동작하는 환경에서 장애를 추적하려면 로그, 메트릭, 트레이싱 체계가 반드시 필요하다.

서비스 경계 설계: MSA의 가장 어려운 부분

MSA를 처음 도입할 때 가장 많이 하는 실수는 "어떻게 쪼갤지"에 집중하는 것이다. 올바른 질문은 "어디서 쪼갤지"다.

DDD + Bounded Context

DDD(Domain-Driven Design) 는 소프트웨어의 복잡성을 도메인(비즈니스 영역)에 집중하여 해소하는 설계 방법론이다. 핵심 개념인 Bounded Context는 특정 도메인 모델이 일관적으로 적용되는 경계를 의미한다.

예를 들어 "사용자(User)"라는 개념은 도메인마다 다르게 정의된다.

  • 결제 도메인에서의 사용자 → 카드 소유자, 청구 주소, 결제 수단
  • 배송 도메인에서의 사용자 → 수령인, 배송지 주소, 연락처
  • 마케팅 도메인에서의 사용자 → 구매 이력, 선호 카테고리, 세그먼트

같은 "사용자"이지만 각 도메인에서 관심 있는 속성과 행동이 전혀 다르다. 이처럼 개념이 의미 있게 유지되는 경계를 기준으로 서비스를 분리하는 것이 Bounded Context 기반 설계다.

  • 좋은 기준: 주문, 상품, 회원, 결제, 배송, 정산, 알림 등 비즈니스 기능 단위
  • 나쁜 기준(안티패턴): Controller, Service, Repository 같은 기술 레이어 단위로 분리

기술 레이어로 분리하면 OrderController가 하나의 서비스, OrderService가 또 다른 서비스가 된다. 이렇게 하면 결국 API 호출로만 연결된 모놀리식이 된다. 서비스가 많아졌지만 복잡도는 오히려 더 늘어난다.

💡 Callout: DDD(도메인 주도 설계) 더 알아보기 (Aggregate, Domain Event, Context Map 항목은 강의자료 외 내용)

DDD는 소프트웨어의 복잡성을 도메인(비즈니스 영역)에 집중하여 해소하는 설계 방법론이다. MSA 설계에서 특히 중요한 개념들은 다음과 같다:

  • Bounded Context: 특정 도메인 모델이 유효한 경계. MSA 서비스 경계와 일치시키는 것이 이상적이다.
  • Aggregate: 일관성을 유지해야 하는 객체들의 묶음. 하나의 트랜잭션 단위가 된다.
  • Domain Event: 도메인에서 일어난 중요한 사건. 서비스 간 비동기 통신의 기반이 된다.
  • Context Map: 여러 Bounded Context 간의 관계를 그린 지도. 어떤 서비스가 어떻게 협력하는지를 시각화한다.

통신 패턴: 동기 vs 비동기

서비스들이 서로 통신하는 방식은 크게 두 가지다. 어느 방식을 선택하느냐에 따라 시스템의 특성이 크게 달라진다.

동기 통신 (Synchronous)

REST, gRPC 등 요청-응답 방식. 요청을 보내고 응답이 올 때까지 기다린다.

간단하고 직관적이지만, 서비스 체인이 길어질수록 문제가 생긴다. A → B → C → D 순서로 동기 호출이 이어진다면, D가 느리면 C가 느려지고, C가 느리면 B가 느려지고, B가 느리면 A까지 느려진다. 마치 고속도로에서 앞차가 막히면 뒤의 모든 차가 막히는 것과 같다.

비동기 통신 (Asynchronous)

메시지 브로커(Kafka, RabbitMQ) 기반 이벤트 전파. 요청을 보내고 응답을 기다리지 않는다.

A는 이벤트를 브로커에 발행하고 바로 자신의 일을 계속한다. B, C, D는 각자의 속도에 맞춰 이벤트를 처리한다. 한 서비스가 느리거나 장애가 나도 전체 흐름이 멈추지 않는다. 느슨한 결합과 장애 격리라는 MSA의 이점을 극대화하는 방식이다.

전략적 선택

현실에서는 두 방식을 목적에 따라 혼합하는 것이 일반적이다.

  • 즉시 응답이 필요한 핵심 플로우 (예: 로그인 확인, 재고 조회): 동기 + Circuit Breaker
  • 결과를 나중에 처리해도 되는 후속 작업 (예: 알림 발송, 통계 집계, 로그 처리): 비동기 이벤트

Database per Service: 데이터도 분리해야 진짜 MSA다

서비스를 분리했지만 DB가 하나로 공유되어 있다면, 그것은 가짜 MSA다. 진짜 MSA는 각 서비스가 자신의 DB를 소유한다.

핵심 원칙: 다른 서비스의 데이터가 필요하다면, 그 서비스의 DB에 직접 접근하는 것이 아니라 해당 서비스의 API를 통해 요청해야 한다.

장점

  • 각 서비스의 데이터 스키마가 내부 구현에 캡슐화된다. A 서비스의 스키마 변경이 B 서비스에 영향을 주지 않는다.
  • 서비스마다 최적의 DB를 선택할 수 있다. 검색 서비스는 Elasticsearch, 캐시 서비스는 Redis, 트랜잭션이 중요한 서비스는 PostgreSQL.

단점과 해결

  • Cross-service Join이 불가능하다. 여러 서비스의 데이터를 합쳐서 보여줘야 하는 화면은 어떻게 처리할까? 이 문제를 해결하기 위해 API Composition, CQRS, 읽기 전용 조회 DB 등의 패턴이 등장한다.

Saga 패턴: 분산 트랜잭션 문제의 해결책

MSA에서 가장 까다로운 문제 중 하나는 트랜잭션이다. 주문, 결제, 재고 차감이 각각 다른 서비스에 있을 때, 하나가 실패하면 어떻게 모두를 롤백할 것인가?

기존의 2PC(Two-Phase Commit) 방식은 분산 환경에서 성능 문제와 교착 상태 위험이 있어 클라우드 네이티브에 적합하지 않다. 대신 Saga 패턴을 사용한다.

Saga 패턴: 각 서비스가 로컬 트랜잭션을 처리하고, 실패 시 이미 완료된 단계들을 되돌리는 보상(Compensation) 트랜잭션을 실행하는 방식이다. 강한 일관성(Strong Consistency) 대신 최종 일관성(Eventual Consistency) 을 추구한다.

예를 들어 주문 프로세스가 실패한 경우:
1. 주문 생성 ✅ → 결제 처리 ✅ → 재고 차감 ❌ (실패)
2. 재고 차감 실패를 감지
3. 결제 취소 보상 트랜잭션 실행 ↩️
4. 주문 취소 보상 트랜잭션 실행 ↩️

💡 Callout: Saga의 두 가지 구현 방식

Choreography (코레오그래피)
이벤트 기반. 각 서비스가 이전 서비스의 이벤트를 받아 자율적으로 다음 단계를 처리한다. 중앙 조율자가 없어서 서비스 간 결합도가 낮다. 하지만 전체 흐름이 코드 여러 곳에 분산되어 있어 추적이 어렵고, 새로운 서비스를 추가할 때 기존 서비스들이 어떤 이벤트를 발행하는지 알아야 한다.

Orchestration (오케스트레이션)
Saga Orchestrator라는 중앙 조율자가 각 서비스를 순서대로 호출하고, 실패 시 보상 트랜잭션을 명시적으로 호출한다. 전체 흐름이 한 곳에 정의되어 있어 이해하기 쉽다. 단, Orchestrator가 단일 장애점(SPOF)이 될 수 있고 Orchestrator 자체가 복잡해진다.

도메인 복잡도가 낮고 서비스 수가 적으면 Choreography, 흐름이 복잡하고 명확한 제어가 필요하면 Orchestration이 유리하다.


관측성, 보안, 인프라: MSA를 떠받치는 기반

인프라 – 컨테이너 & Kubernetes

MSA의 각 서비스를 수동으로 배포하고 관리하는 것은 불가능하다. 컨테이너와 Kubernetes가 이 문제를 해결한다.

  • 컨테이너(Docker): 서비스별 독립된 실행 환경. 이미지 기반 배포로 환경 일관성 확보.
  • Kubernetes: Pod, Deployment, Service, Ingress 리소스로 마이크로서비스를 관리. 서비스 디스커버리, 오토스케일링, 롤링 업데이트를 자동화.
  • 클라우드 플랫폼: EKS/AKS/GKE, Istio, Prometheus/Grafana 등과 자연스럽게 통합.

관측성 – Logs, Metrics, Traces

수십 개의 서비스가 동시에 동작할 때 장애가 발생하면, 어느 서비스에서 문제가 시작됐는지 파악하는 것이 우선이다. 이를 위해 세 가지 관측 수단이 필요하다.

  • Logs: 각 서비스는 stdout/stderr에 JSON 구조화 로그를 출력. ELK, Loki 등에서 중앙 수집.
  • Metrics: QPS, 응답 시간, 에러율 등 수치 지표. Prometheus + Grafana.
  • Distributed Tracing: 요청이 서비스 A → B → C → D를 거치는 전체 경로를 추적. Jaeger, Zipkin, OpenTelemetry.
  • Correlation ID: 하나의 요청에 고유 ID를 부여하고 모든 로그/트레이스에 포함. 요청을 end-to-end로 추적하는 기반이 된다.

보안

  • 인증/인가: API Gateway/BFF에서 JWT, OAuth2/OIDC로 처리. 서비스 간 권한은 서비스 메시 + mTLS + 정책으로 관리.
  • Zero Trust: 서비스 간 트래픽도 "기본적으로 신뢰하지 않는다"는 원칙. 내부 네트워크라도 인증을 거쳐야 한다.
  • 거버넌스: 공통 로깅 포맷, 에러 규격, API 버저닝 전략, SLO/SLI/SLA 표준화.

팀 구조와 DevOps: MSA는 기술만의 문제가 아니다

MSA의 성공 여부는 기술보다 조직 구조에 달려 있다. 소프트웨어의 구조는 그것을 만든 조직의 커뮤니케이션 구조를 반영한다. MSA가 성공하려면 팀 구조도 MSA처럼 바뀌어야 한다.

  • 팀 구조: "2 pizza team" 수준의 작은 팀이 서비스 하나를 end-to-end로 책임진다. 개발, 테스트, 배포, 운영을 한 팀이 모두 담당한다.
  • DevOps: 개발팀과 운영팀의 벽을 허물고, CI/CD 파이프라인과 모니터링을 개발팀이 직접 관리한다.
  • 배포 방식: 짧은 배포 주기(데일리/주간), 자동화된 테스트, Canary/Blue-Green 배포.

MSA의 장단점 정리

MSA는 강력한 아키텍처지만, 모든 상황에 맞는 은탄환은 아니다.

장점

  • 모듈성: 서비스 단위 코드/도메인 분리 → 유지보수성 향상
  • 독립 배포/스케일: 장애, 업데이트, 스케일링을 서비스 단위로 격리
  • 기술 다양성: 서비스마다 최적 기술 스택 선택 가능
  • 조직 민첩성: 소규모 팀이 병렬로 빠르게 개발

단점

  • 분산 시스템 복잡도: 네트워크 지연, 장애 전파, 데이터 일관성 관리가 어려워진다
  • 플랫폼 요구 수준: Kubernetes, CI/CD, Observability 등 인프라 역량이 없으면 운영이 불가능하다
  • 테스트 복잡도: 단위 테스트 외에 서비스 간 계약(Contract) 테스트, E2E 테스트가 필수
  • 조직 성숙도: DevOps 문화와 팀 자율성이 없으면 오히려 더 느려진다

언제 MSA를 선택해야 하는가?

적합한 상황

  • 트래픽이 빠르게 증가하는 제품
  • 여러 팀이 동시에 다른 기능을 개발해야 하는 상황
  • 도메인이 복잡하고 기능별 경계가 명확할 때

신중해야 할 상황

  • 초기 스타트업, 소규모 팀, 단순한 비즈니스 로직
  • 도메인이 아직 정립되지 않은 프로젝트 초기
  • Kubernetes, CI/CD 등 인프라 운영 역량이 아직 부족할 때

현실적인 접근법
처음부터 MSA로 시작하는 것은 오히려 독이 될 수 있다. 초기에는 내부적으로 모듈화가 잘 된 모듈형 모놀리식(Modular Monolith) 으로 시작하는 것이 현명하다. 도메인 경계가 명확해지고, 팀이 커지고, 트래픽이 실제로 문제가 될 때 점진적으로 분리하면 된다.


요약: MSA는 "코드를 쪼개는 기술"이 아니다. 도메인, 데이터, 팀, 플랫폼을 모두 아우르는 종합적인 아키텍처 스타일이다. 기술보다 설계 철학을 먼저 이해해야 한다.


0개의 댓글