[DDD] 애그리거트(Aggregate)

SIK407·2025년 7월 24일

DDD & Clean Architecture

목록 보기
3/6
post-thumbnail

◽ 애그리거트(Aggregate)

여러 요소나 데이터를 하나로 합친 것

소프트웨어 개발에서 관련된 객체들의 집합을 의미한다.
aggregation: 집합

예를 들어, Order라는 도메인이 있다고 가정하자.
주문 상품, 주문 배송지, 결제정보 등 여러 객체들이 존재한다.

이러한 객체들을 하나의 애그리거트(Aggregate)로 묶어 처리하면 엄청 효율적이다!

@Getter
public class Order {

    private String id;
    private List<OrderLine> orderLines;
    private OrderState state;
    private Money totalAmounts;
    private ShippingInfo shippingInfo;

    public Order(List<OrderLine> orderLines, ShippingInfo shippingInfo) {
        id = UUID.randomUUID().toString();
        setOrderLines(orderLines);
        setShippingInfo(shippingInfo);
        state = OrderState.PAYMENT_WAITING;
    }

// 각종 메소드들 (생략) ~~~

이 Order 클래스를 애그리거트 루트(Aggregate Root) 클래스다.

애그리거트를 사용하면, 연관 도메인을 묶기 때문에 모델 관계를 파악하기가 더 쉽다.

또한 애그리거트 단위로 일관성을 관리하면, 코드도 일목조연하게 작성할 수 있다.

코드의 복잡도도 낮아지기 때문에,
유지보수 및 확장, 변경에 들이는 노력이 줄어든다.


◽ 일관성을 지켜야 된다.

Aggregate Root인 Order 클래스는 배송상태(OrderState)를 가진다.

OrderState state = dto.getState(); // get → 배송 상태를 가져옴
state.setState(PREPARING);         // set → 배송 상태를 변경

지금 이 코드는 개발자가 어디에서 사용하든, 배송 상태가 변경 가능한 코드다.

이 코드의 문제점은 이렇다.

  1. 상태 전이 조건이 없음 && 도메인의 "룰"을 무시
    • 예: 배송이 이미 시작됐는데도 PREPARING으로 되돌릴 수 있음
  2. 누가 상태를 바꾸는지 추적 불가
    • getter/setter는 로직이 없어서 책임이 없음

이러한 상태가 애그리어트의 일관성이 깨진 상태다.
외부에서 직접 필드를 조작해서 규칙을 우회했기 때문이다.

이러한 일관성을 지킬려면 다음과 같은 방법이 있다.

✔ Setter 생성 금지

setter는 일반적으로 필드에 값을 할당하기 위해 사용한다.

아무 값이나 자유롭게 넣을 수 있다는 장점이 있지만..
도메인의 의미를 적절하게 표현할 수 없다.

게다가 단순히 특정 필드를 묶은 것처럼 이해될 수 있으며,
그렇게 개발할 가능성이 높아진다.

@Getter
@Setter
public class Order {

    private String id;
    private List<OrderLine> orderLines;
    private OrderState state;
    private Money totalAmounts;
    private ShippingInfo shippingInfo;
    
}

그냥 특정 필드만 묶어놓은 것일 뿐...
Order 클래스는 데이터를 넣고 가져오는것 외의미 있는 행위나 책임이 없다.

이러한 로직은 보통 한 곳에 응집되지 않고, 유지보수 시 분석 및 수정에 더 많은 시간을 들여야 한다.

// 결제 후, 상태를 '준비 중'으로 변경하는 메소드
public void payment() {
    if (state != OrderState.PAYMENT_WAITING) {
        throw new IllegalStateException(ErrorMessages.ALREADY_PAID.getMsg());
   }

    this.state = OrderState.PREPARING;
}

// '배송 준비 중' -> '배송 중' 으로 변경
public void shipped() {
    if (state != OrderState.PREPARING) {
        throw new IllegalStateException(ErrorMessages.NOT_READY.getMsg());
    }

    this.state = OrderState.SHIPPED;
}

하지만 위 코드처럼 setter를 사용하지 않는다면,
의미가 드러나는 메소드 명을 고민하게 되고, 이에 걸맞는 로직을 응집시켜 구현하게 된다.

이러한 방식의 장점은, 코드를 읽는 사람도
"이 객체가 어떤 상황에서 어떻게 동작하지?"를 쉽게 이해할 수 있고,
비즈니스 규칙도 객체 내부에 응집되어 유지보수가 훨씬 쉬워진다.

이러한 방식은 단순히 getter/setter로 필드만 조작하는 것보다
의도를 명확하게 표현하고, 객체지향적 설계 원칙에도 훨씬 부합한다.


◽ 애그리거트 기능 구현

Aggregate Root는 애그리거트 내부의 다른 객체를 조합하여 기능을 완성해야 한다.

@Getter
public class Order {

    private String id;
    private List<OrderLine> orderLines;
    private OrderState state;
    private Money totalAmounts;
    private ShippingInfo shippingInfo;

    public Order(List<OrderLine> orderLines, ShippingInfo shippingInfo) {
        id = UUID.randomUUID().toString();
        setOrderLines(orderLines);
        setShippingInfo(shippingInfo);
        state = OrderState.PAYMENT_WAITING;
    }

    /**
     * 배송 정보 설정
     */
    private void setOrderLines(List<OrderLine> orderLines) {
        if (isEmptyLOrderLines(orderLines)) {
            throw new IllegalStateException(ErrorMessages.NO_ORDER_LINE.getMsg());
        }

        this.orderLines = orderLines;
        calculateTotalAmounts();
    }
    
    (생략)

Order 객체 내에서, OrderLine을 통해 배송 정보를 설정하면 된다.

profile
감자 그 자체

0개의 댓글