본문 바로가기
Spring 7기 프로젝트/플러스 팀 프로젝트

도메인주도 설계 및 데이터 흐름 시각화하기

by JuNo_12 2025. 7. 10.

들어가며

데이터를 관리하는 개발자로서 우리는 흥미로운 사실 하나를 발견할 수 있습니다. 실제 서비스에서 데이터를 생성/수정/삭제하는 경우보다 조회하는 경우가 압도적으로 많다는 것입니다. 생성 테이블은 상대적으로 적고, 우리의 주된 관심사는 "생성된 데이터를 어떻게 사용자에게 빠르게 보여줄 것인가"입니다.

 

하지만 여기서 우리가 놓치고 있는 중요한 부분이 있습니다. 바로 도메인간 데이터 흐름 분석입니다.

우선 저희 프로젝트의 데이터 흐름을 시각자료로 만들었습니다.

 

기술적 해결책에만 집중했던 우리

지금까지 우리는 장애 대응을 위해 다양한 기술적 해결책을 공부해왔습니다:

  • 동시성 문제 해결을 위한 락(Lock) 메커니즘
  • 트랜잭션 전파(Transaction Propagation)
  • 캐싱 전략
  • 비동기 처리

하지만 정작 가장 중요한 '도메인'에 대해서는 깊게 생각해보지 않았습니다. 쉽게 말해, '데이터 흐름'에 대한 진지한 고민이 부족했던 것입니다.

 

전통적인 접근법의 한계

연관관계 중심의 설계

우리는 지금까지 기본적으로 테이블 간 연관관계를 맺는 방식으로 설계해왔습니다:

@Entity
public class Order {
    @ManyToOne
    @JoinColumn(name = "product_id")
    private Product product;  // 직접적인 연관관계
    
    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;        // 직접적인 연관관계
}

 

이 방식은 가장 쉽고 직관적입니다. 하지만 치명적인 단점이 있습니다:

모든 도메인이 강하게 결합되어 있어서, 어느 한쪽에서 장애가 발생하면 시스템 전체에 영향을 끼친다.

 

도메인 의존성 문제

현재 우리는 다음과 같은 것들은 알고 있습니다:

  • 어느 곳에서 어느 데이터가 사용되는지
  • 각 테이블 간의 관계가 어떻게 되어 있는지

하지만 다음과 같은 중요한 질문들에 대한 답은 명확하지 않습니다:

  • 내가 이 데이터를 사용함으로써 의존하고 있는 도메인에게 어떤 영향을 끼칠까?
  • 다른 도메인의 변경이 내 도메인에 어떤 파급효과를 가져올까?

DDD 접근법: 도메인 책임 분리

도메인의 독립성 원칙

Domain-Driven Design(DDD)의 핵심은 도메인의 독립성입니다:

  1. 독립적인 생명주기: 각 도메인은 자신만의 생명주기를 가집니다
  2. 외부 무관심: 다른 도메인이 무엇을 하는지 알 필요가 없습니다
  3. 단일 책임: 자신이 맡은 한 가지 역할만 수행합니다

 

우리 프로젝트의 도메인 분리 사례

기존 설계의 문제점

처음에는 이벤트(핫딜)와 상품을 강하게 결합된 형태로 설계했습니다:

인터페이스를 활용한 추상화로써 의존성을 줄이긴했지만, 결국 강하게 의존한다는 건 여전했습니다.

 

이 설계의 문제점:

  • 상품 도메인의 변경이 이벤트 도메인에 직접 영향
  • 상품 조회 실패 시 이벤트 생성도 실패
  • 도메인 간 순환 의존성 발생 가능

 

개선된 설계: REST API를 통한 느슨한 결합

 

도메인 분리의 이점

  1. 장애 격리: 상품 서비스 장애가 이벤트 서비스에 직접적인 영향을 주지 않음
  2. 독립적 배포: 각 도메인을 독립적으로 배포 가능
  3. 확장성: 각 도메인별로 다른 확장 전략 적용 가능
  4. 유지보수성: 도메인별 책임이 명확해 코드 이해도 향상

동시성 문제와 실시간성 요구사항

핫딜 시스템의 특성

핫딜 시스템에서 가장 중요한 동시성 문제가 발생하는 지점은 바로 '재고'입니다. 여기서는 추가적으로 실시간성이 극도로 중요합니다.

 

동시성 문제 시나리오

1000개의 스레드가 동시에 재고 차감을 시도할 때:

  • 예상 결과: 재고 0개
  • 실제 결과: 재고 수십 개 남음 (Race Condition 발생)

 

Redis Lock을 통한 동시성 제어

 

Lock 구현체:

 

도메인 분리와 동시성 제어의 조화

첫 번째 고민: 이벤트 기반 아키텍처

초기에는 주문과 재고를 완전히 분리하여 이벤트 기반으로 처리하려 했습니다:

// 주문 도메인에서 이벤트 발행
@EventListener
public void handleOrderCreated(OrderCreatedEvent event) {
    stockService.decrease(event.getProductId(), event.getQuantity());
}

 

문제점:

  • 실시간성 보장 어려움
  • 이벤트 처리 실패 시 데이터 불일치 가능
  • 복잡한 보상 트랜잭션 필요

 

두 번째 고민: 주문에서 재고 처리까지

주문 도메인에서 재고 처리까지 담당하는 방안:

@Service
public class OrderService {
    public void createOrder(OrderRequest request) {
        // 재고 확인 및 차감
        stockService.decreaseWithLock(request.getProductId(), request.getQuantity());
        
        // 주문 생성
        Order order = createOrderEntity(request);
        orderRepository.save(order);
    }
}

문제점:

  • 주문 도메인의 책임이 과도하게 증가
  • SRP(Single Responsibility Principle) 위반
  • 재고 로직 변경 시 주문 도메인도 영향받음

최종 해결 방안: 적절한 책임 분리 및 스프링 이벤트 활용

도메인별 명확한 책임 정의

  1. 주문 도메인: 주문 정보 관리, 주문 상태 처리
  2. 재고 도메인: 주문시에 스프링 이벤트를 활용한 이벤트 발행
  3. 상품 도메인: 재고 도메인의 이벤트 구독 및 재고 관리, 상품 메타데이터 제공

데이터 일관성 문제가 생길 수 있지만, 이는 인지하고 있고 추후에 레디스나 카프카를 통해 제어할 생각입니다.

 

핵심 원칙

  1. 각 도메인은 자신의 핵심 책임만 담당
  2. 도메인 간 통신은 인터페이스를 통해
  3. 동시성 제어는 해당 리소스를 소유한 도메인에서
  4. 실시간성이 중요한 부분은 동기 호출 사용

마무리

핫딜 시스템 개발을 통해 우리는 다음을 배웠습니다:

  1. 기술적 해결책만으로는 충분하지 않다: DDD와 같은 설계 원칙이 중요
  2. 도메인 분리의 핵심은 책임 분리: 각 도메인이 자신의 일만 하도록
  3. 동시성과 실시간성의 균형: 적절한 아키텍처 패턴 선택이 중요
  4. 완벽한 분리보다는 적절한 분리: 비즈니스 요구사항에 맞는 설계

결국 좋은 시스템 설계란 기술적 완벽함보다는 비즈니스 요구사항을 만족하면서도 유지보수 가능한 구조를 만드는 것이 아닐까 합니다.