|

애그리거트는 왜 어렵게 느껴질까

DDD 애그리거트 경계를 설명하는 대표 이미지
애그리거트는 객체 묶음이 아니라 무결성과 트랜잭션 경계를 다루는 개념에 가깝다

애그리거트는 왜 어렵게 느껴질까라는 질문에는 이유가 있습니다. 이름만 보면 여러 객체를 묶는 개념처럼 보이지만, 실제로는 무결성을 어디까지 한 번에 지킬 것인가를 정하는 문제에 더 가깝기 때문입니다.

이번 글에서는 aggregate root, outside reference, transaction boundary 관점에서 왜 이 개념이 어렵고, 어디서 자주 잘못 잡히는지 정리하겠습니다.

핵심 정의는 Martin Fowler의 DDD Aggregate 글이 가장 짧고 분명합니다. 이 글에서는 그 정의를 실무 판단 기준 쪽으로 옮겨 설명합니다.



애그리거트라는 말이 왜 처음부터 어렵게 느껴질까

애그리거트는 보통 객체지향을 조금 배운 뒤 DDD로 넘어올 때 처음 만나게 되는 경우가 많습니다. 그런데 이 단어는 일상 언어에서도 잘 쓰지 않고, 클래스나 인터페이스처럼 바로 손에 잡히는 코드 요소도 아닙니다. 그래서 처음부터 “이게 정확히 뭐지?”라는 느낌이 생기기 쉽습니다.

게다가 설명을 들으면 보통 “여러 객체를 하나의 단위로 묶는다”라는 문장이 먼저 나옵니다. 이 문장만 보면 그냥 객체 묶음이나 컬렉션 비슷한 것으로 오해하기 쉽습니다. 하지만 실무에서 중요한 것은 묶음 자체가 아니라, 어떤 규칙을 한 번에 보호할 것인가입니다.

애그리거트는 무엇인가

Martin Fowler는 DDD aggregate를 하나의 단위처럼 다룰 수 있는 domain object cluster로 설명합니다. 예를 들어 주문과 주문 항목은 별도 객체일 수 있지만, 주문이라는 하나의 aggregate로 다루는 것이 자연스러울 수 있습니다.

즉 중요한 것은 객체 개수가 아니라, 어떤 규칙을 한 단위로 묶어야 하는가입니다.


aggregate root가 왜 중요한가

외부에서 aggregate 안쪽 객체를 아무 데나 직접 만지지 못하게 하려면 입구가 필요합니다. 그 역할을 하는 것이 aggregate root입니다. Fowler도 외부 참조는 aggregate root로만 가야 한다고 설명합니다.

이렇게 해야 root가 aggregate 전체의 무결성을 지키는 관문이 될 수 있습니다.



주문 예시로 보면 조금 더 쉬워진다

주문과 주문 항목을 떠올려보겠습니다. 주문에는 상태, 총액, 할인 규칙, 취소 가능 여부 같은 규칙이 있고, 주문 항목에는 수량과 상품 정보가 있습니다. 여기서 보통 중요한 것은 “주문 항목 하나를 바꿀 때 주문 전체 규칙도 같이 지켜져야 하는가”입니다.

class Order {
    private final List<OrderLine> lines = new ArrayList<>();
    private boolean paid;

    public void addLine(Product product, int quantity) {
        if (paid) throw new IllegalStateException("결제 완료 후 변경 불가");
        lines.add(new OrderLine(product, quantity));
    }

    public void changeQuantity(long lineId, int quantity) {
        if (paid) throw new IllegalStateException("결제 완료 후 변경 불가");
        OrderLine line = findLine(lineId);
        line.changeQuantity(quantity);
    }
}

class OrderLine {
    private Product product;
    private int quantity;

    void changeQuantity(int quantity) {
        if (quantity <= 0) throw new IllegalArgumentException();
        this.quantity = quantity;
    }
}

이 예시에서 외부가 OrderLine을 직접 수정하게 두면, 결제 완료 후 변경 금지 같은 규칙이 쉽게 깨질 수 있습니다. 그래서 외부는 Order root를 통해서만 접근하고, Order가 전체 규칙을 지키게 만드는 쪽이 aggregate 감각에 더 가깝습니다.

즉 aggregate root는 단순한 대표 객체가 아니라, 밖에서 안쪽 규칙을 마음대로 깨지 못하게 막는 입구 역할을 합니다.

왜 경계를 잘못 잡기 쉬울까

  • 연관 관계가 많아 보이면 한 번에 다 묶고 싶어진다
  • 화면에서 같이 보인다는 이유로 같은 aggregate처럼 느껴진다
  • 트랜잭션을 편하게 처리하려고 경계를 과도하게 크게 잡는다

하지만 너무 크게 잡으면 수정 하나에 잠금 범위와 영향 범위가 커지고, 너무 작게 잡으면 무결성을 밖에서 계속 조정해야 해서 규칙이 흩어집니다. 어려운 이유는 이 균형점이 도메인마다 다르기 때문입니다.


트랜잭션 경계와 같이 봐야 한다

애그리거트가 중요한 이유 중 하나는 트랜잭션 경계와 연결되기 때문입니다. Fowler도 트랜잭션은 aggregate boundary를 넘지 말아야 한다고 말합니다.

즉 “이 변경은 한 번의 일관된 상태 전환으로 끝나야 하는가”를 물어보면 aggregate 경계가 조금 더 선명해집니다.



너무 크게 잡아도 문제, 너무 작게 잡아도 문제

너무 크게 잡으면

  • 한 번의 수정에 너무 많은 객체가 함께 묶인다
  • 잠금 범위와 로딩 범위가 커진다
  • 자주 안 바뀌는 규칙까지 같이 끌려온다

너무 작게 잡으면

  • 무결성을 여러 서비스가 나눠서 지켜야 한다
  • 정책이 중복되고 규칙이 흩어진다
  • 트랜잭션 경계를 밖에서 억지로 맞추게 된다

그래서 aggregate boundary는 “크게 묶을수록 안전하다”는 식으로 정할 수 없습니다. 실제로는 수정 빈도와 무결성 규칙이 같이 움직이는 범위를 찾는 쪽에 더 가깝습니다.

실무에서 더 도움이 되는 질문

  1. 어떤 규칙을 한 번에 반드시 지켜야 하는가
  2. 외부에서 직접 만지면 안 되는 상태는 무엇인가
  3. 수정 hotspot이 어디에 몰리는가
  4. 한 트랜잭션 안에서 꼭 끝나야 하는 일은 무엇인가

이 질문에 답하다 보면, 단순한 연관 관계보다 무결성 기준으로 경계를 잡게 됩니다.


자주 하는 오해

연관 객체가 많으면 다 한 aggregate다

아닙니다. 연관이 있다는 것과 같은 무결성 경계에 있어야 한다는 것은 다릅니다.

aggregate는 컬렉션 비슷한 말이다

Fowler도 이 오해를 분명히 짚습니다. aggregate는 domain concept이고, 리스트나 맵 같은 generic collection과 같은 말이 아닙니다.

배경 흐름은 트랜잭션 스크립트와 도메인 모델 차이, 빈혈 도메인 모델 글과 함께 보면 더 잘 이어집니다.


마무리

애그리거트가 어려운 이유는 정의가 복잡해서가 아니라, 결국 무결성과 트랜잭션 경계를 어디까지 한 번에 다룰지 정해야 하기 때문입니다.

그래서 애그리거트를 공부할 때는 “무엇을 묶을까”보다, 어떤 규칙을 한 단위로 보호해야 할까를 먼저 묻는 편이 훨씬 실용적입니다.

함께보면 좋은 글