DDD - 마이크로서비스


도메인 주도 설계는 수많은 소프트웨어 개발 수명주기를 다루지만, 모든 소프트웨어 엔지니어링을 다루지는 못한다.
여기에서는 DDD 와 관련있는 다른 방법론과 패턴, 서로 다르면서도 상호 보완적인 MSA 스타일과 DDD 사이의 관계, 그리고 그 접근법이 어떻게 서로 보완하는지에 대해 알아본다.
더 중요한 내용으로 MSA 기반 시스템을 효과적으로 설계하는데 DDD 를 활용하는 방법에 대해 알아본다.


목차


1. 서비스란?

서비스는 미리 정의된 인터페이스를 사용하여 하나 이상의 역량에 접근하기 위한 메커니즘이다.
미리 정의된 인터페이스란 서비스로부터 데이터를 넣고 빼는 모든 메커니즘을 말하는데 여기에는 요청/응답 모델과 같은 동기식이나, 이벤트를 제공하고 사용하는 모델과 같은 비동기식이 있다.

서비스를 통해 들어가고 나오는 데이터는 모두 이 외부에 노출되는 서비스 인터페이스를 통한다.
서비스 퍼블릭 인터페이스는 서비스가 노출하는 기능을 정의하며, 잘 표현된 인터페이스는 서비스가 구현한 기능을 설명하기에 충분하다.


2. 마이크로서비스란?

마이크로서비스는 각자의 마이크로 퍼블릭 인터페이스, 즉 마이크로 프론트 도어를 통해 정의된다.
이 퍼블릭 인터페이스는 외부 시스템이나 다른 서비스들이 해당 서비스를 어떻게 사용할 수 있는지 명확히 보여주는 진입점의 역할을 한다.

또한 서비스의 기능 범위를 작게 유지하면 변경이 필요한 이유도 줄어들어, 서비스의 개발/운영/확장을 더욱 자율적으로 수행할 수 있다.
작고 명확한 인터페이스를 가진 서비스는 아래와 같은 이점을 가진다.

  • 개발팀의 자율성 강화
    • 기능이 명확하게 분리되어 있어 각 서비스를 독립적으로 개발/배포 가능
  • 운영과 확장의 유연성
    • 독립 배포가 가능하므로 서비스별로 개별적으로 스케일 업/다운하거나 장애 대응 가능
  • 서비스 변경의 안정성 확보
    • 다른 서비스에 영향을 주지 않으면서도 내부 로직 변경이나 리팩토링이 수월해짐

마이크로 프론트 도어(Micro Front Door)

외부에 노출되는 API 혹은 엔드포인트
외부 시스템이나 다른 서비스가 해당 서비스를 어떻게 사용할 수 있는지를 정의함

이것은 마이크로서비스가 자신의 DB 를 노출하지 않는 관행도 설명한다.
DB 를 노출해서 서비스의 외부 노출 시스템의 영역의 일부로 만들어버리면 DB 에는 매우 다양한 질의가 실행될 수 있기 때문에 퍼블릭 인터페이스가 매우 거대해진다.


2.1. 서비스형 메서드: 완벽한 마이크로서비스란?

마이크로서비스의 핵심 원칙 중 하나는 기능 단위를 서비스로 분리하는 것이지만, 시스템을 지나치게 작은 단위의 서비스들로 쪼개면 오히려 서비스 외부에 노출되는 인터페이스는 줄어들 수 있지만 전체 기능을 구현하기 위해 각 서비스에 다수의 API 엔드포인트를 만들게 된다.
이는 곧 시스템 전반에 걸쳐 인터페이스의 복잡도 증가로 이어지며, 서비스 간 호출이 잦아지고, 의존성과 통신 비용이 급증할 수 있다.

즉, 작게 나누는 것만이 정답은 아니며, 서비스 분해는 목적이 아니라 수단이다. 적절한 크기와 경계를 고민해야 한다.


2.2. 설계 목표

일부에서는 서비스의 복잡도를 줄이기 위해 서비스를 단일 메서드만 갖도록 쪼개는 극단적인 설계를 하기도 하는데 이는 실제로 아래와 같은 이유로 현실적인 접근이 아니다.

  • 협력에 필요한 퍼블릭 인터페이스의 폭발
    • 단일 메서드를 가진 서비스들끼리는 기능을 완성하기 위해 필연적으로 서로 협력해야 함
    • 이 과정에서 상호 호출을 위한 퍼블릭 인터페이스가 급격히 증가하여 오히려 시스템의 연동 복잡도가 커짐
  • 서비스는 단순해졌지만 전체 시스템은 더 복잡해짐
    • 각 서비스는 단순해보이지만, 결과적으로 전체 시스템의 복잡도 증가
    • 디버깅이나 장애 대응도 훨씬 어려워짐

MSA 의 핵심 목적은 작은 단위의 컴포넌트를 만드는 것이 아니라, 변화에 유연하게 대응할 수 있는 전체 시스템 아키텍처를 설계하는 것이다.
따라서 개별 컴포넌트의 단순화에만 집중하기 보다는 서비스 간 연동, 책임 분리, 배포 전략 등 시스템 전반의 협력 구조까지 고려하는 설계가 중요하다.

즉, 좋은 MSA 는 “적절한 단위로 책임을 나누고, 협력 구조를 단순하게 만드는 것”에 있다.


2.3. 시스템의 복잡성

여기서는 개별 마이크로서비스의 복잡성과 전체 시스템의 복잡성 사이의 관계를 살펴본다.

MSA 설계자는 종종 개별 서비스의 단순화에 집중하는 경향이 있는데 진짜 중요한 것은 시스템 전체의 복잡성, 즉 글로벌 복잡성을 얼마나 잘 통제하느냐이다.

이와 관련하여 글렌포드 마이어스는 그의 저서에서 아래와 같이 강조한다.

복잡성이라는 주제에는 프로그램의 한 부분에 대한 로컬 복잡성을 최소화하려는 단순한 시도보다 더 많은 것이 있다.
더 중요한 복잡성의 유형은 글로벌 복잡성, 즉 시스템의 전체적인 구조에 관한 복잡성이다.
예를 들어 시스템의 주요 요소들이 연관되거나 상호 의존하는 정도이다.

  • 로컬 복잡성
    • 개별 마이크로서비스 내부의 구현 복잡성
    • 코드 구조, 알고리즘, 로직 난이도 등
  • 글로벌 복잡성
    • 전체 시스템 관점에서의 구조적 복잡성
    • 서비스 간 상호 작용, 의존성, 통신 흐름 등

<글로벌 복잡성이 중요한 이유>

  • 서비스는 단순할 수 있지만, 서비스 간 호출 관계가 복잡하면 시스템은 오히려 불안정해짐
  • 배포, 장애 대응, 확장성 등 운영 관점에서의 리스트는 대부분 글로벌 복잡성에서 비롯됨
  • 로컬 복잡성은 개발자의 숙련도에 따라 조절 가능하지만, 글로벌 복잡성은 아키텍처 자체의 설계 철학과 직결됨

즉, 마이크로 서비스는 작게 나누는 것이 전부가 아니라 전체 시스템 구조를 고려한 균형 잡힌 설계가 핵심이다.


2.4. 깊은 서비스로서의 마이크로서비스

여기서는 마이크로 퍼블릭 인터페이스의 개념이 로컬 복잡성과 글로벌 복잡성의 균형 최적화에 어떻게 기여하는지에 대해 알아본다.

모든 시스템에서 모듈은 자신의 함수와 로직에 의해 정의된다.
함수는 모듈이 외부에 제공하는 비즈니스 기능이고, 로직은 그 기능을 구현하는 내부 구조이다.

이 구조를 더 잘 이해하기 위해 존 우스터하우스는 모듈을 사각형으로 시각화하였다.

  • 가로로 넓은 모듈(얕은 모듈)
    • 폭넓은 기능을 제공
    • 퍼블릭 인터페이스가 크고 복잡함
    • 내부 로직이 거의 없고, 퍼블릭 인터페이스와 내부 구현이 동일함
  • 세로로 긴 모듈(깊은 모듈)
    • 제한된 기능을 제공
    • 퍼블릭 인터페이스는 단순하지만 내부 로직은 깊고 강력함
    • 로직의 내포 깊이가 깊어 실제 복잡성을 감추고 추상화가 잘 되어 있음

아래는 극단적으로 얕은 모듈의 예시이다.

fun addTwoNumbers(a: Int, b: Int): Int {
    return a + b
}

위 메서드는 아래와 같은 특징이 있다.

  • 퍼블릭 인터페이스와 내부 로직이 1:1로 일치함
  • 내포된 로직이 없으며, 아무런 추상화도 제공하지 않음
  • 호출하는 쪽에서 로직을 조합해야 하므로, 전체 시스템에 우발적인 복잡성을 전파함

마이크로 퍼블릭 인터페이스는 이런 얕은 설계를 피하고 아래와 같은 장점을 제공한다.

  • 간결한 진입점으로 외부에는 단순한 인터페이스 제공
  • 내부적으로는 복잡한 로직을 추상화하여 숨김
  • 이를 통해 로컬 복잡성은 깊게 유지하고, 글로벌 복잡성은 최소화함

마이크로 퍼블릭 인터페이스는 깊이 있는 모듈을 만드는 출발점이며, 전체 시스템 복잡성을 제어하는 중요한 전략이다.


2.5. 깊은 모듈로서의 마이크로서비스

모듈고 마이크로서비스는 같은 것이 아니다.
마이크로서비스는 물리적인 경계를 의미하고, 모듈은 논리적 경계 뿐 아니라 물리적인 경계까지도 포괄할 수 있다.

얕은 서비스는 시스템의 글로벌 복잡성을 증가시킨다.
단일 비즈니스 메서드만 구현하는 서비스는 얕은 모듈이다. 이런 서비스는 내부 복잡성을 감싸지 않기 때문에 기능을 조합하기 위해 퍼블릭 인터페이스가 점점 넓어진다.
결과적으로 서비스 간 연동 복잡성이 증가하고 전체 시스템은 더 많은 조율과 통합이 필요하게 된다.

즉, 얕은 모듈은 로컬 복잡성은 줄지만 글로벌 복잡성을 급격히 증가시킨다.

얕은 서비스는 MSA 프로젝트가 실패하는 원인이다.
깊은 모듈은 내부의 복잡성을 캡슐화하며 전체 시스템의 구조를 단순하게 유지하지만, 얕은 모듈은 로직이 없기 때문에 복잡성이 외부로 퍼져나가며 전체 시스템을 오염시킨다.
얕은 서비스가 많아지면 아래와 같은 문제가 발생한다.

  • 퍼블릭 인터페이스가 지나치게 많아짐
  • 서비스 간 연동 지점 증가 → 통신 비용 상승
  • 장애 전파 범위 증가
  • 변경 비용이 오히려 증가함

마이크로서비스 분해에는 임계치가 존재한다.
모놀리식 시스템을 마이크로서비스로 분리하면 초기에는 변경 비용이 감소하지만 특정 임계치를 넘어서 계속 분해하면 상황은 역전된다.

  • 서비스가 지나치게 작아짐 → 깊은 서비스가 얕은 서비스로 변질됨
  • 인터페이스는 많아지고 통합 비용은 증가함
  • 결국 시스템은 거대한 분산된 진흙 덩어리로 변함

좋은 MSA 는 분해의 기술이 아니라 경계 설정의 기술이다.
서비스는 얕게 만들수록 더 강한 통합이 필요하고, 결국 복잡성은 시스템 외부로 누수된다.


3. 도메인 주도 설계와 마이크로서비스의 경계

여기서는 깊은 서비스의 경계를 찾는데 DDD 가 어떻게 도움이 되는지에 대해 알아본다.

MSA 도 결국은 경계를 나누는 기술이고, DDD 의 핵심 또한 경계에 있다.

DDD 가 정의하는 경계들

DDD 개념경계 의미관련 아키텍처 수준
바운디드 컨텍스트모델의 경계
동일한 용어라도 의미가 달라질 수 있기 때문에 경계를 분리함
마이크로서비스 설계의 핵심 단위
하위 도메인비즈니스 역량의 경계
핵심, 지원, 일반 하위 도메인으로 나뉘며 조직 구조와 밀접한 연관
팀 조직 및 서비스 분할 기준
애그리거트/밸류 오브젝트트랜잭션의 경계
데이터의 일관성과 응집성을 보장하기 위한 단위
서비스 내부 설계 (Persistence 및 연산 범위)

이 경계들 중 어떤 경계가 마이크로서비스 설계에 가장 도움이 될까?

  • 바운디드 컨텍스트
    • 마이크로서비스의 물리적 경계를 정의하는데 가장 직접적인 연결 고리가 됨
    • 각 바운디드 컨텍스트는 자체적인 모델, 용어, 로직을 가지므로 서비스 간 독립성이 극대화됨
  • 하위 도메인
    • 전체 시스템을 분해할 기준을 제공함
    • 예) 결제 도메인, 회원 도메인 등은 명확한 업무 단위로 쪼갤 수 있는 기준이 됨
  • 애그리거트/밸류 오브젝트
    • 서비스 내부의 복잡성을 관리하는 수단이 됨
    • 복잡한 비즈니스 로직을 내포한 깊은 서비스를 만드는 데 필수적인 설계 단위임

DDD 는 단순히 도메인 모델링을 위한 것이 아니라 마이크로서비스의 경계를 올바르게 나누는 강력한 도구이다.

각 경계 개념을 시각화하면 아래와 같다.

DDD 각 경계 개념

개념하위 도메인바운디드 컨텍스트
정의비즈니스의 기능 영역
예) 결제, 배송, 회원관리
특정 모델/언어/로직이 유효한 모델의 경계
역할시스템을 비즈니스 역량 단위로 분할하나의 모델이 명확하게 유효한 범위를 지정
범위 관계하나의 하위 도메인은 여러 컨텍스트로 나뉠 수 있음하나의 바운디드 컨텍스트는 보통 하나의 하위 도메인에 속함
예시결제 도메인(하위 도메인) 안에 주문 결제, 포인트 결제 등의 바운디드 컨텍스트가 있음주문 결제 컨텍스트, 포인트 정산 컨텍스트

예시를 보면 하위 도메인에 “결제” 가 있고, 바운디드 컨텍스트에는 “카드 결제”, “포인트 결제”, “구독 결제” 등 각각의 독립된 모델과 로직을 가진다.

하위 도메인과 바운디드 컨텍스트의 관계

즉, 하위 도메인 = 비즈니스 영역 기준, 바운디드 컨텍스트 = 소프트웨어 모델 경계 기준이다.
그래서 마이크로서비스 경계로 옮길 때 바운디드 컨텍스트가 더 구체적으로 활용된다.


3.1. 바운디드 컨텍스트

마이크로서비스와 바운디드 컨텍스트는 공통점이 많아 종종 혼용되곤 한다.
이 섹션에서는 바운디드 컨텍스트의 경계가 효과적인 마이크로서비스 경계 설계에 어떻게 기여하는지 살펴본다.

둘 다 물리적인 경계를 가지며, 마이크로서비스는 종종 바운디드 컨텍스트로 구현된다.
하지만 그 반대는 항상 성립하지 않는다. 즉, 모든 바운디드 컨텍스트가 마이크로서비스로 구현될 필요는 없다.

바운디드 컨텍스트는 유비쿼터스 언어와 도메인 모델의 일관성을 보장하기 위한 경계이다.
예를 들어 광고 관리 시스템에서 ‘Lead’ 라는 비즈니스 엔티티는 ‘프로모션 컨텍스트’와 ‘영업 컨텍스트’에서 각기 다른 의미를 가질 수 있다.
이 경우 프로모션과 영업은 각각의 바운디드 컨텍스트 안에서만 유효한 리드 엔티티를 갖게 된다.

만약 Lead 외에 다른 충돌하는 모델이 없다면, 해당 바운디드 컨텍스트는 여러 하위 도메인을 포괄할 수 있어 자연스럽게 더 넓은 경계를 가질 수 있다.
하위 도메인 간에 모델 충돌이 없다면, 아래 그림과 같이 분해한 다양한 바운디드 컨텍스트 모두 유효하다.

바운디드 컨텍스트 분해의 다양한 예시

다만, 위 그림에서 보이듯 유효한 모든 바운디드 컨텍스트가 반드시 마이크로서비스가 되어야 하는 것은 아니다.
특히 바운디드 컨텍스트 #1처럼 비교적 큰 기능을 포함하는 경우에는 더욱 그렇다.

요약하면, 마이크로서비스는 바운디드 컨텍스트일 수 있지만, 모든 바운디드 컨텍스트가 반드시 마이크로서비스여야 하는 것은 아니다.


3.2. 애그리거트

여기서는 애그리거트가 마이크로서비스의 경계를 정의하는데 어떤 역할을 하는지에 대해 알아본다.

바운디드 컨텍스트는 시스템 모델의 유효 범위를 넓게 정의하는 반면, 애그리거트는 가능한 좁고 응집력있는 경계를 설정한다.

애그리거트는 아래와 같은 특징을 가진다.

  • 내부 비즈니스 규칙, 불변성, 로직의 복잡성을 감싸는 단위
  • 외부에서는 루트 엔티티만 접근 가능 (내부 구조는 추상화)
  • 일관된 트랜잭션 처리 단위로 작동

서비스는 애그리거트가 아니다.
마이크로서비스를 하나의 애그리거트 단위로 1:1 매핑하면 서비스를 얕게 만들 위험이 있다.
개별 서비스는 시스템 내의 다른 구성 요소와의 상호 작용을 고려하여 설계되어야 한다.

애그리거트가 자신의 하위 도메인 내의 다른 엔티티와 강하게 결합되어 있고, 이것을 마이크로서비스로 분리하면 아래와 같은 문제가 발생한다.

  • 연동 인터페이스가 증가함
  • 퍼블릭 API 가 확장되어 결국 시스템 전체의 글로벌 복잡성이 커짐

애그리거트는 마이크로서비스 내부의 복잡성을 관리하는 설계 단위이며, 반드시 서비스 경계 자체가 되어야 하는 것은 아니다.


3.3. 하위 도메인

마이크로서비스를 설계할 때 가장 균형 잡힌 휴리스틱 중 하나는 서비스의 경계를 비즈니스 하위 도메인의 경계와 일치시키는 것이다.

하위 도메인을 비즈니스 관점에서 보면 비즈니스가 무엇을 하는가를 설명한다. 즉, 하위 도메인은 비즈니스 역량(= 무엇을 하는 비즈니스인지)을 정의하며, 그 기능이 어떻게 구현되는지는 관여하지 않는다.

기술적 관점에서 보면 하위 도메인은 응집력 있는 유스케이스들의 집합이다. 같은 도메인 모델을 공유하고, 같은 혹은 밀접한 데이터를 다루며, 강한 기능적 연관성을 갖는다.

하위 도메인의 크기는 ‘어떻게’ 보다는 ‘무엇을’ 에 중점을 두기 때문에 복잡한 로직을 감싸는 깊은 모듈이 되기 쉽다.
하위 도메인이 표현하는 ‘기능’ 은 그 내부의 복잡한 로직을 추상화하고 감싼다.
해당 도메인에 포함된 유스케이스들은 높은 응집력을 가지며, 모델의 깊이와 일관성을 보장한다.

유스케이스를 지나치게 세분화하면 복잡한 퍼블릭 인터페이스가 증가하고, 모듈은 얕아지며, 시스템 전체의 복잡성이 증가할 수 있다.

하위 도메인은 마이크로서비스를 위한 안전하고 의미있는 경계이다.
기능 중심의 응집력 있는 유스케이스 집합은 깊이 있는 모듈을 형성하고, 이를 기반으로 효과적인 서비스 경계를 만들 수 있다.


4. 마이크로서비스의 퍼블릭 인터페이스 압축

DDD 는 서비스의 경계를 찾는데에도 쓰일뿐더러 서비스를 깊게 만드는 데도 도움을 준다.
여기서는 오픈 호스트 서비스충돌 방지 계층 패턴이 어떻게 마이크로서비스의 퍼블릭 인터페이스를 간단하게 만드는지에 대해 알아본다.


4.1. 오픈 호스트 서비스(OHS: Open Host Service)

오픈 호스트 서비스는 비즈니스 도메인의 바운디드 컨텍스트 모델과 시스템 외부 구성 요소와의 연동 모델을 분리해주는 역할을 한다.

공표된 언어는 연동 지향적인 모델로서, 다른 시스템 또는 서비스와의 통신을 위한 명확하고 제한된 계약을 정의한다.
따라서 공표된 언어를 도입하면 아래와 같은 이점이 있다.

  • 시스템의 글로벌 복잡성을 줄여줌
  • 서비스 구현 내부는 변경하더라도 외부에 노출되는 인터페이스는 안정적으로 유지 가능
  • 결과적으로 사용자에게 영향을 주지 않고 내부 로직을 진화시킬 수 있음

오픈 호스트 서비스는 바운디드 컨텍스트 내부 모델을 직접 노출하지 않고, 공표된 언어를 통해 명호가하고 제한된 연동 인퍼페이스를 제공함으로써 시스템의 복잡성을 효과적으로 제어한다.

공표된 언어를 통한 서비스 연동


4.2. 충돌 방지 계층(AC: AntiCorruption Layer)

충돌 방지 계층은 오픈 호스트 서비스와는 다른 방식으로 복잡성을 관리한다.
ACL 은 특히 다른 바운디드 컨텍스트와의 연동 시 복잡성을 줄이는데 특화되어 있다.

ACL 은 바운디드 컨텍스트 간 직접 연동을 피하고, 중간 계층에서 모델/언어/인터페이스 차이를 조정해주는 역할을 한다.
즉, 바운디드 컨텍스트 A 가 컨텍스트 B 를 사용할 때 A 가 B 를 직접 참조하는 대신 A 와 B 사이에 ACL을 두어 연동을 추상화한다.

ACL 은 로컬 복잡성과 글로벌 복잡성 모두를 낮춰주는 장점이 있다/

복잡성 유형ACL 의 효과
로컬 복잡성도메인 내부에서 외부 시스템의 영향을 격리시킴
글로벌 복잡성시스템 전반의 연동 구조를 단순화하고 결합도를 낮춤

ACL 은 바운디드 컨텍스트 내부의 비즈니스 로직과 외부 연동의 복잡성을 명확히 분리해준다.
연동 과정에서 생기는 복잡성은 ACL 이 모두 책임지기 때문에 실제 도메인 모델은 외부 변화로부터 보호된다.

ACL 은 컨텍스트 간의 충돌을 막는 방화벽이다.
비즈니스 로직을 외부 변화로부터 보호하고, 연동을 우연하게 관리할 수 있도록 돕는다.

ACL 서비스는 바운디드 컨텍스트를 사용하는 로컬 복잡성과 시스템의 글로벌 복잡성을 모두 줄여준다.
ACL 서비스가 바운디드 컨텍스ㅌ를 사용할 때의 비즈니스 복잡성과 연동할 때의 복잡성을 분리해준다. 연동의 복잡성은 ACL 서비스가 담당한다.

독립된 서비스로 구현된 ACL


정리하며..

  • 모든 마이크로서비스는 바운디드 컨텍스트이지만 모든 바운디드 컨텍스트가 반드시 마이크로서비스인 것은 아님
  • 마이크로서비스는 서비스의 가장 작은 유효한 경계를 정의함
  • 바운디드 컨텍스트느 모델 전반의 일관성을 보호하고, 가장 넓은 유효한 경계를 나타냄

참고 사이트 & 함께 보면 좋은 사이트

본 포스트는 블라드 코노노프 저자의 도메인 주도 설계 첫걸음을 기반으로 스터디하며 정리한 내용들입니다.






© 2020.08. by assu10

Powered by assu10