Architecture

도메인 주도 설계(DDD)에서 가장 흔히 저지르는 실수

Written by 개발자서동우 · 24 sec read >
도메인 주도 설계에서 가장 흔히 저지르는 실수

기술 분야에서 일하면서 많은 아키텍처 실수를 저질렀습니다. 특히 도메인 주도 설계(Domain-Driven Design, DDD)에서 발생한 실수는 특히 용서받기 어렵다는 것을 깨달았습니다. DDD에서 잘못된 추상화는 다른 설계 접근법보다 더 큰 혼란을 야기합니다.

이 글에서는 DDD에서 가장 비용이 많이 드는 설계 실수에 대해 이야기하려 합니다. 이는 단일화된 시스템과 긴밀하게 결합된 시스템을 널리 퍼지게 하는 흔한 실수입니다.


소개

다양한 조직에서 비대하고 취약한 고객 API를 많이 보았습니다. 이러한 취약성에 대한 해결책으로 제안된 것은 고객 API를 더 작고 목적 지향적인 서비스로 분할하는 것이었습니다. 이는 API가 너무 무거워질 때 일어나는 일입니다.

기업은 고객을 위해 존재하기 때문에 고객을 단순히 API로 취급하는 것은 너무 모호하고 다소 게으른 접근입니다. 사실, 구체성이 부족하기 때문에 비대하고 취약한 API가 생기게 됩니다. 이 문제는 주문 API, 제품 API, 계정 API 등 다른 예시에서도 쉽게 볼 수 있습니다.

대부분의 경우 우리가 모델링하려는 것은 이러한 개념을 둘러싼 비즈니스 프로세스이지, 개념 자체가 아닙니다. 이것이 우리가 흔히 저지르는 실수입니다.

중심 개념과 Bounded Context의 혼동

“중심 개념”은 특정 산업의 주요 아이디어를 의미합니다. 예를 들어:

  • 은행의 계정(Account)
  • 보험의 정책(Policy)
  • 공급망 관리의 제품(Product)
  • 항공사의 예약(Reservation)
  • 전자 상거래의 주문(Order)
  • 전자 상거래의 고객(Customer)

“Bounded Context”는 특정 프로세스와 규칙이 적용되는 비즈니스 영역을 의미합니다. 중심 개념이 다양한 규칙 아래 반복적으로 나타나는 곳입니다. 예를 들어:

  • 은행에서 계정(Account)은 대출 발행, 청구, 부채 추심, 마케팅 및 커뮤니케이션에서 각각 다르게 보입니다.
  • 보험에서 정책(Policy)은 인수, 청구, 검사에서 다르게 보입니다.
  • 공급망 관리에서 제품(Product)은 계획, 소싱, 재고 관리, 배송에서 다르게 보입니다.
  • 항공사에서 예약(Reservation)은 예약, 운영, 화물 관리, 로열티에서 다르게 보입니다.
  • 전자 상거래에서 주문(Order)은 구매, 공급망, 이행, 고객 지원에서 다르게 보입니다.
  • 전자 상거래에서 고객(Customer)은 광고, 주문, 배송에서 다르게 보입니다.

문제는 바로 여기서 시작됩니다. DDD안에서 중심 개념들은 그 복잡성과 빈번한 등장으로 인해 우리가 생각하는 것보다 더 자주 우리를 혼란스럽게 만듭니다. 이들은 우리가 이를 실제 Bounded Context로 취급하도록 속입니다.


제가 이 실수를 처음 경험한 곳인 핀테크 산업을 예로 들어 보겠습니다. 우리는 계정 API를 만들기로 결정했는데, 처음에는 합리적인 아이디어처럼 보였습니다. 그러나 계정은 Bounded Context나 API가 되어서는 안 됩니다. 이 점을 이론적 관점과 실용적 관점에서 설명해 보겠습니다.

이론적 관점

먼저, 계정을 단일 관점에서 보는 것은 DDD의 핵심 원칙에 어긋납니다. 이는 유연성이 부족하고 비대해진 도메인 모델을 초래합니다.

다양한 Bounded Context는 계정을 각기 다른 시각으로 인식하며, 경우에 따라 계정은 비즈니스 전반에서 다른 이름으로 불리기도 합니다(Ubiquitous Language).

예를 들어, 다음은 계정 정보에 중점을 둔 Bounded Context의 목록입니다:

  • 대출 발행은 배경 조사, 위험 평가, 이자율 및 신용 한도와 같은 계정 조건에 중점을 둡니다.
  • 청구계정의 적시 결제 및 명세서 생성에 중점을 둡니다.
  • 마케팅 및 커뮤니케이션은 고객의 커뮤니케이션 선호도와 계정의 결제 알림에 중점을 둡니다.
  • 부채 추심은 고객이 어려움을 겪을 때 이자율을 낮추거나 대체 결제 계획을 제공하는 데 도움을 줍니다. 불행히도, 경우에 따라 추심 기관에 계정을 판매하기도 합니다. 이러한 기관은 고객을 괴롭히고 자산을 압류할 수 있습니다.

둘째, 도메인 주도 설계(DDD)는 복잡한 비즈니스 문제를 해결하는 데 중점을 두며, 소프트웨어 모델은 비즈니스 프로세스를 반영해야 합니다. 계정 선호도 관리와 부채 추심은 비즈니스 프로세스입니다. 하지만 계정 자체는 비즈니스 프로세스가 아닙니다. 계정은 다양한 비즈니스 프로세스 내에 존재하는 개념일 뿐입니다.

실용적 관점

계정을 하나의 Bounded Context로 취급하면 연쇄적으로 부정적인 결과가 발생합니다.

불필요한 결합

첫 번째 문제는 계정 Bounded Context가 ‘신의 도메인(God Domain)’이 되어 과도한 책임을 지게 된다는 것입니다. 그 결과, 다른 Bounded Context와의 긴밀한 결합이 발생합니다.

Incorrect Bounded Context Modeling
잘못된 계정(Account) Bounded Context
팀 간 마찰

이런 결합은 문제 해결을 중심으로 팀을 조직하는 데 부정적인 영향을 미칩니다. 모든 팀이 변경 사항을 적용하기 위해 계정과 상호작용해야 하므로 의존성과 마찰이 증가합니다. 이로 인해 다음과 같은 바람직하지 않은 부작용이 발생할 수 있습니다:

  • 계정 팀이 다른 팀의 기능 요청으로 과부하가 되어 요청 수가 증가함에 따라 지연이 발생합니다.
  • 계정 팀이 다른 팀이 변경 사항(Pull Requests)을 제출할 수 있도록 허용합니다. 이 방법은 계정 팀이 많은 양의 요청을 검토해야 하므로 여전히 지연을 초래합니다.
  • 계정 팀의 크기가 작업량에 맞추어 증가하지만, 팀원 간 병합 충돌이 더 자주 발생합니다.

위에서 설명한 모든 접근 방식은 팀의 자율성과 소프트웨어 출시 예측 가능성을 저해합니다. 그 결과, 조직은 동시에 여러 아이디어를 실행하는 데 제한을 받게 됩니다.

단일 실패 지점 및 복원력 문제

다른 Bounded Context에서 계정으로부터 정보를 요청하거나 계정에 정보를 전파하게 되면, 도메인 간 상호작용이 빈번해져서 성능 및 복원력 문제가 발생합니다.

계정을 단일 진실 공급원으로 삼으면 시스템 전체의 단일 실패 지점이 됩니다. 예를 들어, 계정 생성 코드에 버그가 있어 메모리 누수가 발생하면 결제 일정과 같은 관련 없는 영역의 서비스가 중단될 수 있습니다. 이는 계정 생성뿐만 아니라 결제 처리와 같은 주요 비즈니스 기능에도 영향을 미칩니다.

함정 피하기

분해 및 캡슐화

제 경험을 돌이켜보면, 대부분의 시간은 시스템을 분해하고 캡슐화하는 데 사용되었습니다.

문제를 더 작은 조각으로 나누는 것이 분해(Decomposition)입니다. 이러한 조각들은 종종 범위가 지정되어야 하는 비즈니스 규칙과 유사합니다. 여기서 캡슐화가 필요합니다.

캡슐화(Encapsulation)는 비즈니스 규칙의 경계를 정의하고 다른 영역으로 누출되지 않도록 보호하는 것입니다. 이는 다른 시스템이 이러한 규칙에 접근하고 상호작용하는 방식을 정의합니다.

DDD도 마찬가지입니다. 이제 우리는 계정(Account)이 다양한 상황에서 여러 표현을 가질 수 있다는 것을 이해했습니다. 따라서 이를 다른 Bounded Context로 분해할 수 있는지 평가해야 합니다.

다음 단계는 비즈니스 규칙을 범위 내에서 보호하고 캡슐화하는 방법을 생각하는 것입니다. 즉, 중심 개념인 계정을 어떻게 정확히 표현할 것인지 고민해야 합니다.

여기서 애그리거트(Aggregates)가 등장합니다. 애그리거트는 계정을 캡슐화하는 단위입니다. 이들은 비즈니스 프로세스에 따라 데이터의 무결성을 유지할 책임이 있습니다. 각자의 Bounded Context 내에서 목적에 맞는 API를 통해 접근 규칙을 정의할 수 있습니다.

다음은 계정을 다양한 Bounded Context 내에서 애그리거트로 분해하는 예시입니다:

계정(Account) Bounded Context를 애그리거트(Aggregate)로 분해하기
(CollectionAccount, AccountPreferences, AccountTerms, AccountPayments)

부채 추심의 경우, 계정(Account)의 개념을 CollectionAccount로 정의하여 정상 계정과 다른 상태임을 나타냅니다. 예를 들어, 특정 PaymentPlan(이자율 없는)을 부여하려면 CollectionAccount를 통해 해당 변경을 진행해야 합니다.

청구(Billing)에서는 AccountPayments가 중심 개념(애그리거트)입니다. 고객이 결제를 하고, 명세서와 결제 내역을 확인할 수 있게 하는 것이 이 Bounded Context에 해당하는 주요 작업입니다.

구현 관점에서 보면, 부채 추심의 CollectionAccount는 청구의 AccountPayments와 다른 스키마와 동작을 가지게 됩니다. 이는 매우 정상적이며, 사실 DDD 원칙에서 권장하는 바입니다.

마무리 생각

도메인 주도 설계(Domain-Driven Design, DDD)는 복잡한 비즈니스 프로세스와 규칙을 모델링하는 것입니다. DDD에서 흔히 저지르는 실수 중 하나는 금융 서비스의 ‘계정(Accounts)’, 전자 상거래의 ‘고객(Customer)’, 보험의 ‘정책(Policy)’과 같은 중심 개념을 Bounded Context나 API로 잘못 모델링하는 것입니다. 이 실수는 특히 도메인 주도 설계를 사용할 때 되돌리기가 매우 비용이 많이 듭니다.

이 함정을 피하기 위해서는 설계를 모델링하는 비즈니스 프로세스와 연결하는 것이 중요합니다. 이러한 중심 개념은 도메인 주도 설계에서 중요한 역할을 합니다. 하지만 이들의 역할은 Bounded Context 자체가 아니라, Bounded Context의 전체적인 일관성을 유지하고 관리하는 역할(즉, 애그리거트)입니다.

Written by 개발자서동우
안녕하세요! 저는 기술 분야에서 활동 중인 개발자 서동우입니다. 명품 플랫폼 (주)트렌비의 창업 멤버이자 CTO로 활동했으며, AI 기술회사 (주)헤드리스의 공동 창업자이자 CTO로서 역할을 수행했습니다. 다양한 스타트업에서 일하며 회사의 성장과 더불어 비즈니스 상황에 맞는 기술 선택, 개발팀 구성 및 문화 정착에 깊은 경험을 쌓았습니다. 개발 관련 고민은 언제든지 편하게 연락주세요 :) https://linktr.ee/dannyseo Profile

Leave a Reply

Your email address will not be published. Required fields are marked *