DomainService는 Repository를 몰라도 될까?

2026. 5. 29. 15:58·루퍼스/WIL

이번 주에는 DDD 관점에서 Product, Brand, Like, Order 기능을 구현하면서 DomainService가 Repository를 직접 참조해도 되는가를 가장 많이 고민했다.

처음에는 Service 안에서 필요한 데이터를 직접 조회하는 방식이 자연스러워 보였다.

public class OrderService {

    private final ProductRepository productRepository;
    private final StockRepository stockRepository;

    public OrderModel createOrder(Long userId, List<OrderItemCommand> items) {
        ProductModel product = productRepository.findById(...);
        StockModel stock = stockRepository.findByProductId(...);

        stock.deduct(...);
        ...
    }
}

이렇게 하면 호출부는 단순하다.

Controller나 Facade에서는 orderService.createOrder()만 호출하면 되고, 필요한 조회는 Service 내부에서 처리된다.

하지만 구현을 진행하면서 이 구조가 정말 DomainService다운 구조인지 의문이 들었다.


Vaughn Vernon과 Harry Percival의 관점

참고한 글에서는 Vaughn Vernon과 Harry Percival의 관점을 비교한다.

Domain model에서는 직접 repository를 사용하지 말자.

두 사람 모두 Aggregate가 Repository에 직접 의존하는 것은 지양해야 한다는 방향은 비슷하다.

다만 DomainService에서 Repository를 사용해도 되는지에 대해서는 관점 차이가 있다.

  • Vaughn Vernon
    • Aggregate의 Repository 직접 의존은 지양한다.
    • 다만 복잡한 도메인 의존성을 Application Service가 모두 풀어내기 어려운 경우, DomainService의 Repository 사용을 제한적으로 허용할 수 있다고 본다.
  • Harry Percival
    • Repository 사용은 Application Service의 책임에 가깝다고 본다.
    • DomainService는 필요한 객체를 전달받아 비즈니스 로직을 수행하는 것이 좋다고 본다.

이번 과제에서는 Harry Percival의 관점에 더 가깝게 구현해보기로 했다.

즉, Repository 조회와 저장은 Facade가 담당하고, DomainService는 전달받은 도메인 객체만 조작하도록 했다.

참고한 글:
https://littlemobs.com/blog/using-repository-in-domain-model/


처음 구조의 문제

처음 구조에서는 OrderService가 너무 많은 책임을 가지고 있었다.

  • Product 조회
  • Stock 조회
  • 재고 차감
  • Order 생성
  • Order 저장
  • Payment 호출
  • 결제 실패 시 보상 처리

이 구조는 호출부는 단순하지만, 테스트가 복잡해진다.

내가 검증하고 싶은 것은 사실 다음과 같은 도메인 규칙이었다.

  • 재고가 충분하면 차감된다.
  • 재고가 부족하면 예외가 발생한다.
  • 주문 항목이 생성된다.
  • 주문 총액이 계산된다.
  • 결제 실패 시 차감했던 재고가 복구된다.

그런데 OrderService가 Repository를 직접 들고 있으면, 이 규칙을 테스트하기 위해 Repository Mock을 먼저 준비해야 한다.

given(productRepository.findById(...)).willReturn(...);
given(stockRepository.findByProductId(...)).willReturn(...);

결국 테스트의 관심사가 도메인 규칙이 아니라 Repository 세팅으로 흐르게 된다.


Facade와 DomainService의 책임 분리

그래서 구조를 다음처럼 나눴다.

OrderFacade
- Repository 조회
- Order 저장
- 트랜잭션 관리
- PaymentService 호출
- 예외 변환

OrderService
- 재고 차감
- 주문 항목 생성
- 주문 총액 확정
- 재고 복구

OrderFacade는 Application Layer로서 유스케이스 흐름을 조율한다.

@Transactional(noRollbackFor = PaymentFailedException.class)
public OrderInfo createOrder(Long userId, List<OrderItemCommand> items) {
    List<OrderLine> orderLines = items.stream()
        .map(cmd -> new OrderLine(
            findProductOrThrow(cmd.productId()),
            findStockOrThrow(cmd.productId()),
            cmd.quantity()
        ))
        .toList();

    OrderModel order = orderService.createWithStockDeduction(userId, orderLines);
    OrderModel saved = orderRepository.save(order);

    PaymentResult result = paymentService.process(saved.getId(), saved.getTotalPrice());

    if (result.isSuccess()) {
        saved.complete();
        return OrderInfo.from(saved);
    }

    saved.cancel();
    orderService.restoreStocks(orderLines);

    throw new PaymentFailedException("PAYMENT_FAILED: " + result.failureReasonOrDefault());
}

반면 OrderService는 Repository를 모른다.

public OrderModel createWithStockDeduction(Long userId, List<OrderLine> orderLines) {
    OrderModel order = new OrderModel(userId);

    for (OrderLine line : orderLines) {
        line.stock().deduct(line.quantity());

        order.addItem(new OrderItemModel(
            order,
            line.product().getId(),
            line.product().getName(),
            line.product().getPrice(),
            line.quantity()
        ));
    }

    order.confirmTotalPrice();
    return order;
}

이제 OrderService는 전달받은 도메인 객체만 조작한다.

덕분에 OrderServiceTest에서는 Repository Mock 없이 도메인 규칙만 검증할 수 있다.


OrderLine으로 문맥 묶기

처음에는 Product, Stock, OrderItemCommand를 각각 List로 넘길 수도 있다고 생각했다.

List<ProductModel> products
List<StockModel> stocks
List<OrderItemCommand> items

하지만 이 방식은 세 리스트의 인덱스가 항상 일치해야 한다는 암묵적인 규칙을 만든다.

products[0] ↔ stocks[0] ↔ items[0]
products[1] ↔ stocks[1] ↔ items[1]

이런 구조는 나중에 정렬, 필터링, 중복 제거 과정에서 쉽게 깨질 수 있다.

그래서 주문 한 줄에 필요한 문맥을 OrderLine으로 묶었다.

public record OrderLine(
    ProductModel product,
    StockModel stock,
    int quantity
) {
}

이제 OrderLine 하나가 “이 상품을, 이 재고에서, 이 수량만큼 주문한다”는 의미를 가진다.


이번 주에 배운 점

이번 주에 가장 크게 배운 점은 Application Layer를 얇게 만든다는 것이 단순히 코드 줄 수를 줄인다는 의미는 아니라는 것이다.

처음에는 Facade에 Repository 조회 코드가 늘어나는 것이 어색했다.

하지만 Repository 조회, 저장, 트랜잭션, 외부 API 호출은 유스케이스 흐름에 가깝다.
반면 재고 차감, 주문 항목 생성, 주문 총액 계산은 도메인 규칙에 가깝다.

이 둘을 분리하니 테스트의 초점이 더 명확해졌다.

정리하면 이번 주의 결론은 다음과 같다.

Facade는 필요한 도메인 객체를 조회한다.
DomainService는 전달받은 객체만 조작한다.
DomainService는 Repository를 모른다.

물론 이 구조가 항상 정답이라고 생각하지는 않는다.

Vaughn Vernon의 관점처럼 복잡한 도메인 의존성을 Application Layer가 모두 풀어내기 어려운 경우, DomainService가 일부 책임을 나누는 선택도 가능하다.

하지만 이번 과제에서는 도메인 로직을 순수 Java 단위 테스트로 검증 가능하게 만드는 것이 목표였기 때문에, Repository 의존성을 DomainService 밖으로 밀어내는 방향이 더 적절하다고 판단했다.

저작자표시 (새창열림)

'루퍼스 > WIL' 카테고리의 다른 글

외부 PG 장애를 견디는 비동기 결제 설계  (0) 2026.06.26
인덱스/비정규화/캐시  (0) 2026.06.19
주문과 결제 사이에서 재고는 언제 차감해야 할까?  (0) 2026.06.12
설계 문서  (0) 2026.05.22
TDD  (0) 2026.05.15
'루퍼스/WIL' 카테고리의 다른 글
  • 인덱스/비정규화/캐시
  • 주문과 결제 사이에서 재고는 언제 차감해야 할까?
  • 설계 문서
  • TDD
쿤쿤쿤
쿤쿤쿤
  • 쿤쿤쿤
    QQQ
    쿤쿤쿤
  • 전체
    오늘
    어제
    • 분류 전체보기 (70)
      • Spring (6)
      • JAVA (5)
      • System Design (0)
      • AI (0)
      • DB (1)
      • Linux (1)
      • Git (1)
      • 트러블 슈팅 (3)
      • 오픈소스 (0)
      • CS 기초 지식 (0)
        • 네트워크 (0)
        • 운영체제 (0)
        • 알고리즘 (0)
        • 개발상식 (0)
      • 알고리즘 (45)
        • 구현 (Implementation) (0)
        • DFS, BFS (0)
        • 완전탐색 (Bruteforce) (0)
        • 그리디 (Greedy) (0)
        • 투포인터 (Two Pointer) (0)
        • 이분탐색 (Binary Search) (0)
        • 스택, 큐 (Stack, Queue) (0)
        • DP (Dynamic Programming) (0)
        • 다익스트라 (Dijkstra) (0)
        • 구간합 (Prefix) (0)
        • 문제모음 (0)
        • 백준 (20)
        • 2020 KAKAO (7)
        • 2021 KAKAO (7)
        • 2022 KAKAO (7)
        • 프로그래머스 (0)
        • Softeer (3)
      • 기타 (0)
      • 루퍼스 (7)
        • WIL (6)
        • WRITING (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    dipendency injection
    di
    의존성 주입
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
쿤쿤쿤
DomainService는 Repository를 몰라도 될까?
상단으로

티스토리툴바