MSA에서 DDD 활용하기

Choi Wontak·2025년 4월 14일

아이쿠MSA

목록 보기
6/12

난이도 ⭐️⭐️
작성 날짜 2025.01.04

고민 내용

이벤트 스토밍을 통해 도메인을 모두 분리했다.
이제 코드를 작성할 때 DDD를 사용해야 한다.

이유는 다음과 같다.

  1. 관련된 도메인만 접근할 수 있도록 마이크로 서비스를 분리했다.
  2. 다른 도메인을 값을 건들게 되면 예상하지 못한 이슈가 발생할 수 있다.

🤔
코드 레벨에서 DDD를 잘 활용하기 위한 규칙은 무엇이 있을까?


찾아보기

값 변경과 Validation은 도메인 내에서

도메인 값을 변경할 때 외부에서 변경하게 되면 예상하지 못한 변화가 생길 수 있기 때문에 도메인을 보호할 수 없다.

final, private 사용 그리고 setter 금지 등을 통해 객체 상태를 보호해야 한다.

public class Money {
    private final int amount;

    public static Money of(int amount) {
        if (amount < 0) throw new IllegalArgumentException("금액은 음수가 될 수 없습니다.");
        return new Money(amount);
    }
}

비즈니스 규칙과 관련된 검증은 도메인 내부에서 진행하는 것이 좋다.
규칙을 집약해두어야, 나중에 기획이 바뀔 때에도 수월하며
도메인 외부 (서비스 등) 계층에서 수정되는 것을 막을 수 있다.

public class Order {
    public void cancel() {
        if (this.status != OrderStatus.WAITING) {
            throw new IllegalStateException("이미 처리된 주문은 취소할 수 없습니다.");
        }
        this.status = OrderStatus.CANCELED;
    }
}

어그리거트 내 변경 사항은 코어 도메인에서만 실행한다.

어그리거트 내 루트 도메인과 연관된 다른 엔티티의 경우에는 루트 도메인을 통해서만 수정, 변경이 가능하다.
다방면에서 변경에 열려있는 경우, 일관된 흐름 파악이 어렵다.

public class Order {
    private Long id;
    private List<OrderItem> items;

    public void addItem(Product product, int quantity) {
        this.items.add(new OrderItem(product, quantity));
    }

    public int totalPrice() {
        return items.stream().mapToInt(OrderItem::price).sum();
    }
}

서비스, 레포지토리 레벨에서 다른 도메인을 변경하지 않는다.

도메인 내에서 작성하기 어려운 로직의 경우, 해당 도메인의 서비스에 작성한다.
단, 해당 서비스는 다른 도메인을 변경할 수 없다.

public class TransferService {
    public void transfer(Account from, Account to, Money amount) {
        from.withdraw(amount);
        to.deposit(amount);
    }
}
  • CQRS

Command Query 분리 전

Command Query 분리 후

출처 : https://wonit.tistory.com/628

Command와 Query에 대한 책임 분리로

  1. 복잡도를 낮춘다.
    Command에 해당하는 로직과 Query에 해당하는 로직이 나뉘어 각자의 역할에만 집중한다.
  2. 성능을 증가시킨다.
    Query 로직에는 Command를 로직이 없기 때문에 DB Lock으로부터 자유로워진다.

DB를 분리한다면, 확실한 차이를 확인할 수 있을 것 같다!

다른 도메인의 값을 변경해야 할 때는 이벤트를 이용한다.

로직 내에 다른 도메인에 대한 변경이나 연관된 커맨드가 발생하는 경우 이벤트를 터뜨려 강결합을 줄인다.

public class Order {
    public void completePayment() {
        this.status = OrderStatus.PAID;
        DomainEvents.raise(new OrderPaidEvent(this.id));
    }
}
@Component
public class OrderEventHandler {
    @EventListener
    public void handle(OrderPaidEvent event) {
        // 알림 발송, 외부 API 호출 등
    }
}

결론

DDD 원칙을 지키기 위해선 평소 작성하던 코드의 형식에서 벗어나야 하는 것 같아 적응까진 시간이 좀 걸릴 것 같다!
그러나 확실히 개발자가 좋아하는 높은 응집도와 낮은 결합도에 도움이 되는 개념인 것 같아 확실히 체화시키고 싶다..!


profile
백엔드 주니어 주니어 개발자

0개의 댓글