티스토리 뷰
개요
# 참고
IMPLEMENTING DOMAIN DRIVEN DESIGN (도메인 주도 설계 구현) - 반 버논 지음
# 개요
DDD에 대한 기술서적을 읽고 DDD가 무엇인지 이해하고 어떻게 코드에 적용시킬 수 있는지 고민해 보려고 한다.
# 10장의 로드맵
- 잘못된 애그리게잇의 모델링이 초래하는 부정적인 결과
- 애그리게잇 경험 법칠에 따라 설계하는 방법
- 일관성의 경계 안에서 실제 비즈니스 규칙에 따라 진정한 고정자를 모델링하는 방법
- 애그리게잇을 작게 설계할 때 얻을 수 있는 이점
- 애그리게잇의 다른 애그리게잇을 참조할 때 식별자를 사용하는 이유
- 애그리게잇 경계 바깥에서 결과적 일관성을 사용해야 하는 중요성
10장. 애그리게잇
처음에는 신중을 가해서 일관성 경계를 통해 엔티티와 값 객체를 애그리게잇으로 묶는 일이 쉬워 보일 수 있지만, 애그리게잇은 모든 DDD의 전술적인 시점주에서도 무엇보다 정확히 규명되지 않은 패턴 중 하나이다. 애그리게잇이란 단지 공통 부모 아래 긴밀하게 연결된 그래프를 묶는 방법 중 하나일까? 그렇다면 그래프 안에 들어갈 객체의 수에는 실질적인 제한이 있을까? 하나의 애그리게잇 인스턴스가 다른 애그리게잇 인스턴스를 참조할 수 있는데, 그렇다면 이 연결에 따라 깊이 탐색해 가며 다양한 객체를 수정할 수 있을까? 그리고 고정자라는 개념은 무엇이고 일관성 경계란 어떤 의미일까?
애그리게잇을 잘못 리모델링하는 방식에는 여러 가지가 있다. 컴포지션의 편의에 맞춰 설계하다 보면 애그리게잇을 너무 크게 만들어버리는 함정에 빠질 수 있다. 반면 애그리게잇을 모두 걷어내 버리면 진정한 고정자를 보호하지 못하는 경우도 있다.
10-1. 스크럼 핵심 도메인에서 애그리게잇 사용하기
애플리케이션을 중심으로 사스오베이션이 애그리게잇을 사용하는 방식을 살펴보자. 최선의 객체 클러스터를 선정하는 방법은 무엇일까? 애그리게잇 패턴은 컴포지션을 다루며 정보 은직을 가능케 한다. 애그리게잇에선 일과성 경계와 트랜잭션도 함께 다르게 되는데, 영속성 메커니즘이 데이터으 ㅣ원자적 커밋을 유지하도록 도움을 준다. 그러나 이는 심각한 오해이다.
10-2. 첫 번째 시도 : 큰 클러스터의 애그리게잇
크기가 큰 애그리게잇은 처음엔 그럴싸해 보였지만, 실제론 실용적이지 않다. 다수의 사용자 환경에서 애플리케이션을 실행하면, 트랜잭션이 주기적으로 실패할 수 있다. 애그리게잇 인스턴스는 낙관적 동시성을 활용해 다른 클라이언트가 영속성 객체를 동시에 중복 수정하지 않도록 보호하며, 데이터베이스 락의 사용을 피할 수 있다.
동시성을 처리하기 위해 이와 같은 일반적인 방식에 따라 영속성 메커니즘을 사용한다. 이 접근법은 애그리게잇 고정자를 동시에 발생하는 변경으로부터 보호하는 중요한 역할을 한다.
이 영속성의 문제는 사용자가 단 둘이었을 때도 나타났으며, 사용자 수가 더해지면 더 큰 문제가 발생한다. 조의 커밋은 왜 실패했을까? 그 중심에는 큰 클러스터의 애그리게잇이 실제 비즈니스 규칙이 아닌 잘못된 고정자를 기준으로 설계했다는 문제이다. 이 잘못된 고정자는 개발자가 만들어낸 인위적인 제약 조건이다. 이런 설계는 트랜잭션의 문제를 일으킬 뿐 아니라, 성능과 확장성의 측면에서도 안 좋은 영향을 미친다.
하나의 큰 애그리게잇을 네 개로 쪼개면 메소드 계약 일부가 변경된다. 모든 메소드가 CQS 커맨드다. 즉, 이는 새로운 컴포넌트를 컬렉션에 추가해 Entity의 상태를 수정하는데, 이에 따라 void 반환 타입을 갖게 된다. 그러나 여러 애그리게잇으로 나눈 설계에선 다음과 같은 형태를 갖는다. 새롭게 설계된 메소드는 CQS 쿼리 계약을 맺고 있으며 패토리로서 동작한다. 즉, 각 메소드는 새로운 애그리게잇 인스턴스를 생성해서 그 참조를 반환한다. 우린 밖으로 빼서 모델링함으로써 트랜잭션 실패 문제를 해결했다. 그러나 분명 트랜잭션의 이점에도 불구하고 클라이언트가 사용하느 관점에서 보면 네 개의 작은 애그리게잇은 사용이 불편하다. 어쩌면 큰 애그리게잇을 조금 다듬어서 동시성 문제를 해결할 수 있을지도 모른다.
10-3. 진짜 고정자를 일관성 경계 안에서 모델링하라
바운디드 컨텍스트에서 애그리게잇을 찾으려면 모델의 진짜 고정자를 이해해야 한다. 이를 알아야만 주어진 애그리게잇으로 묶어야 할 객체가 무엇인지 결정할 수 있다.
고정자는 언제나 일관성을 유지해야만 한다는 비즈니스 규칙이다. 일관성에는 여러 종류가 있다. 그중 한 가지느느 트랜잭션적 일관성인데, 이는 즉각적이고 원자적이라 간주된다. 또 한 가지로 결과적 일관성이 있따. 고정자에 관한 논의는 트랜잭션적 일과성과 관련이 있다.
일관성 경계는 어떤 오퍼레이션이 수행되든 상관없이 경계 안의 모든 대상이 특정 비즈니스 고정자 규칙 집합을 준수하도록 논리적으로 보장해준다. 이 경계 밖의 일관성은 애그리게잇과 무관하다. 그러므로 애그리게잇은 트랜잭션적 일관성 경계와 동의어다.
전형적인 영속성 메커니즘을 사용하면 단일 트랜잭션을 사용해 일관성을 관리한다. 트랜잭션이 커밋하는 시점에서 하나의 경계 안에 속한 모든 것이 반드시 일관성ㅇ르 유지해야 한다. 올바르게 설계된 애그리게잇은 단일 트랜잭션 내에서 와녁한 일관성을 유지하며, 비즈니스적으로 요구되는 모든 방식과 그 고정자에 맞춰 수정될 수 있어야 한다. 또한 바르게 설계된 바운디드 컨텍스트는 어떤 상황에서든 한 트랜잭션당 한 애그리게잇 인스턴스만 수정한다. 즉, 전형적으로 고정자는 작은 애그리게잇으로 설계하는 편이 더 좋다.
10-4. 작은 애그리게잇으로 설계해라
이제 큰 클러스터의 애그리게잇을 유지하는데 드는 추가적인 비용이 무엇인지 보자. 모든 트랜잭션이 성공한다고 보장되더라도, 큰 클러스터에느 여전히 성능과 확장성의 문제가 있다.
# 큰 애그리게잇의 단점 (with. 지연로딩)
성능과 확장성의 측면에서, 어떤 테넌트의 한 사용자가 수년간 이미 수천 개의 백로그 항목이 쌓인 제춤에 하나의 백로그 항목을 더 추가하고 싶다면 어떤 일이 일어날까? 지연로딩이 가능한 영속성 메커님즘이라고 가정한다면, 모든 정보를 한 번에 가져오는 경우는 거의 없다. 하지만 이미 거대해진 컬렉션에 단 하나의 항목을 추가하기 위해 수천 개의 항목을 메모리로 가져올지도 모른다. 거기다 영속성 메커니즘이 지연 로딩을 지원하지 않는다면 상황은 더 나빠진다. 즉, 꽤 단순한 오퍼레이션을 수행하기 위해서도 무수히 많은 객체를 메모리로 한 번에 가져와야 할 가능성이 높다.
(지연로딩과 즉시로딩의 개념은 JPA 게시글을 읽어 보자)
# 작은 애그리게잇은 얼마나 작음을 의미할까?
큰 클러스터 애그리게잇은 성능이나 확장성이 절댈로 좋을 수 없다. 작은 애그리게잇을 설계할 때 '작다'느 단어는 어떤 수준을 의미할까? 작은 애그리게잇이란 극단적으로 전역 고유 식별자와 추가 특성을 갖고 있는 애그리게잇을 의미하지 않는다. 작은 애그리게잇은 루트 엔티티와 최소한의 특성이나 값 타입의 속성으로 제한하는 것이 좋다.
한 부분을 하나의 엔티티로 모델링한다고 생각해보자. 시간에 따라 변화하는지, 아니면 변경이 필요할 때 완전히 대체시키면 되는지 생각해보자. 안전히 대체돼야 한다면 엔티티보다는 값 객체를 사용하는 편이 맞다. 애그리게잇을 구성하는 일부로서 값 객체를 선호하는 접근이 애그리게잇 자체가 불변한다는 의미는 아니며, 값 타입 속성을 대체하면 해당 루트 엔티티 자체가 변경된다. 일정 부분을 값으로만 제한하는 데는 중요한 이점이 있다. 엔티티느 별도의 추적 저장소가 필요할 수도 있으며, 하이버네이트를 사용해 SQL 조인이 필요해 질 수 있다. 따라서 값 객체를 사용하면 더 작고 안전하게 사용할 수 있으며, 불변성으로 인해 단위 테스트를 통해 정확성을 확인하기 쉬워진다.
크기가 작은 애그리게잇은 성능과 확장성이 더 좋을 뿐 아니라, 커밋을 가로막는 문제가 거의 일어나지 않기 때문에 트랜잭션이 성공할 가능성이 높다.
10-5. ID로 다른 애그리게잇을 참조하라
1. 두 개의 어그리게잇이 존재할 때 참조하는 애그리게잇과 참조된 애그리게잇을 같은 트랜잭션 안에서 수정해선 안 된다. 하나의 트랜잭션에선 둘 중 한쪽만 수정해야 한다.
2. 하나의 트랜잭션에서 여러 인스턴스를 수정하고 있다면 일관성 경계가 잘못됐다는 신호일 가능성이 높다.
3. 크게 묶여 있는 애그리게잇에 영향을 미친다면, 이는 원자적 일관성 대신 결과적 일관성을 사용해야 한다는 표시이다.
참조 없이는 다른 애그리게잇을 수정할 수 없다. 여러 애그리게잇을 같은 트랜잭셔에서 수정하라는 유혹은 이런 상황 자체를 만들지 않음으로써 피해갈 수 있따. 그러나 도메인 모델은 항상 일정한 수준의 연결이 필요하기 때문에, 이는 너무 지나친 제한이기도 하다. 어떻게 하면 필요한 연결을 유지하면서 트랜잭션이 잘못 사용되거나 지난친 실패를 초래하지 않도록 보호하고, 성능과 확장성을 갖도록 할 수 있을까?
애그리게잇이 ID 참조를 통해 서로 함께 동작하도록 해보자. 외부 애그리게잇보다는 참조를 사용하되, 객체 참조(포인터)를 직접 사용하지 말고 전역 고유 식별자를 이용하자. 추론 객체 참조를 가진 애그리게잇은 참조를 즉시 가져올 필요가 없기 때문에 당연히 더 작아진다. 인스턴스를 가져올 때 더 짧은 시간과 적은 메모리가 필요하기 때문에, 모델 성능도 좋아진다.
10-6. 모델 탐색
ID를 통한 참조를 사용한다고 모델을 전혀 탐색할 수 없는 건 아니다. 조회를 위해선 애그리게잇의 내부에서 리파지토리를 사용하는 방법이 있다. 이는 단절된 도메인 모델이란 기법인데, 실제론 지연로딩의 한 형태다. 애그리게잇의 행동을 호출하기에 앞서 리파지토리나 도메인 서비스를 통해 의존 관계에 있는 객체를 조회하는 방법도 추천할 만하다. 즉, 결론적으로 어떤 애그리게잇이 다른 애그리게잇으로의 접근을 획득하는 방법이 무엇이든, 하나의 요청이 여러 애그리게잇을 참조하더라도 그중 둘 이상을 수정할 수 있는 자격이 주어지지 않다.
모델이 오직 ID만을 사용해 참조하도록 제한한다면 클라이언트에게 사용자 인터페이스 뷰를 조합해서 보여주기가 더욱 어려워질 수 있다. 여러분은 아마도 하나의 유스케이스에 해당하는 뷰를 만들기 위해 여러 리파지토리를 사용해야만 하는 상황에 놓일 수 있다.
10-7. 경계의 밖에선 결과적 일관성을 사용하라
여러 애그리게잇이 하나의 클라이언트 요청에만 영향을 받아야 하는 상황에서 일관성을 유지하기 위해 해얗만 하는 일괴 깊은 관련이 있다. 하나의 애그리게잇 인스턴스에서 커맨드를 수행할 떄 하나 이상의 애그리게잇에서 추가적인 비즈니스 규칙이 수행돼야 한다면 결과적 일관성을 사용하자. 큰 규모의 트래픽이 많은 엔터프라이즈에선 애그리게잇 인스턴스가 절대적이고 완전하게 일관성을 유지할 수 없다는 점을 받아들인다면, 결과적 일관성이 더 적은 인슽턴스가 관련되 더 작은 규모에서도 의미 있다는 사실을 좀 더 쉽게 이해할 수 있다.
각각의 구독자가 다른 유형의 애그리게잇 인스턴스를 가져오고, 그에 기반해 동작을 수행한다. 각 구독자는 분리된 트랜잭션 내에서 수행되며, 트랜잭션당 하나의 인스턴스만을 수정한다는 애그리게잇 규칙을 따른다. 구독자가 다른 클라이언트와 동시성 결합을 겪어서 수정에 실패하면 어떻게 될까? 구독자 메시징 메커니즘을 통해 수정 성공을 알리지 않으면 수정을 재시도 할 수 있다. 메시지가 재전달되고 새로운 트랜잭션이 시작되며, 필요한 커맨드를 실행하려는 시도를 새롭게 시작하고, 그에 따른 커밋이 이뤄진다. 이 재시도 프로세스는 일과성이 달성될 때까지나 재시도 제한에 이를 때까지 계속된다. 완전히 실패하게 된다면, 그에 따른 대응을 하거나 최소한 대기 중인 관련 작업을 위해 실패했음을 알려야 한다.
10-8. 규칙을 어겨야 하는 이유
DDD를 수행하는 경험 많은 사람이라면 가끈은 하나의 트랜잭션에서 여러 애그리게잇 인스턴스를 저장하기로 결정할 때도 있겠지만, 여기엔 충분한 이유가 있어야 한다. 지금부터 규칙을 어겨야하는 4가지 이유를 설명하겠다.
# 첫번째 이유 : 사용자 인터페이스의 편의
편의를 위해 사용자가 한 번에 여러 일의 공통 특성을 정의해 배치를 생성할 수 있도록 허용할 떄도 있다. 사용자 인터페이스에선 모든 공통 속성을 하나의 섹션에 넣은 후 차이가 나는 몇 가지 속성을 개별적으로 추가하도록 해서 반복된 움직임을 줄일 수 있다.
Q. 이 때문에 고정자의 관리에 문제가 생길 수 있을까?
A. 이 경우는 그렇지 않은데 이미 한 번에 생성했든 배치로 생성했든 상관없기 때문이다. 인스턴스화되는 객체는 모두 애그리게잇이며, 이렇게 인스턴스화된 애그리게잇은 자신의 고정자를 갖고 있게 된다. 때문에 애그리게잇 인스턴스의 배치를 한 번에 생성하는 방식과 반복적으로 하나씩 생성하는 방식 사이에 차이점이 없다면, 이는 경험에 근거한 규칙을 깨더라도 문제가 되지 않는 한 가지 이유가 된다.
# 두 번째 이유 : 기술적 메커니즘 부족
결과적 일관성을 위해선 메시징이나 타이머, 또는 백그라운드 스레드와 같은 추가적인 처리 기능의 사용이 필요할 수 있다.
Q. 만약 여러분의 프로젝트가 이러한 기능을 전혀 제공하지 않는다면 어떻게 해야 할까?
A. 이런 상황에선 주의하지 않으면 다시 큰 클러스터의 애그리게잇을 설계하는 방향으로 기울게 된다. 그렇다면 단일 트랜잭션의 규칙을 지키고 있다는 느낌은 주지만, 성능을 떨어트리고 확장성을 제한할 수 있다. 이런 상황을 피하기 위해선 애그리게잇을 전반적으로 변경해, 모델을 통해 문제를 해결할 수 있다.
사용자와 애그리게잇 사이의 연관성은 규칙에서 벗어나도 될 이유를 더해주는 또 하나의 요소다. 한 명의 사용자가 오직 하나의 애그리게잇 인스턴스 집합에만 집중해야 하는 비즈니스 워크플로우인지 질문해보자. 사용자와 애그리게잇 사이의 연관성을 확실히 한다면 여러 애그리게잇 인스턴스를 하나의 트랜잭션에서 변경하는 결정을 좀 더 합리적으로 내릴 수 있는데, 이는 고정자 위반과 트랜잭션 충돌을 막는데 도움이 되기 때문이다. 따라서 동시성 결합을 겪고 설계를 변경할 수 없는 상황에선, 때론 다수의 애그리게잇 인스턴스를 하나의 트랜잭션에서 수정하는 편이 더 바람직할 수 있다.
# 세 번째 이유 : 글로벌 트랜잭션
레거시 기술과 엔터프라이즈 정책의 영향도 고려해야 할 한 가지 요소다. 글로벌한 2단계 커밋 트랜잭션을 엄격히 지켜 사용해야 할 때가 그렇다. 이는 적어도 단기간 내에선 지연될 가능성이 거의 없어야 하는 상황 중 하나다. 글로벌 트랜잭션을 사용한다고 해도, 여러분의 바운디드 컨텍스트 내에서 다수의 애그리게잇 인스턴스를 한 번에 수정할 필요는 없다. 이렇게 하지 않을 수 있따면 핵심 도메인 내에서 트랜잭션 충돌이 발생하지 않도록 막을 수 있고, 애그리게잇의 규칙을 최대한 따를 수 있게 된다. 글로벌 트랜잭션의 단점으로는 2단계 커밋을 사용하지 않을 수 있는 상황에선 달성할 수 있었을 확장성을 얻을 수 없고, 그에 따라 즉각적인 일관성을 보장할 수 없게 된다는 점이 있다.
# 네 번쨰 이유 : 쿼리 성능
다른 애그리게잇으로의 직접 객체 참조를 유지하는 편이 최선일 때가 있다. 이는 리파지토리의 쿼리 성능 문제를 해결하는 데 사용할 수 있다. 잠재적인 크기와 전반적인 성능이라는 상충점이 의미하는 바를 고려해 신중하게 선택해야 한다.
10-9. 메모리 소비
지연로딩 때문에 이 설계가 성능 문제를 일으킬까? 그럴 가능성도 있는 것이, 이는 사실 태스크를 위한 지연로딩과 추정 로그 엔트리를 위한 지연 로딩 같ㅇ느 두번의 지연 로딩을 유발한다. 이는 여러 번의 가져오기 동작에서 발생할 수 있는 오버헤드를 조사히가 위해 테스트 해야 한다.
10-10. 값 객체 파트를 선호하라
가능하다면 포함된 애그리게잇 파트를 엔티티보다는 값 객체로서 모델링하는 편을 선택하자. 만약 모델이나 인프라에서 의미 있는 오버헤드를 야기하지 않는다면, 완전히 대체될 수 있는 파트가 최선의 선택지이다.
10-11. '데메테르의 법칙'과 '묻지 말고 시켜라'를 사용하기
데메테르의 법칙과 묻지 말고 시켜라 모두 애그리게잇을 구현할 떄 사용할 수 있는 설계 원칙으로, 둘 모두 정보 은직을 강조하고 있다.
# 데메테르의 법칙
이 원칙은 최소 지식의 원칙을 강조한다. 클라이언트는 서버를 사용할 때 서버의 구조에 관해 가능한 모르는 편이 좋다. 서버의 특성과 속성은 클라이언트에게 완벽히 감춰져야 한다. 클라이언튼는 서버에게 그 표면 인터페이스상에 선언된 커맨드를 수행토롱 부탁할 수 있다. 그러나 클라이언트는 서버 안쪽까지 도달해선 안 되고, 서버에게 일부 내부 파트를 부탁한 후 해당 파트상의 커맨드를 싱행해야 한다. 데메테르의 법칙의 간단한 요약은 다음과 같다. 모든 객체의 모든 메소드는 다음을 통해서만 메소드를 호출해야 한다. 자기 자신, 매개변수, 자신이 인스턴스화하는 객체, 자신이 직접 액세스할 수 있는 스스로가 포함된 파트 객체
# 묻지 말고 시켜라
이 원칙은 단순히 객체에게 할 일을 알려줘야 한다는 점을 강조하고 있다. 묻지 말고라 다음과 같이 클라이언트에게 적용된다. 클라이언트 객체는 서버 객체에게 서버객체가 갖고 있는 파트를 요구해선 안되며, 자신이 갖고 있는 상태에 기반해 결정해야 하고, 그 후에 서버 객체가 일을 하도록 만들어야 한다. 그 대신, 클라이언트는 반드시 서버에게 무엇ㅇ르 하지 '시켜야'하며, 이때는 서버의 퍼블릭 인터페이스로 커맨드를 보내야 한다. 이 법칙은 데메테르의 법칙과 매우 유사한 동기를 갖고 있지만, '묻지 말고 시켜라'가 좀 더 넓은 범위에 쉽게 적용될 수 있다.
'데메테르의 법칙'과 '묻지 말고 시켜라 원칙'은 다른 점도 비교해봐야 한다. '데메테르의 법칙'은 확실히 좀 더 제한이 많으며, 루트 너머의 애그리게잇 파트 탐색을 전혀 허용하지 않는다. 한편 '묻지 말고 시켜라' 원칙은 루트 너머의 탐색은 허용하지만, 애그리게잇 상태의 수정은 클라이언트에 속한 것이 아니라 애그리게잇에게 속해 있다는 점을 강조한다. 그렇기 때문에 애그리게잇 구현에 있어서 '묻지 말고 시켜라' 원칙을 좀 더 폭넓게 적용할 수 있다.
'DDD' 카테고리의 다른 글
12장. 리파지토리 / IDDD (도메인 주도 설계 구현) (0) | 2022.03.17 |
---|---|
11장. 팩토리 / IDDD (도메인 주도 설계 구현) (0) | 2022.03.16 |
9장. 모듈(패키지) / IDDD (도메인 주도 설계 구현) (0) | 2022.03.16 |
8장. 도메인 이벤트 / IDDD (도메인 주도 설계 구현) (0) | 2022.03.15 |
7장. 서비스 / IDDD (도메인 주도 설계 구현) (0) | 2022.03.15 |