Aggregate Root

2023. 6. 4. 20:10Spring Boot/Spring Data JPA

Aggregate Root도메인 주도 설계(DDD: Domain-Driven Design)에서 핵심 개념 중 하나로, 특히 복잡한 비즈니스 로직이 필요한 애플리케이션에서 유용한 설계 방법입니다. 이를 이해하기 위해서는 DDD의 애그리게이트(Aggregate) 개념과 그 안에서의 역할을 이해하는 것이 중요합니다. Spring Data JPA는 이러한 개념을 적용하여 도메인 모델을 더 명확하고 일관성 있게 관리할 수 있도록 도와줍니다.

1. 애그리게이트(Aggregate)란?

애그리게이트는 하나 이상의 객체(엔티티와 값 객체)를 묶어서 한 단위로 취급하는 개념입니다. 이 묶음은 도메인 논리에 의해 긴밀하게 결합된 객체들로 구성됩니다. 애그리게이트는 시스템에서 일정한 경계(boundary)를 설정하여 해당 경계 내에서 객체들 간의 일관성을 보장합니다. 이러한 일관성을 유지하면서, 외부에서는 애그리게이트 내부 구조를 알지 못하더라도 해당 애그리게이트가 제공하는 기능만을 사용하게 됩니다.

2. Aggregate Root(애그리게이트 루트)

애그리게이트 내에 여러 객체가 있을 때, 그중에서 Aggregate Root는 애그리게이트의 진입점 역할을 합니다. 이는 애그리게이트에 속한 객체들에 대한 외부의 접근을 통제하며, 애그리게이트를 대표하는 중심 엔티티가 됩니다. Aggregate Root를 통해서만 애그리게이트 내부의 객체들에 접근할 수 있고, 이 Root는 애그리게이트 내에서 모든 상태 변경 및 일관성 관리를 책임집니다.

Aggregate Root의 특징

  1. 일관성 경계를 설정:
    Aggregate Root는 애그리게이트 내부 객체들이 외부에서 직접 접근되거나 변경되는 것을 막아, 애그리게이트의 상태를 일관되게 유지합니다. 이는 데이터의 무결성을 보장하고 애플리케이션의 복잡한 상태를 쉽게 관리할 수 있도록 합니다.
  2. 객체 간의 관계와 규칙 관리:
    애그리게이트 내의 객체들은 다양한 관계를 가질 수 있으며, Aggregate Root는 이러한 관계 및 규칙을 책임집니다. 내부의 다른 객체들은 Root를 통해서만 간접적으로 조작됩니다.
  3. 애그리게이트의 외부 인터페이스:
    Aggregate Root는 외부와의 유일한 인터페이스로, 애그리게이트 내에서 무슨 일이 벌어지고 있는지 외부에서 알 필요가 없습니다. 외부에서는 오직 Aggregate Root를 통해 애그리게이트를 다루게 됩니다.
  4. Repository와의 연관성:
    Spring Data JPA에서는 애그리게이트 루트만을 위한 Repository가 존재합니다. 다시 말해, Repository는 Aggregate Root에만 해당하며, 애그리게이트 내부의 다른 엔티티들은 직접 Repository를 가지지 않습니다. 이로 인해 데이터를 조작할 때는 항상 Aggregate Root를 통해 접근하게 됩니다.

왜 Aggregate Root가 중요한가?

  • 데이터 일관성: 애그리게이트가 하나의 트랜잭션 경계를 설정하기 때문에, Aggregate Root는 상태 변경을 하나의 단위로 관리하여 데이터 일관성을 보장합니다. 애그리게이트의 일관성은 Root를 통해 관리되고 유지됩니다.
  • 비즈니스 규칙 적용: 비즈니스 규칙이 애그리게이트 내부 객체들에 적용될 때, Root가 이를 조정하여 비즈니스 규칙이 어긋나지 않도록 합니다.
  • 외부 의존성 최소화: Aggregate Root는 애그리게이트 경계를 명확히 정의하기 때문에 외부 의존성을 최소화하고, 애플리케이션의 복잡성을 줄입니다.

3. Aggregate Root의 구조적 예시

3.1 주문 관리 시스템(Order Management System)에서의 예시

주문 시스템에서는 주문(Order)이라는 애그리게이트를 통해 여러 엔티티가 관리됩니다. 예를 들어, 주문은 다음과 같은 하위 엔티티들로 구성될 수 있습니다:

  • Order: 주문 자체를 나타내는 엔티티 (Aggregate Root)
  • OrderLineItem: 주문에 포함된 개별 상품 항목
  • Payment: 결제 정보
  • ShippingAddress: 배송지 정보

예제 코드:

@Entity
public class Order {  // Order가 Aggregate Root
    @Id
    private Long id;

    private String customer;

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderLineItem> lineItems = new ArrayList<>();

    @Embedded
    private Payment payment;

    @Embedded
    private ShippingAddress shippingAddress;

    // 비즈니스 로직을 담은 메서드들
    public void addLineItem(OrderLineItem item) {
        this.lineItems.add(item);
    }

    public void removeLineItem(OrderLineItem item) {
        this.lineItems.remove(item);
    }

    public void makePayment(Payment payment) {
        this.payment = payment;
    }

    // Getter, Setter 생략
}

@Entity
public class OrderLineItem {  // Order의 하위 엔티티
    @Id
    private Long id;

    private String productName;
    private int quantity;

    // Getter, Setter 생략
}

@Embeddable
public class Payment {
    private String paymentMethod;
    private BigDecimal amount;

    // Getter, Setter 생략
}

@Embeddable
public class ShippingAddress {
    private String street;
    private String city;

    // Getter, Setter 생략
}

위 예시에서 Order는 애그리게이트 루트입니다. Order는 여러 OrderLineItem을 가지고 있으며, PaymentShippingAddress도 포함됩니다. 중요한 점은 OrderLineItem, Payment, ShippingAddress와 같은 객체들은 Order 애그리게이트 안에 포함되어 있고, 외부에서는 Order를 통해서만 이들에게 접근하고 수정할 수 있다는 것입니다.

3.2 Repository의 역할

Order는 Aggregate Root이므로, 우리는 OrderRepository를 통해 애그리게이트를 저장하고 조회할 수 있습니다. OrderLineItem이나 Payment는 별도의 Repository를 가지지 않으며, OrderRepository를 통해서만 조작됩니다.

public interface OrderRepository extends JpaRepository<Order, Long> {
    // Aggregate Root인 Order에 대한 데이터베이스 조작 메서드
}

4. 트랜잭션과 일관성 경계

Aggregate Root는 애그리게이트 내에서 변경이 일어날 때 항상 트랜잭션 단위로 처리됩니다. 즉, 애그리게이트 내의 객체들이 변경되면 이 변경사항들은 하나의 트랜잭션으로 처리되어야 하며, 이는 애그리게이트가 일관된 상태를 유지하게 도와줍니다. 예를 들어, Order에 새로운 OrderLineItem을 추가하는 로직이 있을 때, Order 객체 자체가 트랜잭션의 경계가 됩니다.

@Transactional
public void addOrderLineItem(Long orderId, OrderLineItem item) {
    Order order = orderRepository.findById(orderId)
                                  .orElseThrow(() -> new EntityNotFoundException("Order not found"));
    order.addLineItem(item);
    orderRepository.save(order);  // Order를 통해 OrderLineItem도 자동으로 저장됨
}

이와 같이, Order를 수정하고 저장하는 모든 작업은 트랜잭션으로 묶여 처리되어 데이터의 일관성을 보장합니다.

5. Spring Data JPA에서의 사용 예시

Spring Data JPA에서는 Aggregate Root가 Repository에 의해서 관리됩니다. JpaRepositoryCrudRepository를 사용하여 Aggregate Root를 저장하거나 조회하며, 하위 엔티티들은 Root의 생명주기에 따라 자동으로 관리됩니다.

애그리게이트의 내부 상태 변경이나 비즈니스 로직을 처리할 때, Repository는 항상 Aggregate Root에 대한 참조만을 제공합니다. 이로 인해 애그리게이트 내부 객체에 대한 직접적인 접근을 제한하고, 도메인 계층이 더 명확해집니다.

Aggregate Root는 도메인 모델의 일관성을 유지하고, 외부와의 인터페이스를 단순화하며, 트랜잭션 경계를 명확히 정의하는 중요한 개념입니다. 이를 통해 복잡한 비즈니스 로직을 체계적으로 관리하고, 애플리케이션의 유지보수를 용이하게 할 수 있습니다. Spring Data JPA에서는 이러한 Aggregate Root 개념을 적용하여 도메인 모델을 간결하게 관리하고, 하위 객체들이 일관성 있게 동작할 수 있도록 돕습니다.

'Spring Boot > Spring Data JPA' 카테고리의 다른 글

Core concepts  (0) 2024.10.19
Getting Started  (0) 2024.10.19
Query Hint  (0) 2023.06.06
Database Index  (0) 2023.06.04
Slice  (0) 2023.04.17