3. 애그리거트

애그리거트

상위 수준에서 모델을 정리하면 복잡한 모데인 모델의 관계를 이해하는 데 도움이 된다. 상위 수준에서 모델을 조망하는 방법이 애그리거트이다. 애그리거트는 관련된 객체를 하나의 군으로 묶어준다.

  • 애그리거트는 일관성을 관리하는 기준이 된다.

  • 애그리거트는 관련된 모델을 하나로 모은 것이기 떄문에 한 애그리거트에 속한 객체는 유사하거나 동일한 라이프 사이클을 갖는다.

  • 애그리거트는 경계를 갖는다. 한 애그리거트에 속한 객체는 다른 애그리거트에 속하지 않는다. 애그리거트는 독립된 객체 군이며, 각 애그리겅트는 자기 자신을 관리할 뿐 다른 애그리거트를 관리하지 않는다. 경계를 설정할 때 기본이 되는 것은 도메인 규칙과 요구사항이다. 도메인 규칙에 따라 함께 생성되고 변경되는 구성요소는 한 애그리거트에 속할 가능성이 높다.

  • ⚠ 'A가 B를 갖는다'라는 요구사항이 있어도 반드시 A와 B가 한 애그리거트에 속하는 것은 아니다.

    상품과 리뷰가 예시이다. Product가 Review를 갖는 것으로 생각할 수 있다. 하지만, 상품과 리뷰는 함꼐 생성되거나 변경되지 않고 변경 주체도 다르기 때문에 서로 다른 애그리거트에 속한다.

애그리거트 루트

애그리거트에 속한 모든 객체가 일관된 상태를 유지하려면 애그리거트 전체를 관리할 주체가 필요한데 이 책임을 지는 것이 애그리거트의 루트 엔티티이다. 애그리거트 루트 엔티티는 애그리거트의 대표 엔티티로 애그리거트에 속한 객체는 애그리거트 루트 엔티티에 직접 또는 간접적으로 속한다.

도메인 규칙과 일관성

애그리거트 루트의 핵심 역할은 애그리거트의 일관성이 깨지지 않도록 하는 것이다. 애그리거트 루트가 아닌 다른 객체가 애그리거트에 속한 객체를 직접 변경하면 안된다. 이는 애그리거트 루트가 강제하는 규칙을 적용할 수 없어 모델의 일관성을 깨는 원인이 된다.

불필요한 중복을 피하고 애그리거트 루트를 통해서만 도메인 로직을 구현하게 만들려면 도메인 모델에 대해 다음의 두 가지를 습관적으로 적용해야 한다.

  • 단순히 필드를 변경하는 set 메서드를 공개(public) 범위로 만들지 않는다.

  • 밸류 타입은 불변으로 구현한다. 밸류 객체가 불변이면 밸류 객체의 값을 변경하는 방법은 새로운 밸류 객체를 할당하는 것뿐이다.

밸류 타입의 내부 상태를 변경하려면 애그리거트 루트를 통해서만 가능하므로, 애그리거트 루트가 도메인 규칙을 올바르게만 구현하면 애그리거트 전체의 일관성을 올바르게 유지할 수 있다.

애그리거트 루트의 기능 구현

애그리거트 루트는 애그리거트 내부의 다른 객체를 조합해서 기능을 완성한다. 애그리거트 루트가 구성요소의 상태만 참조하는 것이 아닌 기능 실행을 위임하기도 한다.

트랜잭션 범위

트랜잭션 범위는 작을수록 좋다. DB 테이블을 기준으로 한 트랜잭션이 한 개 테이블을 수정하는 것과 세 개의 테이블을 수정하는 것은 성능에서 차이가 발생한다.

마찬가지로 한 트랜잭션에서는 한 개의 애그리거트만 수정해야 한다. 한 트랜잭션에서 한 애그리거트만 수정한다는 것은 애그리거트에서 다른 애그리거트를 변경하지 않는다는 것을 의미한다. 만약 부득이하게 한 트랜잭션으로 두 개 이상의 애그리거트를 수정해야 한다면 애그리거트에서 다른 애그리거트를 직접 수정하지 말고 응용 서비스에서 두 애그리거트를 수정하도록 구현해야 한다.

리포지터리와 애그리거트

애그리거트는 개념상 완전한 한 개의 도메인 모델을 표현하므로 객체의 영속성을 처리하는 리포지터리는 애그리거트 단위로 존재한다. 새로운 애그리거트를 만들면 저장소에 애그리거트를 영속화하고 애그리거트를 사용하려면 저장소에서 애그리거트를 읽어야 하므로 리포지터리는 다음의 두 메서드를 제공해야 한다.

  • save : 애그리거트 저장

    애그리거트는 개념적으로 하나이므로 리포지터리는 애그리거트 전체를 저장소에 영속화해야 한다.

  • findById : ID로 애그리거트를 구함

    애그리거트를 구하는 리포지터리 메서드는 완전한 애그리거트를 제공해야 한다.

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

애그리거트도 다른 애그리거트를 참조한다. 애그리거트에서 다른 애그리거트를 참조한다는 것은 애그리거트의 루트를 참조한다는 것과 같다.

필드를 이용한 참조는 구현이 쉽지만 다음과 같은 문제를 야기할 수 있다.

  • 편한 탐색 오용 : 한 애그리거트 내부에서 다른 애그리거트 객체에 접근할 수 있으므로 다른 애그리거트 상태를 쉽게 변경할 수 있게 된다.

  • 성능에 대한 고민

  • 확장 어려움

이런 세 가지 문제를 완화할 때 사용할 수 있는 것이 ID를 이용해서 다른 애그리거트를 참조하는 것이다.

ID 참조를 사용하면 모든 객체가 참조로 연결되지 않고 한 애그리거트에 속한 객체들만 참조로 연결된다. 이는 애그리거트의 경계를 명확히 하고 애그리거트 간 물리적인 연결을 제거하기 때문에 모델의 복잡도를 낮춰준다. 애그리거트 간의 의존을 제거하므로 응집도를 높여주는 효과가 있다.

ID를 이용한 참조 방식을 사용하면 한 애그리거트에서 다른 애그리거트를 수정하는 문제를 원천적으로 방지할 수 있다. 외부 애그리거트를 직접 참조하지 않기 때문에 애초에 다른 애그리거트의 상태를 변경할 수 없게 된다. 또한 애그리거트 별로 다른 구현 기술을 사용하는 것도 가능해진다.

ID를 이용한 참조와 조회 성능

다른 애그리거트를 ID로 참조하면 참조하는 여러 애그리거트를 읽어야 할 때 조회 속도가 문제될 수 있다. 두 가지 애그리거트를 함께 조회해야 할 경우 조회 대상이 N개 일 때 N개를 읽어오는 한 번의 쿼리와 연관된 데이터를 읽어오는 쿼리를 N번 실행한다 해서 N+1 조회 문제(지연로딩의 대표적 문제)가 발생한다. 이 문제가 발생하지 않도록 하려면 조인(즉시 로딩)을 사용해야 한다. 하지만, 이 방식은 애그리거트 간 참조를 객체 참조 방식으로 다시 되돌리는 것이다.

ID 참조 방식을 사용하면서 N+1 조회와 같은 문제가 발생하지 않도록 하려면 전용 조회 쿼리를 사용하면 된다. 데이터 조회를 위한 별도 DAO를 만들고 DAO 조회 메서드에서 세타 조인을 이용해서 한 번의 쿼리로 필요한 데이터를 로딩하면 된다.

Last updated

Was this helpful?