make it simple
article thumbnail

애그리거트란?

  • 위 처럼 백 개 이상의 테이블을 위처럼 ERD(Entity Relationship Diagram)로 모두 표시하면 테이블관의 관계를 파악하느라 큰 틀에서 데이터 구조를 이해하는데 어렵다. 도메인 객체 모델이 복잡해지면 개별 구성요소 위주로 모델을 이해하고 전반적인 구조나 큰 수준에서 도메인 간의 관계를 파악하기 어려워진다.
  • 주요 도메인 요소 간의 관계를 파악하기 어렵다는 것은 코드를 변경하고 확장하는 것이 어려워 진다는것이다. 상위 수준에서 도메인 모델의 관계를 알아야 전체 모델을 망가뜨리지 않으면서 추가 요구 사항을 모델에 반영한다. 당장 돌아가게끔은 코드를 추가할 수 있지만 장기적으로 봤을 때는 스파게티 코드가 될 확률이 높으며 유지보수가 어려워진다.
  • 복잡한 도메인을 이해하고 관리하기 쉬운 단위로 만들려면 상위 수준에서 모델을 조망할 수 있는 방법이 필요한데, 그 방법이 Aggregate(애그리거트)다.

Aggregate 적용안한 모델 / Aggregate 적용한 모델

  • 위 처럼 같은 모델인데도 Aggregate를 적용함으로써 한눈에 도메인의 범위가 파악되고 모델을 이해하기 쉬워진다. 이처럼 Aggregate는 복잡한 도메인을 단순한 구조로 만들어주고 복잡도가 낮아지며 도메인 기능을 확장하고 변경하는데 필요한 개발시간이나 복잡도가 줄어든다.
  • Aggregate 경계를 설정할 때 중요한 것은 도메인 규칙과 요구사항이다. 도메인 규칙에 따라 함께 생성되는 구성요소는 한 aggregate에 속할 가능성이 높다.
    • 좋은 예) 주문할 상품 개수, 배송자 정보, 주문자 정보는 주문 시점에 함께 생성되므로 이들은 한 aggregate에 속한다.
    • 안좋은 예) 흔히 'A가 B를 갖는다' 로 생각한다. 하지만 예를 들어 상품과 리뷰가 있다. 상품 상세 페이지에 들어가면 리뷰내용을 보여줘야 한다는 요구사항이 있을때 Product와 Review를 같은 aggregate에 속한다고 생각할 수 있다. 하지만 Product와 Review는 함께 생성되지 않고, 함께 번경되지도 않는다. 게다가 Product를 변경하는 주체가 상품 담당자라면 Review를 생성하고 변경하는 주체는 고객이다. 그래서 Product/Review는 다른 aggregate에 속한다.

Aggregate Root란?

  • 직역하면 '집합 뿌리' 이다. Aggregate는  여러 객체로 구성되기 때문에 한 객체만 상태가 정상이면 안 된다. 도메인 규칙을 지키려면 애그리거트에 속한 모든 객체가 정상 상태를 가져야한다. Order Aggregate에서는 OrderLine의 quantity(상품개수)를 변경해야 Order의 totalAmounts(총 금액)도 변경해야한다, 그래야 가격이 상품 수에 맞게 맞는다. 이처럼 aggregate에 속한 모든 객체가 일관된 상태를 유지하려면 전체를 관리할 주체가 필요하다. 이 주체를 aggregate root라 한다.
  • aggregate root의 핵심은 aggregate의 일관성이 깨지지 않게 하는것이다. 그래서 aggregate root는 도메인 기능을 구현한다. 예를 들면 주문 aggregate에서는 Order 엔티티가 aggregate root이며 배송지 변경, 상품 변경과 같은 기능을 제공한다.
  • aggregate root가 제공하는 메서드는 도메인 규칙에 따라 aggregate에 속한 객체의 일관성이 깨지지 않게 구현해야한다. 배송이 시작되기 전까지만 배송지 정보를 변경할 수 있다는 규칙이 있다면, aggregate root인 Order의 changeShippingInfo() 메서드는 이 규칙에 따라 배송 시작 여부를 확인하고 규칙을 충족할 때만 배송지 정보를 변경해야 한다.
  • Aggregate Root를 통해 불필요한 중복을 피하고 도메인 로직을 구현하게 만들려면 밑에 두가지를 습관적으로 적용해야 한다.
    1. 단순히 필드를 변경하는 set 메서드를 공개(public) 범위로 만들지 않는다
    2.  외부에서 변경 못하게 value 타입은 불변으로 구현한다.
public class Order {
    private Shippinginfo shippinginfo;
	
    public void changeShippingInfo(ShippingInfo newShippinglnfo) { 
            verifyNotYetShipped();
            setShippinglnfo(newShippinglnfo);
    }
	
    // set 메서드의 접근 허용 범위는 private이다.
    private void setShippingInfo(ShippingInfo newShippinglnfo) {
                // 밸류가 불변이면 새로운 객체를 할당해서 값을 변경해야 한다.
                // 불변임으로 this.shippinginfo.setAddress(newShippingInfo.getAddress())와 같은 
                // 코드를 사용할 수 없다.
        this.shippinglnfo = newShippinglnfo;
    }
}
  • 트랜잭션 범위는 잘수록 좋다. 한 개 테이블을 수정하면 트렌잭션 충돌을 막기 위해 잠그는 대상이 한 개의 테이블의 한 행으로 확정되지만, 세 개의 테이블을 수정하면 잠금 대상이 더 많아진다. 잠금 대상이 더 많아진다는 것은 그만큼 동시에 처리할 수 있는 트랜잭션 개수가 줄어든다는 것을 의미하고 이것은 전체적인 성능 (처리량)을 떨어트린다.
  • 한 트랜잭션에서는 한 개의 aggregate만 수정해야한다. 그래야 책임 범위가 좁혀지며 독립적으로 되어 결합도가 낮아진다. 결합도가 높으면 수정 비용이 증가하므로 aggregate에서 다른 aggregate의 상태를 변경하면 안된다.

결론

  • aggregate  범위를 설정할때 서로 영향을 주는지 안 줘야하는지 잘 판단한 후 aggregate root를 잘 선택해서 aggregate의 핵심 로직인 도메인 관련 로직을 작성해야겠다. 도메인 규칙을 지키며 일관성이 안깨지고 캡슐화를 잘 사용하고 트랜잭션 범위를 최대한 좁게 작성하면서 개발하는 습관을 길러야겠다.

reference

profile

make it simple

@keep it simple

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!