[MSA] DDD 구조 고려한 패키지를 생성하는 방법

2025. 3. 1. 23:33·TIL
목차
  1. 패키지 구조
  2. 수직 분리방식의 패키지 구조
  3. application패키지의 역할
  4. Event 패키지의 역할
  5. 이 구조의 핵심
  6. 유즈케이스에서의 서비스 직접 호출
  7. 잘못된 UseCase (도메인 엔티티를 직접 조작)
  8. 올바른 UseCase
  9. 이번엔 도메인을 분리하는 과정에서 생긴 의문이다. 매장과 메뉴는 동일한 콘텍스트에 둬야 할까, 다른 콘텍스트에 둬야할까?
728x90
반응형
SMALL

기존에 있던 모노리딕 프로젝트의 패키지 구조를 변화시키고 각 도메인별로 콘텍스트를 나눠 이후 분산환경으로 전환하더라도 문제없이 수행할 수 있도록 개선하는 작업을 진행해보려고 한다.

이 관계에서 여러가지 고민들을 했었는데 이 부분들에 대한 감각을 기록해놓으려고 한다.

패키지 구조

내가 찾아본 DDD 아키텍처에 대해서는 여러 패키지 전략이 존재했다.

첫 번째 구조는 다음과 같았다.
여기서 독특한 점은 application의 usecase와 event 였다.
domain아래에 서비스가 존재하는데 application에서 별도로 usecase라는 행위와 event라는 행위를 구분한 점을 처음 발견했다.

com.example.project
 ├── domain            # 핵심 도메인 로직
 │   ├── order         # 주문 도메인
 │   │   ├── entity    # 주문 관련 엔티티
 │   │   ├── repository # 주문 저장소 인터페이스
 │   │   ├── service   # 주문 비즈니스 로직
 │   │   ├── dto       # 주문 관련 DTO
 │   │   ├── exception # 주문 도메인 관련 예외
 │   │   └── command   # 주문 명령 모델 (CQRS 적용 시)
 │   │
 │   ├── payment       # 결제 도메인
 │   ├── user          # 유저 도메인
 │   ├── store         # 매장 도메인
 │
 ├── application       # 도메인 서비스들을 조합하는 계층 (주문 + 결제 처리 등)
 │   ├── usecase       # 주요 유스케이스 처리 (서비스보다 상위 개념)
 │   ├── event         # 도메인 이벤트 관련 클래스
 │
 ├── infrastructure    # 인프라 계층 (외부 시스템, DB, API 연동 등)
 │   ├── config        # 공통 설정 (WebClient, Security, DB 설정 등)
 │   ├── client        # 외부 API 연동 (WebClient 사용)
 │   ├── persistence   # DB 관련 코드 (JPA, MyBatis 등)
 │
 ├── common            # 공통 유틸리티 및 예외 처리
 │   ├── exception     # 전역 예외 처리
 │   ├── util          # 공통 유틸 클래스
 │
 ├── api               # 컨트롤러 및 API 관련 코드
 │   ├── controller    # REST API 컨트롤러
 │   ├── dto           # API 요청/응답 DTO

이 구조의 역할을 먼저 정의해보자면 다음과 같다.

  • domain : 비즈니스 로직(핵셈 도메인 모델)
  • application : 여러 도메인을 조합하는 계층(유스케이스, 이벤트 처리)
  • infrastructure : DB, WebClient, Redis, 외부 API등 concrete한 객체 또는 인프라 코드
  • api : 컨트롤러 및 API 관련 코드
  • common : 공통 유틸리티 및 예외 처리

수직 분리방식의 패키지 구조

위 패키지 구조에서 서비스 규모가 작거나 중간 정도일때에는 domain별로 패키지를 분류하여 관련 코드가 한 곳에 모이게 할 수도 있다.

com.example.project
 ├── domain
 ├── application
 ├── infrastructure
 ├── api
 ├── common

application패키지의 역할

다른 패키지는 역할이 이해가 갔지만 처음보는 usecase와 event처리는 예시가 필요했다.

  • Order 엔티티를 생성하고 저장한다.
  • 그다음 결제 처리를 외부서비스에서 호출한다.
  • 주문 생성 이벤트를 발생시켜 구독한 객체들에게 변화를 알린다.

유즈케이스는 유저의 행위 중심을 말하는 것으로 알고 있는데 여러 도메인들이 행하는 행위를 이 계층에서 복합적으로 처리하는 방식이였다.

@RequiredArgsConstructor
@Service
public class CreateOrderUseCase {

    private final OrderRepository orderRepository;
    private final PaymentService paymentService;
    private final EventPublisher eventPublisher;  // 이벤트 발생기

    @Transactional
    public Order createOrder(UUID customerId, UUID storeId, int totalPrice) {
        // 1. 주문 생성
        Order order = new Order(customerId, storeId, totalPrice);
        orderRepository.save(order);

        // 2. 결제 처리 (외부 서비스 호출)
        paymentService.processPayment(customerId, totalPrice);

        // 3. 이벤트 발행 (주문 생성 이벤트)
        eventPublisher.publish(new OrderCreatedEvent(order.getId(), customerId, storeId, totalPrice));

        return order;
    }
}

Event 패키지의 역할

보다시피 위에서 수행한 비즈니스 로직에 대해 다른 도메인이 서비스에 대해서 처리할 수 있도록 알림을 보내거나, 후처리를 할 수 있도록 로직을 생성하는 역할이었다.

@Slf4j
@Component
@RequiredArgsConstructor
public class OrderEventListener {

    private final NotificationService notificationService;

    @EventListener
    public void handleOrderCreated(OrderCreatedEvent event) {
        log.info("Handling order created event: {}", event);

        // 1. 알림 전송
        notificationService.sendOrderNotification(event.getCustomerId(), event.getOrderId());

        // 2. 기타 후속 작업 수행 가능 (예: 포인트 적립, 통계 업데이트 등)
    }
}

@Component
@RequiredArgsConstructor
public class EventPublisher {
    private final ApplicationEventPublisher applicationEventPublisher;

    public void publish(Object event) {
        applicationEventPublisher.publishEvent(event);
    }
}

이 구조의 핵심

  • UseCase는 여러 도메인 서비스를 조합하여 하나의 기능을 수행한다.
  • Event는 특정 도메인이 로직이 실행 된 후, 비동기적으로 다른 도메인에 영향을 주는 작업을 처리하는 것이다.

이 과정에서 드는 다음 궁금증은 유즈케이스에서 직접 서비스를 호출하는게 괜찮을까?였다.

유즈케이스에서의 서비스 직접 호출

내가 정리한 내용은 다음과 같았다.

  • 유저의 요청을 받아 적절한 도메인 서비스와 리포지토리를 호출하면서도, 도메인 규칙을 침범하지 않아야 한다.
  • UseCase는 도메인 모델을 조합해서 실행하는 역할이기 때문에 여러 도메인 서비스를 조합해야하는 경우 직접 서비스 호출이 가능하지만 직접 도메인 엔티티 내부의 상태를 변경하는 것은 만든시 도메인 엔티티의 메서드를 호출해야 한다는 결론이었다.

잘못된 UseCase (도메인 엔티티를 직접 조작)

@Transactional
public void createOrder(UUID customerId, UUID storeId, int totalPrice) {
    Order order = new Order();  // ❌ 직접 객체 생성
    order.setCustomerId(customerId);
    order.setStoreId(storeId);
    order.setTotalPrice(totalPrice);

    orderRepository.save(order); // 도메인 서비스 없이 바로 저장
}

올바른 UseCase

  • Order 객체는 애플리케이션 레이어가 아닌 도메인 레이어에서 생성되어야 한다. 만약 new 키워드로 생성되면 Order의 생성규칙이 애플리케이션 레이어에 노출되기 때문에 유지보수가 어렵고, Order 객체 생성 규칙이 변경되면 여러 Use Case를 수정해야 하는 문제가 발생한다. 그러므로 createNewOrder를 사용하면 Order생성은 캡슐화하고 유지보수에도 용이해진다. 또한 도메인에서 생성되어야 하는 원칙을 지킬 수 있다.

  • 유즈케이스는 도메인 객체를 저장할 책임을 가진다. 하지만 save() 자체는 도메인의 책임이 아니라 Repository의 책임이다. 도메인 내부에 orderRepository.save 와 같은 코드가 들어가게 되면 도메인 객체가 영속성 레이어에 종속되므로 순수한 도메인 모델이 아니게 된다. 그러므로 유즈케이스에서 호출하는게 맞다.

  • 주문 생성과 결제 처리는 별개의 도메인이므로, Order 도메인이 Payment 도메인에 직접 접근하면 안된다. 이를 유즈케이스에서 대신 도메인 서비스를 호출해서 결제를 처리하는 것이 맞다. 만약 Order 내부에서 payment관련 메서드가 직접 호출된다면 도메인 간 결합도가 높아지기 때문에 Order는 payment없이 생성될 수도 있는 비즈니스가 생기면, 불필요한 의존성 때문에 Order를 수정해야 한다.

  • 유즈케이스는 도메인 이벤트를 발행하는 역할을 담당할 수 있다. OrderCreatedEvent를 발생시키면, 다른 서비스가 Order의 상태 변경을 감지하고 비동기적으로 처리 가능하다.

@RequiredArgsConstructor
@Service
public class CreateOrderUseCase {

    private final OrderRepository orderRepository;
    private final PaymentService paymentService;
    private final EventPublisher eventPublisher;

    @Transactional
    public Order createOrder(UUID customerId, UUID storeId, int totalPrice) {
        //  Order 객체 생성은 도메인 내부에서 수행
        Order order = Order.createNewOrder(customerId, storeId, totalPrice);

        orderRepository.save(order);

        // 도메인 서비스 활용 (비즈니스 로직 포함)
        paymentService.processPayment(customerId, totalPrice);

        eventPublisher.publish(new OrderCreatedEvent(order.getId(), customerId, storeId, totalPrice));

        return order;
    }
}

이번엔 도메인을 분리하는 과정에서 생긴 의문이다. 매장과 메뉴는 동일한 콘텍스트에 둬야 할까, 다른 콘텍스트에 둬야할까?

매장과 메뉴의 종속성을 고려해봤을때 항상 함께 존재해야 하므로, 비즈니스 적으로 강하게 결합되어 있다고 볼 수 있다. 매장이 없으면 메뉴도 의미가 없기 때문이다. 그래서 하나의 트랜잭션 내에서 관리되어야겠다고 생각했다.
그래서 Store와 Menu는 같은 BoundedContext라고 정의했다.

[Store]  
 ├── id  
 ├── name  
 ├── address  
 ├── owner_id  
 ├── menus (List<Menu>)  ❗ 메뉴가 직접 포함됨  

[Menu]  
 ├── id  
 ├── store_id  (❗ Store에 종속)  
 ├── name  
 ├── price  

하지만 이게 완전히 정답은 아니다. 만약 메뉴가 여러 매장에서 공유가 가능하다거나, 매장과 별도로 등록이 가능한 시스템이라고 하면 Menu가 별도의 비즈니스를 가져야 하고, Store와 Menu는 서로 영향을 주지 안하야 하므로 이때는 분리하는게 맞다.

[Store Bounded Context]  
 ├── Store  
 ├── StoreService  
 ├── StoreRepository  

[Menu Bounded Context]  
 ├── Menu  
 ├── MenuService  
 ├── MenuRepository  

분산환경을 배우면서 느낀점은 설계가 점점 중요해지고 있다는 것이다. 각 도메인의 독립성을 중요시하고 서로의 변화를 어떻게 처리할 것이지, 서비스에서 다른 도메인이 필요할 경우 어떻게 영향을 미치지 않고 처리해야 하는지 각 도메인을 매우 이기적으로 독립시켜야 한다는 생각이 배우면 배울수록 드는 생각이다.

728x90
반응형
SMALL

'TIL' 카테고리의 다른 글

[DDD 아키텍처/잡담] 도메인 주도 설계를 왜 해야 할까?  (0) 2025.02.28
[TIL] 모놀리딕 스프링 부트 프로젝트를 수행하고 내가 공부해야 할 것들  (0) 2025.02.26
[Spring Security 예외 처리] 인증 및 인가에 대한 예외처리 방법  (0) 2025.02.25
[TIL] Stream 활용하기, CI/CD 파이프라인 개발, 엔티티 개발 협업  (0) 2025.02.20
[TIL] Filter예외 처리, 서버 배포  (0) 2025.02.19
  1. 패키지 구조
  2. 수직 분리방식의 패키지 구조
  3. application패키지의 역할
  4. Event 패키지의 역할
  5. 이 구조의 핵심
  6. 유즈케이스에서의 서비스 직접 호출
  7. 잘못된 UseCase (도메인 엔티티를 직접 조작)
  8. 올바른 UseCase
  9. 이번엔 도메인을 분리하는 과정에서 생긴 의문이다. 매장과 메뉴는 동일한 콘텍스트에 둬야 할까, 다른 콘텍스트에 둬야할까?
'TIL' 카테고리의 다른 글
  • [DDD 아키텍처/잡담] 도메인 주도 설계를 왜 해야 할까?
  • [TIL] 모놀리딕 스프링 부트 프로젝트를 수행하고 내가 공부해야 할 것들
  • [Spring Security 예외 처리] 인증 및 인가에 대한 예외처리 방법
  • [TIL] Stream 활용하기, CI/CD 파이프라인 개발, 엔티티 개발 협업
공부하고 기억하는 공간
공부하고 기억하는 공간
IT 비전공자로 시작하여 훌륭한 개발자가 되기 위해 공부하고 있는 공간입니다. 틀린 내용이나 부족한 부분이 있으면 댓글로 알려주세요 바로 수정하겠습니다.
IT - railroadIT 비전공자로 시작하여 훌륭한 개발자가 되기 위해 공부하고 있는 공간입니다. 틀린 내용이나 부족한 부분이 있으면 댓글로 알려주세요 바로 수정하겠습니다.
    250x250
  • 공부하고 기억하는 공간
    IT - railroad
    공부하고 기억하는 공간
  • 전체
    오늘
    어제
    • 분류 전체보기 (325) N
      • 면접 준비 (22) N
        • OS (6)
        • Spring Security (0)
        • Java (3)
        • DB (11) N
        • Network (3)
      • ElasticSearch (2)
      • Kafka (4)
      • Spring (22)
        • Spring Cloud (7)
        • Security6 (5)
        • JPA (12)
        • 프로젝트 리팩토링 회고록 (4)
        • Logging (8)
        • Batch (2)
      • Redis (17)
        • Redis 개념 (8)
        • Redis 채팅 (5)
        • Redis 읽기쓰기 전략 (1)
      • AWS (11)
      • 리눅스 (29)
        • 리눅스 마스터 2급 (5)
        • 네트워크(기초) (7)
        • 리눅스의 이해 (6)
        • 리눅스의 설치 (2)
        • 리눅스 운영 및 관리 (6)
      • JAVA-기초 (16)
        • JAVA기본 (11)
        • Design Pattern (5)
      • JSP (27)
        • JSP 기본 개념 (10)
        • JSP (1)
      • SQL (1)
      • TIL (36)
      • 문제 풀이 (2)
        • Programmers (9)
        • 백준 문제풀이 (28)
      • JavaScript (10)
      • HTML (17)
      • Ngrinder (1)
        • Ngrinder 문서 정리 (1)
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

      레디스
      자바기초
      HTML
      Spring Data Redis
      JavaScript
      자바 알고리즘
      Spring
      프로그래머스
      CSS
      JSP
      Til
      redis 채팅
      jsp request
      자바 면접질문
      리눅스마스터2급
      자바
      스프링프레임워크
      자바 면접
      자바스크립트
      JS
      리눅스마스터2급정리
      리눅스
      백준
      spring redis
      자바 반복문
      springsecurity
      Springframework
      jsp기초
      java
      redis
    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.3
    공부하고 기억하는 공간
    [MSA] DDD 구조 고려한 패키지를 생성하는 방법

    개인정보

    • 티스토리 홈
    • 포럼
    • 로그인
    상단으로

    티스토리툴바

    단축키

    내 블로그

    내 블로그 - 관리자 홈 전환
    Q
    Q
    새 글 쓰기
    W
    W

    블로그 게시글

    글 수정 (권한 있는 경우)
    E
    E
    댓글 영역으로 이동
    C
    C

    모든 영역

    이 페이지의 URL 복사
    S
    S
    맨 위로 이동
    T
    T
    티스토리 홈 이동
    H
    H
    단축키 안내
    Shift + /
    ⇧ + /

    * 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.