DDD 애그리거트 패턴

임태환·2025년 2월 14일

study

목록 보기
6/10

이전 게시글에서도 언급했었던 애그리거트에 대해 상세하게 설명하려한다.
이전 게시글

애그리거트란?

도메인 주도 설계(DDD)에서 사용되는 핵심 설계 패턴으로 도메인 모델의 복잡성을 줄이고
데이터의 일관성을 유지하기 위해 관련된 객체들을 하나의 단위로 묶어 관리하는 개념

애그리거트를 사용해야하는 이유?

불분명한 경계 문제?

도메인 모델에서 객체들이 복잡하게 얽혀 있을 때 어떤 객체가 어떤 작업을 담당해야하는지
명확하지 않은 문제

불분명한 경계 문제 예시

// 불변값 문제 예시
public class Address {
    private String city;
    private String street;

    public Address(String city, String street) {
        this.city = city;
        this.street = street;
    }

    // Getter와 Setter 제공
    public String getCity() {
        return city;
    }

    public void setCity(String city) { // 외부에서 직접 수정 가능
        this.city = city;
    }

    public String getStreet() {
        return street;
    }

    public void setStreet(String street) { // 외부에서 직접 수정 가능
        this.street = street;
    }
}

해당 코드는 Address 객체를 외부에서 setter로 선언한 메서드를 통해 수정이 가능하다
즉 애그리거트 내부의 상태를 외부에서 변경할 수 있어 데이터 일관성이 깨진다

// 트랜잭션 문제 예시
public class OrderService {
    private final OrderRepository orderRepository;
    private final PaymentRepository paymentRepository;

    public OrderService(OrderRepository orderRepository, PaymentRepository paymentRepository) {
        this.orderRepository = orderRepository;
        this.paymentRepository = paymentRepository;
    }

    public void placeOrderAndProcessPayment(Order order, Payment payment) {
        // 트랜잭션 시작
        orderRepository.save(order); // 주문 저장

        // 결제 처리 (같은 트랜잭션 내에서 처리)
        payment.completePayment();
        paymentRepository.save(payment);

        // 트랜잭션 종료
    }
}

트랜잭션 시작 후 결제 처리부분을 확인해보면
주문 저장과 결제 처리부분이 같은 트랜잭션 내에서 이루어진다.
만약 주문 저장 후 결제 부분에서 문제 발생 시 주문 데이터도 롤백이 되어버린다.

발생하는 문제점

데이터 불일치

  • 한 객체를 수정했는데 관련된 다른 객체가 제대로 업데이트 되지 않아 데이터 일관성 깨지는 문제

트랜잭션 충돌

  • 여러 트랜잭션이 동시에 실행되면서 데이터 경합이나 충돌 발생

불변값 문제

  • 밸류 객체가 외부에서 직접 수정 가능 시 애그리거트 내부 상태의 무결성이 깨짐

애그리거트의 구성 요소

에그리거트는 하나의 루트 엔티티와 하나 이상의 기타 엔티티와 밸류 객체로 구성된다.

루트 엔티티

  • 애그리거트를 대표하는 중심 엔터티로 외부에서 애그리거트 접근 시 반드시 루트를 통해야함

기타 엔티티

  • 루트 엔티티와 연관된 하위 엔티티로 루트 엔티티 내부에서만 관리

밸류 객체

  • 식별자가 없고 값 자체로 의미를 가지는 불변 객체 (애그리거트의 속성)
// 구성 요소 예시 코드
public class Order { // 루트 엔티티
    private Long id; // 주문 ID
    private String customerName; // 고객 이름
    private List<OrderItem> items = new ArrayList<>(); // 하위 엔터티 리스트
    private Address shippingAddress; // 배송 주소 (밸류 객체)

    public Order(Long id, String customerName, Address shippingAddress) {
        this.id = id;
        this.customerName = customerName;
        this.shippingAddress = shippingAddress;
    }

    public void addItem(String productName, int quantity) {
        items.add(new OrderItem(productName, quantity)); // 하위 엔터티 추가
    }

    public void changeShippingAddress(Address newAddress) {
        this.shippingAddress = newAddress; // 밸류 객체 변경
    }
}

public class OrderItem { // 하위 엔터티
    private String productName;
    private int quantity;

    public OrderItem(String productName, int quantity) {
        this.productName = productName;
        this.quantity = quantity;
    }
}

public class Address { // 밸류 객체
    private final String city;
    private final String street;

    public Address(String city, String street) {
        this.city = city;
        this.street = street;
    }
}
항목설명예시
작업 범위 설정로드/수정/삭제 같은 작업은 반드시 루트를 통해 수행루트 엔티티.addItem()으로 수정 후 저장
작업이 전체에 적용되는 이유일관성과 무결성을 유지하기 위해 하나의 트랜잭션으로 처리주문 항목 추가와 재고 감소를 동시에 처리
지연 로딩 문제필요 시점에 데이터를 가져오면서 성능 저하나 N+1 문제가 발생Fetch Join 또는 즉시 로딩(Eager Loading)으로 해결
삭제 시 모두 사라지는 이유하위 데이터는 루트에 종속되며 함께 삭제됨orderRepository.delete(루트 엔티티) → 관련된 모든 OrderItem도 삭제

애그리거트는 일관된 경계

애그리거트는 전체 애그리거트를 업데이트하므로 일관성 문제가 해소되며
업데이트 작업은 애그리거트 루트에서 호출되기 때문에 불변 값이 강제되며
동시성 역시 애그리거트 루트를 버전 번호나 DB락으로 처리한다.

하지만 전체 애그리거트를 업데이트할 필요는 없고 해당 객체와 로우만 업데이트할 수도 있다.

애그리거트 규칙

규칙 1 애그리거트 루트만 참조하라

애그리거트 내부의 하위 엔티티나 밸류 객체는 외부에서 직접 접근 및 참조 불가능

외부에서는 반드시 애그리거트 루트를 통해서만 애그리거트 내부에 접근

캡슐화 및 데이터 일관성 유지를 위해 루트만 참조해야한다.

규칙 2 애그리거트 간 참조는 반드시 기본키를 사용하라

하나의 애그리거트가 다른 애그리거트를 참조할 때 직접 객체를 참조하지 않고
기본키를 사용해야한다.

예시 코드

//잘못된 예시
public class Order {
    private Customer customer; // 고객 객체 직접 참조
}
//올바른 방식
public class Order {
    private Long customerId; // 고객 ID로 참조
}

의문점
위의 예시에서 Order 객체가 customerId를 어떻게 가지고 올까?

방법 1 : 서비스 계층에서 ID를 전달 받는다.
방법 2 : 서비스 계층에서 다른 애그리거트를 조회하여 ID 추출
방법 3 : 이벤트 기반으로 다른 시스템이나 컨텍스트에서 전달 받기

기본키를 사용하여 참조할 경우 의존성을 줄이고 데이터 로딩을 방지할 수 있다.

규칙 3 하나의 트랜잭션으로 하나의 애그리거트를 생성/수정하라

DDD에서는 한 번의 트랜잭션에서 하나의 애그리거트만 생성하거나 수정해야한다.
여러 애그리거트를 동시 수정 시 트랜잭션 경계가 확장되어 복잡성이 증가하며
동시성 문제, 성능 저하가 발생할 수 있다.

만약 애그리거트 간의 상호작용이 필요하면 사가(SAGA) 패턴을 사용하면 된다
SAGA 패턴은 여러 애그리거트를 수정하는 작업을 독립적인 트랜잭션으로 나누어 관리한다.

애그리거트 입도

애그리거트 입도는 하나의 애그리거트가 얼마나 많은 데이터를 포함하고 얼마나 큰 범위를
관리하는지를 의미한다.

애그리거트는 잘게 나누면 좋은데

한 번의 로드나 저장 작업에서 처리해야할 데이터가 줄어든다.
필요한 데이터만 로드하거나 저장하여 성능이 향상된다.

애그리거트가 작을수록 동시성 충돌 가능성이 줄어 동시성 문제가 감소한다.

마이크로 서비스 패턴 책에서는 작으면 작을수록 좋다고하지만
일관성 경계, 트랜잭션 범위, 응집도를 고려하여 크기를 정하는게 좋을 것 같다.

profile
웹 개발자

0개의 댓글