[DDDStart] 3장. 애그리거트

3장. 애그리거트

애그리거트가 무엇이고 구현을 어떻게하는가?

애그리거트

요구사항의 변경이 일어났다. 우리팀 코드를 수정해야 한다. 수정하려고 코드를 들여다 봤더니 ‘수정하면 기존의 기능들은 문제없이 잘 동작할까?’ 의구심이 든다. 그래서 상위 수준에서 모델이 어떻게 엮여있는지 살펴봤다. 오우싵,, 도메인 간의 관계를 파악하기가 너무 어렵다. 변경 안하는 쪽으로 우리팀을 설득시켜 봐야겠군.

도메인 객체 모델이 복잡해지면 전반적인 구조나 큰 수준에서 도메인 간의 관계를 파악하기가 어려워지게 되는데, 그 원인은 개별 구성요소 위주로 모델을 이해를 하게 되기 때문이다.

애그리거트로 객체를 묶어서 바라보면 상위 수준에서 도메인 모델 간의 관계를 파악하기가 쉬워진다

 

 

상품과 리뷰

상품 상세페이지에 들어가면 상품 정보와 리뷰가 나옴.

이것은 한 애거리거트에 속한다고 생각하면 안됨.

  1. Create : 상품과 리뷰의 생성시점이 다름!!!

  2. Update : 변경도 함께 이루어지지 않음!!!

  3. Subject : 변경 주체도 다름. (상품: 상품담당자 / 리뷰 : 고객)

 

애그리거트 루트

애거리거트 루트의 핵심 역할 : 애그리거트의 일관성이 깨지지 않도록 하는 것

각 여러 객체는 정상적인 상태여야 한다. 이를 루트가 담당한다.

루트는 애그리거트가 제공해야 할 도메인 기능을 구현함.

ex) 주문 애그리거트의 기능 : 배송지변경, 상품변경 등임. -> 루트인 Order가 기능을 구현한 메서드를 제공함

이 구현한 메서드가 루트가 아닌 객체의 일관성이 깨지지 않도록 구현해야 한다.

도메인에 제약조건 같은것들.. verify~ 예외처리 이런거를 루트에서 처리!!!

 

애그리거트 루트가 아닌 다른 객체들이 애그리거트에 속한 객체를 변경하면 안되!!

public class Order {

    private OrderLines orderLines;
    private Money totalAmounts;

    //...
}
// 서비스 계층의 로직에서 아래와 같이 접근 가능하다면!! 정합성에 문제가 생긴다.
public class OrderService {
    public void changeOrderLines(OrderLines newOrderLines) {
        OrderLines lines = order.getOrderLines();
        lines.changeOrderLines(newOrderLines);
    }
}

OrderLine의 목록이 바뀌었는데 totalAmounts는 기존의 금액 그대로다!!

 

// 밖에 보내면 상태바뀌니까 이걸로 감싸서 보내라!!! 그럼 불변이니까!
@Setter
@AllArgsConstructor
class Node {
    int value;
}

public class SampleApplication {

    public static void main(String[] args) {
        List<Node> list = new ArrayList<>();
        list.add(new Node(1));
        list.add(new Node(2));
        list.add(new Node(3));
        list.get(0).setValue(100);

        List list2 = Collections.unmodifiableList(list);

        list2.get(0).setValue(100); // Error!! setValue 사용 불가!
    }
}        

 

verify~ 와 같은 검증 로직들을 서비스 단에서 할 수도 있겠지만, 코드 중복이 발생할 확률이 높다. 이는 결합도를 높인다. 따라서 도메인 단에 넣는게 이상적인듯

 

set메서드를 public으로 만들지 말자

set 메서드는 도메인의 의도를 표현하지 못함.

도메인 로직이 도메인 객체가 아닌 Application / UI 계층으로 분산되게 하는 원인이 됨.

-> Entity, VO에 public set을 넣지 않는것 만으로도 일관성이 깨질 가능성이 줄어들게 된다.

 

불변이 안되면 protected/default로!!

불변을 강제할 수 없다면, 외부에서 접근할 수 없도록 접근제한자를 둘 수도 있다.

한 애그리거트의 요소들은 같은 패키지 안에 있으므로 패키지 내부에서만 접근가능하도록 한다면 외부에서의 변경을 방지할 수 있다.

 

트랜잭션

어떤 애그리거트 루트에서 다른 애그리거트 루트를 변경하면 GG 쳐야함

한 트랜잭션에서 두 개 이상의 애그리거트를 수정하면 트랜잭션 충돌이 발생할 가능성이 높아진다. 성능 구려짐.

꼭 바꿔야 한다면 서비스 단에서 두 애그리거트를 수정하도록 구현하자

(팀이나 조직의 표준에 따라 사용자 유스케이스와 관련된 응용 서비스의 기능을 한 트랜잭션으로 실행해야 하는 경우에는 한 트랜잭션에서 두개 이상의 애그리거트를 수정할 경우도 있음.)

 

음… Micro Services에서는 트랜잭션처리 우예함?
내 서비스에서 다른 서비스에 수정하라고 요청해놓고 내 서비스에서 에러나서 롤백되면 다른 서비스도 롤백되야 할텐데 우예할꼬

 

애그리거트 & 리포지터리

리포지터리는 애거리거트의 루트를 위한 리포지터리만 존재한다.

(루트가 아닌 것들은 어짜피 애그리거트에 속하는 구성요소임)

save 할 때 모든 구성요소들을 넣어야하고,
findById 할 때 모든 구성요소들을 가지고 와야 한다.

안그러면 NullPointerException 에러를 만나게 될 것임

 

객체 참조 vs ID 참조 (애그리거트 참조)

사례

class Order {
    private Orderer orderer;
    //...
}

class Orderer {
    private Member member; // Order 애그리거트에서 Member 애그리거트를 참조한다.
    private String name;
    //...
}

class Member {
    //...
}

문제점 1. 편한 탐색 오용

한 애그리거트(Order) 내부에서 다른 애그리거트(Member)에 대한 참조를 가지면 다른 애그리거트의 상태를 쉽게 변경할 수 있게 된다.

이제 다음과 같은 실수를 범할 것이다.

public class Order {
    private Orderer orderer;

    public void changeShippingInfo(ShippingInfo newShippingInfo, boolean useNewShippingAddrAsMemberAddr) {
        if (useNewShippingAddrAsMemberAddr) {
            // 한 애그리거트에 대한 접근이 가능해진다면
            // 서비스 계층까지 안나가고, 여기서 변경할 확률이 높아진다!!!!
            // 결국 결합도를 증가시킨다~!!!!
        }
    }
}

 

문제점 2. 성능에 대한 고민

애그리거트를 직접 참조하면 성능 과 관련된 고민을 해야한다!? JPA를 사용할 경우 LAZY, EAGER 두가지 방식으로 로딩이 가능한데, 어떤 것을 사용할지는 애그리거트의 어떤 기능을 사용하느냐에 따라 달라진다.
(상태를 변경하는 기능을 실행하는 경우에는 굳이 불필요한 객체를 함께 로딩할 필요가 없음 -> 지연로딩 유리)

애그리거트를 간접 참조하면 무조건 LAZY이므로 고민할 필요가 없다
-> 사실 이건 N+1 문제를 떠올리면 EAGER가 유리할 때도 있으므로(Join) 상황에 맞게 써야할듯

 

문제점 3. 확장

서비스가 커지면 부하를 분산시키기 위해 도메인별로 시스템을 분리하기 시작한다.

Micro Services를 하면서 DB 까지 분리할 수도 있다. 한쪽은 마리아, 한쪽은 몽고.. DB를 분리해버리면 당연히 다른 애그리거트의 Member를 가져올 수 없다.

 

해법

객체 참조 대신 ID로(PK) 다른 애그리거트를 참조.

class Orderer {
    private MemberId memberId;
    private String name;
    // ...
}

class Member {
    private MemberId id;
    // ...
}
  1. 애그리거트간의 의존을 제거한다, 응집도 높여준다

  2. 구현 복잡도도 낮아진다. 지연 로딩할지 즉시 로딩할지 고민안해도 된다.

  3. 다른 DB 사용해도 문제없음

 

N+1 문제

ID로 참조 시 즉시로딩은 없는 것 같음…

그런데 지연로딩에는 N+1문제가 있음.

N개의 요소를 가진 Collection 을 지연로딩하면 Collection을 사용할 때 쿼리가 N+1번 날라감…

어떻게 해결 ?

-> 이 땐 객체의 ID가 아닌, 객체로 참조하고 조인 해서 한번에 가져와야 할듯.

 

팩토리 메서드

처음 딱 팩토리 메서드를 봤을 때 좀 회의적이었다.

흠.. 이렇게 하는게 맞을까?

 

RegisterProductService

if( account.isBlocked()) {
    throw new StoreBlockedException();
}
Product product = new Product(id, ...);
productRepository.save(product);
return id;

단점 : 중요한 도메인 로직 처리가 응용 서비스에 노출되었다.
=> 이게 노출된건가.. !???

아무튼 Product를 생성시키는 주체가 Store이고, Product를 생성할 때 Store에 대해 검증이 이루어져야 하기 때문에 아래처럼 한다고 함.

RegisterProductService

Product product = account.createProduct(id, ...);
productRepository.save(product);
return id;

애그리거트가 갖고 있는 데이터를 이용해서 다른 애그리거트를 생성해야 한다면 팩토리를 고려해보자

   

Q1. 애그리거트의 상태가 변경되면 모든 변경을 원자적으로 저장소에 반영해야 한다. 여기서의 원자적의 의미는? (p86)

들어갈꺼면 다 한번에 빡 들어가야함. 유저의 정보가 변경되었는데 부분적용된 상태에서 쿼리가 읽히는 일은 없다!

 

Q2. 한 서비스 계층에서 다른 애그리거트가 필요하다면 누구를 호출해야 할까? 다른 애그리거트의 서비스계층? Repository? 누굴 호출해야 할까!!
같은 레이어인 서비스를 호출해야 할 것 같았는데 p91에 보면 Repository를 이용함. 왜 … Customer서비스에 저장하라고 요청하지 않았지? 이것도 트랜잭션과 연관이 있는건가?

TODO

 

Q3. Exception 도 도메인이다!?!?!?!??

도메인에서 발생하면 도메인 Exception이고 서비스에서 발생하면 서비스 Exception이다!