팀이 ‘고객’ 정의에 합의하지 못해 코드베이스가 엉망이다

발행: (2026년 6월 7일 PM 12:45 GMT+9)
13 분 소요
원문: Dev.to

Source: Dev.to

아무도 이 말을 듣고 싶어 하지 않는다.
하지만 여러분의 소프트웨어가 변경하기도 어렵고, 테스트하기도 어렵고, 새로운 엔지니어에게 설명하기도 힘든 이유는 기술 스택이 아니라, 코드가 실제 비즈니스 흐름을 반영하지 못하고 있기 때문이다.

엔지니어들은 “고객”, “주문”, “학생”, “구독자” 같은 한 단어를 사용하면서도, 시스템의 어느 부분을 건드리느냐에 따라 전혀 다른 의미로 쓰고 있다. 도메인 전문가가 “주문”이라고 하면, 데이터베이스 스키마가 정의한 “주문”과는 완전히 다른 뜻이다.

그 차이가 바로 복잡성이 살아 숨쉬는 곳이며, 버그가 탄생하는 곳이고, 시니어 엔지니어들이 금요일을 보내는 이유다.

Domain‑Driven Design(DDD)은 그 차이를 메우는 방법론이다. 여기서는 학술적인 잡담 없이 실질적으로 무엇을 의미하는지 설명한다.


지도 비유

지하철 노선, 수중 위험 구역, 등산로, 비행 경로를 한 번에 보여주려는 지도를 상상해 보라.
그런 지도는 쓸모가 없다.

지하철 지도는 열차를 타는 데 필요한 것만 보여주고, 항해 차트는 항해에 필요한 것만 보여준다. 각각은 특정 목적을 위해 만든 추상화이며, 특정 상황에서만 유효하다.

소프트웨어 모델도 마찬가지다.

한 번에 청구팀, 마케팅팀, 지원팀, 물류팀 모두를 만족시켜야 하는 “Customer” 클래스를 만들면, 그 클래스는 부풀어 오른 모호한 재앙이 된다. 모두가 필드를 추가하고, 아무도 제거하지 않는다. 모델은 어느 누구에게도 구체적인 의미를 잃는다.

이것이 단일 모델 함정이며, 대부분의 대규모 코드베이스가 바로 그 안에 있다.

DDD는 설계를 두 층으로 나눈다. 전략적 설계가 먼저이며, 이는 코드를 한 줄도 쓰기 전에 하는 작업이다.


Step 1: Find Your Subdomains

서브도메인 찾기

서브도메인은 비즈니스 문제의 조각이다. 주문, 배송, 알림, 결제, 재고 등. 이것들은 마이크로서비스가 아니라, 분석 과정에서 식별한 비즈니스 문제이다.

이 식별 작업은 엔지니어만의 회의에서 절대 이루어져서는 안 된다. 엔지니어가 도메인 전문가 없이 서브도메인을 정의하면, 그들은 “비즈니스가 하는 일”을 추측해 모델링하게 된다. 이는 전혀 다른 결과다. 반드시 도메인 전문가를 회의에 참여시켜라. DDD의 목표는 현실 세계 프로세스를 반영하는 소프트웨어를 만드는 것이며, 이를 위해서는 모두가 같은 페이지에 있어야 한다.

서브도메인을 파악했으면 다음과 같이 분류한다.

  • 핵심 서브도메인 – 경쟁 우위가 되는 영역. 직접 구축한다. 최고의 엔지니어가 여기서 일한다.
  • 지원 서브도메인 – 필요하지만 차별화 요소는 아니다. 간단히 구현하거나 외주를 줄 수 있다.
  • 일반 서브도메인 – 이미 해결된 문제. 상용 소프트웨어를 사용한다. 이메일 전송을 직접 구현하지 말라.

Step 2: Establish a Ubiquitous Language

통합 언어 만들기

다른 어떤 DDD 개념보다도 바로 실효를 보는 개념이다.

통합 언어란 비즈니스와 엔지니어링 팀이 같은 단어같은 개념을 설명한다는 뜻이다. 동의어나 근사치가 아니다. 정확히 같은 단어다.

비즈니스에서는 “구독(subscription)”이라 부르고, 코드에서는 “플랜(plan)”, 데이터베이스에서는 “계약(contract)”이라 부른다면, 모든 대화, 티켓, 버그 리포트, 신규 입사자 온보딩마다 번역 레이어가 필요하게 된다.

번역을 없애라. 코드에서 비즈니스가 부르는 그대로 이름을 짓고, 도메인 전문가가 엔티티 이름만 보고도 자신의 용어임을 알아볼 수 있게 하라.


Step 3: Event Storming

이벤트 스톰링

아키텍처를 설계하기 전에 이벤트 스톰링 세션을 진행한다. 도메인 전문가와 엔지니어를 한 방에 모아 포스트잇을 붙인다. 시스템에서 발생하는 모든 사건을 과거형으로 적는다. 예: “주문 접수”, “결제 확인”, “드론 파견”.

그 다음, 해당 사건을 트리거하는 명령을 적고, 명령을 내리는 주체를 적는다.

포스트잇 벽은 공유 이해관계가 된다. 사건들을 클러스터링하면 서브도메인이 자연스럽게 패턴 속에서 드러난다.

**경계가 있는 컨텍스트(bounded context)**는 통합 언어 질문에 대한 아키텍처적 답이다.

불편한 진실: 같은 단어가 비즈니스의 서로 다른 영역에서 정당하게 다른 의미를 가질 수 있으며, 이는 괜찮다.

예를 들어 토마토는 식물학에서는 과일, 요리에서는 채소다. 두 정의 모두 각자의 맥락에서 옳다. 식물학적 정의가 승리할 필요도, 요리사의 정의가 패배할 필요도 없다. 각각의 도메인 안에서 정확하다.

청구 컨텍스트에서 “구독자”는 플랜, 청구 주기, 결제 수단을 가진 과금 대상이다. 알림 컨텍스트에서 “구독자”는 메시지를 받는 엔드포인트다. 이를 하나의 모델에 억지로 끼워넣으면 양쪽 모두 마찰이 발생한다.

경계가 있는 컨텍스트는 명확한 선을 그린다. 그 선 안에서는 통합 언어가 유효하고 일관된다. 선을 넘어서는 경우, 번역을 의도적으로 관리한다—우연히가 아니라.

컨텍스트가 만드는 세 가지 경계

  • 물리적 경계 – 컨텍스트가 독립 서비스 혹은 배포 가능한 유닛으로 동작한다.
  • 논리적 경계 – 컨텍스트가 더 큰 코드베이스 안의 모듈이나 패키지이며, 엄격한 인터페이스를 가진다.
  • 소유권 경계 – 한 팀이 컨텍스트를 처음부터 끝까지 완전히 책임진다.

마지막 경계는 대부분의 팀이 깨닫지 못하는 중요한 포인트다. 경계가 공유된 컨텍스트는 두 팀이 끊임없이 서로에게 영향을 미치는 결정을 내리게 만든다. 그 마찰이 코드에 결합도로 나타난다. 각 컨텍스트에 명확한 소유자를 지정하라. 조직적 경계가 아키텍처적 경계를 강화한다.

컨텍스트 간 상호작용

경계가 있는 컨텍스트는 고립되어 있지 않다. 주문 컨텍스트는 배송 컨텍스트와, 결제 컨텍스트는 알림 컨텍스트와 소통해야 한다.

컨텍스트 맵은 어떤 도메인이 서로 상호작용하고, 어떻게 통신하며, 관계 흐름이 어느 방향인지 문서화한다.

두 컨텍스트가 정보를 교환해야 할 때 가장 중요한 도구는 **반부패 계층(Anti‑Corruption Layer, ACL)**이다. ACL은 컨텍스트 사이에 위치한 번역 인터페이스다. 예를 들어 배송 컨텍스트가 주문 컨텍스트로부터 데이터를 받을 때, ACL은 그 데이터를 배송 컨텍스트 자체 모델과 언어로 변환한다—즉 주문 도메인의 개념이 배송 내부 로직에 스며드는 것을 방지한다.

ACL이 없으면 새는 추상화가 생긴다. 주문 팀이 필드명을 바꾸면 배송 팀의 테스트가 갑자기 깨진다. 이런 의존성은 설계 실패이며, 운이 나쁜 것이 아니다.

전략적 설계는 무엇을 만들고 경계는 어디에 둘지 알려준다. 전술적 설계는 도메인을 코드에 어떻게 표현할지 알려준다.

엔티티

엔티티는 고유한 식별자를 가지고 시간이 지나도 지속되는 객체다.
드론은 엔티티이다. 배터리 수준과 모델이 동일하더라도 Drone #A47은 Drone #B12와 다르다. 중요한 것은 식별자이며, 속성값이 아니다. 엔티티는 변할 수 있다—상태는 바뀔 수 있지만 식별자는 변하지 않는다.

값 객체

값 객체는 전적으로 그 속성으로 정의된다. 자체 식별자가 없다.
배터리 수준 80%는 단순한 값이다. 80% 하나는 다른 80%와 동일하다. “BatteryLevel #4291” 같은 식별자는 존재하지 않는다—그저 값일 뿐이다.

값 객체는 불변이다. 배터리 수준이 바뀌면 기존 객체를 수정하지 않고 새로운 객체를 만든다. 이렇게 하면 공유된 가변 상태에 대한 참조가 의도치 않은 부작용을 일으키는 버그를 근본적으로 차단할 수 있다.

값 객체를 적극 활용하라. 이메일 주소는 단순 문자열이 아니라 검증 로직이 내장된 값 객체다. 금액은 부동소수가 아니라 통화, 정밀도, 연산 규칙을 가진

0 조회
Back to Blog

관련 글

더 보기 »