1. 애그리거트

애그리거트 단위로 묶으니 이해하기 쉬운 형태가 되었다.

복잡한 도메인을 이해하고 관리하기 쉬운 단위로 만들기 위해 관련된 객체를 하나로 묶을 수 있는데, 이를 애그리거트라고 한다. 또한, 애그리거트는 모델을 이해하는 데 도움을 줄 뿐만 아니라, 일관성을 관리하는 기준도 된다. 

 

관련된 객체를 하나로 모았기 때문에 한 애그리거트에 속한 객체는 유사하거나 동일한 라이프 사이클을 갖는다. 도메인 규칙에 따라 모든 객체를 동시에 생성하지 않는 경우도 있지만, 일반적으로는 함께 생성하고 함께 제거한다.

 

필자의 경험에서는, 보통 애그리거트는 하나의 엔티티 객체만 갖는 경우가 많았으며 두 개 이상의 엔티티로 구성되는 애그리거트는 드물었다.

 

2. 애그리거트 루트의 역할

애그리거트는 여러 객체로 구성되기 때문에 한 객체만 정상이면 안된다. 가령 Order의 필드가 OrderLine과 관련이 있는 경우, OrderLine을 변경하면, Order에도 변경 사항을 반영해주어야 한다. 이처럼 애그리거트에 속한 모든 객체가 일관된 상태를 유지하기 위해, 관리의 책임을 가지고 있는 엔티티를 루트 엔티티(이 경우에는 Order가 해당)라고 한다. 

 

애그리거트가 관리의 책임을 진다는 것은 곧, 외부에서는 오직 루트 엔티티와만 교류해야 한다는 것을 의미한다. 다른 객체에 직접 접근할 수 있다면, 애그리거트 루트가 강제하는 규칙(= 요구사항 or 도메인 규칙)을 적용할 수 없기 때문에 일관성을 깨는 원인이 되기 때문이다.

 이를 실현하기 위해서는 1) 의미가 모호한 public setter를 가급적 피하고 2) 밸류 타입을 불변으로 구현해야 한다. 그 후에 루트 엔티티만이 내부 객체를 변경할 수 있도록 하여, 애그리거트 전체의 일관성을 지키도록 하자. 단순히 기능을 위임하는 코드이더라도 루트 엔티티로 통하도록 한다.

 혹시나 팀 표준이나 구현 기술의 제약으로 밸류 타입을 불변으로 구현할 수 없다면, 변경 기능을 패키지나 protected 범위로 한정해서 외부에서 실행할 수 없도록 제한하는 방법도 있다.

 

트랜잭션 범위는 작을수록 좋다. 락을 거는 대상이 많을수록(범위가 클수록) 동시성이 떨어지게 되고, 이는 전체적인 성능을 떨어뜨린다. 그러므로 가급적이면 한 트랜잭션이 한 개의 애그리거트만 수정하도록 한다. 부득이하게 한 트랜잭션으로 두 개 이상의 애그리거트를 수정해야 한다면, 애그리거트 간에는 독립성을 유지하고 응용 서비스에서 처리하도록 한다.

물론 팀 표준이나 기술 제약, UI 구현의 편리 등의 이유로, 한 트랜잭션이 두 개 이상의 애그리거트를 변경하도록 고려할 수 있다.

 

3. 애그리거트와 리포지터리

애그리거트는 개념상 완전한 한 개의 도메인 모델을 표현하므로, 객체의 영속성을 처리하는 리포지터리는 애그리거트 단위로 존재한다. 데이터를 저장할 때는 에그리거트 전체를 영속화해야 하고, 동일하게 조회를 할 때에도 완전한 애그리거트를 제공해야 한다.

 

개인적으로 생각해보았을 때, 조회 시 완전한 애그리거트를 제공한다면 성능에 악영향이 있지 않을까 우려했었다. 그러다 이내 JPA에는 지연로딩 기능을 통해 해결할 수 있다는 것을 인지할 수 있었다.

 

4. ID를 이용한 애그리거트 참조

한 객체가 다른 객체를 참조하는 것처럼 애그리거트도 다른 애그리거트를 참조한다. 이때 애그리거트 관리 주체는 루트이므로, 루트 엔티티를 참조한다는 것과 같다.

 

ORM 기술 덕분에 객체 참조를 바탕으로 다른 애그리거트로 쉽게 접근할 수 있다. 하지만 아래의 문제를 야기할 수 있다.

1. 편한 탐색 오용 : 필드로 바로 접근할 수 있기 때문에, 한 애그리거트에서 다른 애그리거트를 수정하기 쉬운 환경이다. 앞서 우리는 한 트랜잭션은 가급적 하나의 애그리거트만 수정해야 한다는 것을 학습했다.

2. 성능에 대한 고민 : JPA는 즉시 로딩과 지연 로딩을 모두 제공하기 때문에 어떤 것을 적용할지 고민해야 한다. 이 부분을 덧붙이자면, 김영한님 강의를 통해 기본적으로 지연 로딩을 걸고, 필요할 때만 즉시 로딩을 거는 것이 적절하다고 학습하였다.

3. 확장의 어려움 : 트래픽이 증가하면 부하 분산을 위해 하위 도메인별로 시스템을 분리하기 시작한다. 이때 도메인마다 서로 다른 DBMS를 사용한다면 JPA라는 단일 기술을 적용하기 힘들어진다.

 

ID를 이용해서 다른 애그리거트를 참조한다면 이러한 문제들을 완화할 수 있다. ID 참조 방식의 장점은 1) 애그리거트의 경계를 명확히 하고 물리적인 연결을 제거하기 때문에 모델의 복잡도를 낮출 수 있다. 2) 애그리거트 간의 의존을 제거하므로 응집도를 높여줄 수 있다. 또한, 한 애그리거트에서 다른 애그리거트를 수정하는 문제를 근원적으로 방지할 수 있다. 3) 애그리거트별로 DBMS가 다르더라도 구현 난이도가 낮아진다. 

다른 애그리거트를 ID로 참조하면 여러 애그리거트를 읽을 때 조회 속도가 문제 될 수 있다. 이는 조회 쿼리를 직접 작성하여 해결할 수 있다. 애그리거트마다 서로 다른 저장소를 사용한 경우라면, 조회 성능을 높이기 위해 캐시를 적용하거나 조회 전용 저장소를 따로 구성하여 해결한다.

 

5. 애그리거트를 팩토리로 사용하기

우리는 앞서 한 트랜잭션에서는 가급적 하나의 애그리거트를 수정해야한다고 배웠다. 다만 도메인 기능과 관련되어 있는 경우, 가령 고객이 특정 상점을 여러 차례 신고해서 해당 상점이 더 이상 물건을 등록하지 못하도록 차단한 상태라고 하자. 이 때는 매번 서비스에서 중복 코드를 작성하지 않고 아래와 같이 루트가 가지도록 하는 것이 좋다.

public classs Store {
  public Product createProduct(ProductId new ProuctId, ...) {
    if (isBlocked()) throw new StoreBlockedException();
    
    return new Product(new ProductId, getId(), ...);
  }
}

이처럼 애그리거트가 팩토리로 사용하면 얻을 수 있는 장점은 다음과 같다. 1) 요구 사항이 변경되더라도 해당 로직 수정하면 되기 때문에 변경에 유연하다. 2) 도메인의 응집도가 높아진다.

 

여기서 핵심은 코드의 중복이 발생하지 않도록 한곳에서 관리한다는 것이다.

+ Recent posts