매출(정산) Domain Design part.2
feat. DDD-Lite 전술적 설계
JOBKOREA X ALBAMON
매출(정산) Domain Design part.2
feat. DDD-Lite 전술적 설계
안녕하세요 잡코리아 사내플랫폼개발팀 한동우 입니다 😀
이전 글인 매출(정산) 도메인 설계 1편 feat. DDD-Lite 전략적 설계 에 이어, 이번에는 2편인 DDD-Lite 전술적 설계로 돌아왔습니다.
전략적 설계의 다음 단계는 Bounded Context와 서브 도메인을 기반으로 시스템을 더 구체적으로 모델링하는 전술적 설계입니다.
🖥️ 전술적 설계
매출(정산) 도메인에 대한 전술적 설계(Tactical Design)는 구체적인 엔티티, 값 객체, 도메인 서비스, 리포지토리, 서비스 계층 등을 설계하여 실제 비즈니스 로직을 구현하는 과정입니다.
이 과정을 통해 도메인 모델을 실제 코드로 구현하고, 시스템의 복잡성을 해결하기 위해 필요한 다양한 설계 패턴을 적용할 수 있습니다.
아래는 매출(정산) 도메인의 전술적 설계 과정을 세부적으로 구성한 내용입니다.
전술적 설계의 핵심 요소
전술적 설계에서는 도메인 모델을 실제 코드로 구현하기 위해 DDD 패턴을 적용합니다. 주요 설계 요소는 다음과 같습니다:
- 엔티티(Entity): 고유한 식별자를 가지며 상태를 변경할 수 있는 객체
- 값 객체(Value Object): 고유한 식별자는 없지만 속성으로 정의되는 불변 객체
- 집합체(Aggregate): 여러 엔티티와 값 객체를 포함하는 그룹으로, 하나의 루트 엔티티(aggregate root)를 통해 일관성을 유지
- 도메인 서비스(Domain Service): 도메인 로직을 캡슐화하고, 여러 엔티티 간의 복잡한 비즈니스 로직을 처리하는 서비스
- 리포지토리(Repository): 도메인 객체를 저장하고 검색하는 역할을 담당
전술적 설계의 과정
전술적 설계는 1) Entity 정의, 2) Value Object 정의, 3) 도메인 서비스 정의 하는 단계로 진행 하겠습니다. 각 단계에서 산출물은 클래스 다이어그램, 도메인 서비스 인터페이스 등을 정의 할 수 있습니다.
1) Entity 정의
영업수익데이터 (RevenueData)
- 영업수익에 관한 원본 데이터. 외부에서 수집된 영업수익 원본 데이터를 담고 있습니다.
- 주요 속성: 수익금액, 거래일자, 웹사이트 ID, 계약번호, 회원ID, 매출귀속부서, 영업부서, 상품 코드, 서비스 코드 등.
계약번호로 집계된 영업수익 (RevenueByContract)
- 계약번호별로 집계된 영업수익 데이터.
- 주요 속성: 계약번호, 집계된 영업수익, 집계일자 등.
회원 ID로 집계된 영업수익 (RevenueByMember)
- 회원별로 집계된 영업수익 데이터.
- 주요 속성: 회원 ID, 집계된 영업수익, 집계일자 등.
상품 코드로 집계된 영업수익 (RevenueByProductCode)
- 상품 코드별로 집계된 영업수익 데이터.
- 주요 속성: 상품 코드, 집계된 영업수익, 집계일자 등.
매출 귀속 부서로 집계된 영업수익 (RevenueByDepartment)
- 매출 귀속 부서별로 집계된 영업수익 데이터.
- 주요 속성: 부서 ID, 집계된 영업수익, 집계일자 등.
영업자별 집계된 영업수익 (RevenueBySalesAgent)
- 영업자별로 집계된 영업수익 데이터.
- 주요 속성: 영업자 ID, 집계된 영업수익, 집계일자 등.
영업 부서로 집계된 영업수익 (RevenueBySalesDepartment)
- 영업 부서별로 집계된 영업수익 데이터.
- 주요 속성: 영업 부서 ID, 집계된 영업수익, 집계일자 등.
일별 집계된 영업수익 (RevenueByDate)
- 일별로 집계된 영업수익 데이터.
- 주요 속성: 집계일자, 집계된 영업수익, 웹사이트 ID 등.

매출(정산) entity diagram
클래스 및 관계 설명
- RevenueData:
RevenueData는 영업수익의 원본 데이터를 나타내며, 이 데이터는 다양한 집계 기준(계약번호, 회원ID, 상품코드 등)으로 집계됩니다. 이 클래스는 영업수익을 저장하고, 이를 업데이트하거나 삭제하는 메소드를 가집니다. - RevenueByContract, RevenueByMember, RevenueByProductCode, RevenueByDepartment, RevenueBySalesAgent, RevenueBySalesDepartment, RevenueByDate: 각 클래스는 집계된 영업수익 데이터를 나타냅니다. 예를 들어,
RevenueByContract는 계약번호별로 집계된 데이터를 저장하고,RevenueByMember는 회원별로 집계된 데이터를 저장합니다. - 모든 클래스는
aggregateRevenue()메소드를 통해 집계된 영업수익을 계산합니다. 이는 주어진 기준에 따라 영업수익을 집계하는 핵심 메소드입니다. - aggregation 관계: 각 집계된 영업수익 클래스는 원본 데이터(
RevenueData)와 aggregation 관계를 갖습니다. 예를 들어,RevenueByContract클래스는RevenueData에서contractNumber를 기준으로 집계된 데이터를 참조합니다.
2) Value Object 정의
Value Object는 식별자가 필요하지 않고, 변경 불가능(immutable)한 객체로, 특정 속성 값들의 집합을 나타냅니다. 주로 도메인에서 돈, 날짜, 통화와 같은 의미 있는 데이터를 표현하는 데 사용됩니다. 매출(정산) 도메인에서는 다양한 값 객체들이 사용될 수 있습니다.
1. RevenueAmount (영업수익 금액)
영업수익 금액을 나타내는 값 객체입니다. 금액은 수학적 계산이 필요하기 때문에 BigDecimal과 같은 정확한 값 타입을 사용하여 구현됩니다. 또한, 값 객체는 변경할 수 없고, 금액의 변동이 필요하다면 새로운 객체를 생성해야 합니다.
속성
taxableAmount: BigDecimal(과세 대상 금액)nonTaxableAmount: BigDecimal(비과세 대상 금액)
메서드
add(RevenueAmount otherAmount): RevenueAmount현재 객체의 과세금액과 비과세금액에 다른RevenueAmount객체를 더한 새로운 객체를 반환합니다.subtract(RevenueAmount otherAmount): RevenueAmount현재 객체의 과세금액과 비과세금액에서 다른RevenueAmount객체를 뺀 새로운 객체를 반환합니다.multiply(BigDecimal multiplier): RevenueAmount과세금액과 비과세금액을 주어진 배율로 곱한 새로운 객체를 반환합니다.equals(RevenueAmount otherAmount): boolean두RevenueAmount객체의 과세금액과 비과세금액이 동일한지 비교합니다.toString(): String RevenueAmount객체를 문자열로 표현합니다. 예: "과세금액: 50000, 비과세금액: 15000"
2. DateRange (날짜 범위)
매출 데이터가 집계되는 기간을 나타내는 값 객체입니다. from 날짜와 to 날짜를 가지며, 날짜 범위의 유효성 검사를 포함한 메서드를 제공합니다.
속성
startDate: LocalDateendDate: LocalDate
메서드
isValid(): boolean(날짜 범위가 유효한지 확인하는 메서드)includes(LocalDate date): boolean(주어진 날짜가 범위에 포함되는지 확인하는 메서드)toString(): String(날짜 범위를 문자열로 변환)
Value Object 설계 이유 및 유의 사항
- 불변 객체(Immutable Object):
RevenueAmount는 불변 객체로 설계되어, 한번 생성된 객체는 변경되지 않으며 값이 변경될 필요가 있을 경우 새로운 객체를 생성해야 합니다. 이는 금액 계산이 안전하게 이루어지도록 보장하고, 영업수익 데이터의 불변성을 유지합니다. - 정확한 수치 처리:
BigDecimal을 사용하여 금액을 처리하므로, 소수점 처리나 반올림 오류가 발생하지 않습니다. 이는 영업수익 계산에서 매우 중요한 부분으로, 특히 재무와 관련된 시스템에서 정확한 계산이 필수적입니다. - 유연성: 과세금액과 비과세금액을 개별적으로 다룰 수 있는 유연성을 제공합니다. 예를 들어, 세금이 부과되지 않는 일부 매출 항목을 처리할 수 있도록 하여, 다양한 상황에 맞는 정확한 계산이 가능해집니다.
- 계산 편의성:
add(),subtract(),multiply()와 같은 메서드를 통해 금액의 계산을 손쉽게 처리할 수 있으며, 이를 통해 다른RevenueAmount객체와의 연산을 명확하게 수행할 수 있습니다.
3) 도메인 서비스 정의
도메인 서비스는 엔티티와 값 객체들이 수행하는 복잡한 비즈니스 로직을 캡슐화합니다. 주로 여러 엔티티나 값 객체들이 협력해야 할 때 유용하며, 도메인 객체에 너무 많은 책임이 집중되지 않도록 비즈니스 로직을 외부로 분리합니다.
1. RevenueAggregationService (영업수익 집계 서비스)
매출(정산) 시스템에서 수집된 다양한 영업수익 데이터를 집계하는 서비스입니다. 집계 기준별로 영업수익을 집계하고, 집계된 데이터를 RevenueByContract, RevenueByMember, RevenueByProductCode 등으로 반환합니다.
책임
- 계약번호, 회원ID, 상품코드, 매출 귀속 부서 등 기준에 맞춰 영업수익을 집계
- 집계된 영업수익을
RevenueByContract,RevenueByMember,RevenueByProductCode등의 엔티티로 저장
메서드
aggregateRevenueByContract(DateRange dateRange): List<RevenueByContract>(계약번호로 영업수익 집계)aggregateRevenueByMember(DateRange dateRange): List<RevenueByMember>(회원ID로 영업수익 집계)aggregateRevenueByProductCode(DateRange dateRange): List<RevenueByProductCode>(상품코드로 영업수익 집계)aggregateRevenueBySalesAgent(DateRange dateRange): List<RevenueBySalesAgent>(영업자별영업수익 집계)
2. RevenueValidationService (영업수익 유효성 검증 서비스)
영업수익 데이터가 유효한지 검증하는 서비스입니다. 수익 금액이 음수인지, 날짜 범위가 유효한지 등의 검증 로직을 포함합니다.
책임
RevenueData객체의 유효성 검사- 영업수익 금액, 날짜, 계약번호 등의 유효성 검사
메서드
validateRevenueData(RevenueData data): boolean(영업수익 데이터 유효성 검사)validateRevenueAmount(RevenueAmount amount): boolean(영업수익 금액 유효성 검사)validateDateRange(DateRange dateRange): boolean(날짜 범위 유효성 검사)
3. RevenueCalculationService (영업수익 계산 서비스)
집계된 영업수익 데이터를 기반으로 영업수익을 계산하는 서비스입니다. 예를 들어, 특정 영업사원의 매출을 계산하거나, 특정 상품의 매출 총합을 계산할 수 있습니다.
책임
- 영업수익 데이터 집계 후 계산된 총합, 평균 등을 반환
- 계산 결과를 다양한 기준으로 집계된 영업수익 객체에 반영
메서드
calculateTotalRevenue(List<RevenueByContract> revenues): RevenueAmount(계약번호별로 집계된 영업수익 총합 계산)calculateAverageRevenue(List<RevenueBySalesAgent> revenues): RevenueAmount(영업자별로 집계된 영업수익 평균 계산)calculateRevenueByDepartment(List<RevenueByDepartment> revenues): RevenueAmount(부서별 집계된 영업수익 계산)
매출(정산) 도메인의 전술적 설계에서는 엔티티, 값 객체, 도메인 서비스, 리포지토리, 서비스 계층을 중심으로 비즈니스 로직을 구현합니다. 이를 통해 각 서브 도메인 간의 명확한 책임 분리를 유지하고, 확장 가능한 시스템을 구축할 수 있습니다.
확장성 및 유지보수성 고려 사항
유연한 도메인 모델 설계 방안
매출(정산) 도메인은 비즈니스 로직이 복잡하고 자주 변경될 수 있기 때문에 유연한 도메인 모델을 설계하는 것이 중요합니다.
- 도메인 이벤트 기반 설계: 새로운 집계 기준이 추가나 비즈니스 요구사항이 변경시 도메인 모델을 수정하지 않고 도메인 이벤트를 활용해 시스템을 확장할 수 있습니다. 예를 들어, 새로운 매출 항목을 추가하고자 할 때, 기존 매출 집계 시스템에 도메인 이벤트를 통해 새로운 로직을 적용할 수 있습니다.
- 오픈/클로즈 원칙: 도메인 모델은 확장 가능하면서도 기존 코드를 변경하지 않도록 설계해야 합니다. 새로운 집계 기준이 추가되거나 비즈니스 로직이 변경될 때마다 기존 코드를 수정하지 않고 새로운 클래스를 추가하는 방식으로 유연하게 변경할 수 있어야 합니다.
시스템 구축 및 운영 시 발생한 문제점과 해결 방안
- 데이터 불일치 문제: 배치 작업을 통해 매출 집계를 진행할 때, 외부 API에서 데이터를 받아오는 과정에서 시간이 지연되어 매출 집계 시점에 불일치가 발생하는 문제가 있었습니다. 이를 해결하기 위해, 배치 작업의 순차적 처리와 타임스탬프 기반 데이터 처리 방식을 도입하였고, 추가로 외부 API에서 제공 받은 totalCount와 실제 조회 된 rowCount를 비교하여 불일치시 트랜잭션 롤백하여 데이터 정합성을 확보했습니다.
이렇게 매출(정산) 도메인의 전략적 설계와 전술적 설계를 모두 다루어 보았습니다. 전략적 설계는 시스템을 큰 틀에서 나누고 관계를 정의하는 작업이라면, 전술적 설계는 이를 실제로 어떻게 구현할지에 대한 구체적인 방법론을 제시 합니다. 각 설계에서 도출되는 다이어그램, 클래스 설계 등은 도메인 모델을 명확히 하고, 유지보수성 및 확장성을 고려한 시스템을 구축하는 데 중요한 역할을 합니다.
1편과 2편에서 다룬 매출(정산) 도메인의 설계 과정은 DDD(도메인 주도 설계) 관점에서 진행하려고 노력하였습니다. DDD는 비즈니스 로직을 중심으로 시스템을 모델링하고, 도메인 전문가와 협력하여 도메인 객체의 책임을 명확히 분리 합니다. 이를 통해 매출(정산) 도메인을 유연하고 확장 가능한 시스템으로 설계 할 수 있었으며, 변화하는 비즈니스 요구사항에 효과적으로 대응할 수 있을 것으로 생각 합니다.👍
메타데이터
- post_id
- d4403d010be9
- slug
- 매출-정산-도메인-설계-feat-ddd-lite-전술적-설계-작성중-d4403d010be9
- url
- https://techblog.jobkorea.co.kr/%EB%A7%A4%EC%B6%9C-%EC%A0%95%EC%82%B0-%EB%8F%84%EB%A9%94%EC%9D%B8-%EC%84%A4%EA%B3%84-feat-ddd-lite-%EC%A0%84%EC%88%A0%EC%A0%81-%EC%84%A4%EA%B3%84-%EC%9E%91%EC%84%B1%EC%A4%91-d4403d010be9
- canonical_url
- https://techblog.jobkorea.co.kr/%EB%A7%A4%EC%B6%9C-%EC%A0%95%EC%82%B0-%EB%8F%84%EB%A9%94%EC%9D%B8-%EC%84%A4%EA%B3%84-feat-ddd-lite-%EC%A0%84%EC%88%A0%EC%A0%81-%EC%84%A4%EA%B3%84-%EC%9E%91%EC%84%B1%EC%A4%91-d4403d010be9
- author_url
- https://medium.com/@dongwoo_93085
- status
- ok
- fetched_at
- 2026-06-09 15:37:30