지나공 : 지식을 나누는 공간
DDD, Aggregate Root란? + JPA CasecadeType 영속성전이 본문
기존에 작성했던 글이지만 내용을 좀 더 보충해서 재발행한다. 우아콘2021 도메인원정대, DDD-start 책의 내용을 참고했다.
이번에 알아볼 것은,
- DDD란?
- Domain이란?
- Domain Layer?
- Domain Model?
- 도메인 모델링 해보기! + Aggregate 개념 이해 + Aggregate Root
- 큰 애그리거트와 작은 애그리거트
- 영속성 전이 속성 CasecadeType 과 DDD 속 Aggregate
DDD(Domain Driven Design)란?
도메인 중심으로 설계하는 디자인 방법론
Domain이란?
- 소프트웨어로 해결하고자 하는 문제 영역.
- 애플리케이션 내 로직들이 관여하는 정보와 활동의 영역
쿠팡이나 11번가와 같은 오픈마켓을 구현하려면 아래 기능이 필요하다.
- 회원가입, 회원탈퇴 기능 -> 회원 영역
- 장바구니에 담긴 전체 물건을 주문하는 기능 -> 주문 / 정산 / 결제 영역
- 결제할 때 혜택이나 쿠폰을 적용하는 기능 -> 결제 / 쿠폰 영역
화살표로 관련된 영역을 적어놨는데 저게 도메인들이다. 회원, 주문, 정산, 쿠폰 등등!
Domain Layer?
애플리케이션 아키텍처를 설계할 때는 전형적인 4가지 영역으로 나뉜다. [표현, 응용, 도메인, 인프라스트럭차]다.
간단하게 도메인 모델의 네 영역을 설명하자면 아래와 같다.
- Presentation Layer : 표현영역. UI. 사용자의 요청을 받아 응용 영역에 전달하고, 응용 영역의 처리 결과를 다시 사용자에게 보여주는 역할을 한다. (Controller영역, DispatcherServlet에게 요청과 응답을 전달하는 역할을 한다.)
- Application Layer : 응용영역. 시스템이 사용자에게 제공해야 할 기능을 구현한다. (Service영역)
- Domain Layer : 도메인 영역. 도메인 모델을 구현한다. (이름, 주소, 상품 등)
- InfraStructure Layer : 구현 기술에 대한 것을 다룬다. (외부 API, 데이터베이스, 외부 라이브러리 사용 등)
이 중에서 DDD와 관련된 건 당연히 도메인 영역이고, 도메인 영역은 도메인 모델을 구현한다. 주문 도메인의 경우 '배송지 변경', '결제 완료', '주문금액 계산'등의 핵심 로직이 필요한데 이러한 로직을 도메인 모델에서 구현한다. 반면 인프라스트럭처 영역은 논리적인 개념을 표현하기보다는 실제 구현을 다룬다.
Domain Model
- 특정 도메인을 개념적으로 표현한 것. 정보를 제공하기 위한 목적을 가진 게 '모델'이다.
- 기획, 디자인, 개발할 때 의사소통의 수단으로 사용된다.
개발자는 구축한 도메인 모델을 통해 자신이 요구사항을 제대로 이해했는지 확인할 수 있고, 기획자도 자기들끼리 의견을 맞출 때 도메인 모델을 사용해서 소통할 수 있다.
도메인 모델링을 해보자. Aggregate 까지!
도메인 모델링의 기본은 모델을 구성할 핵심 구성요소, 각각의 규칙, 기능을 찾는 것이다. 우아콘에서는 '강의'라는 도메인으로 예시를 들었는데 이걸로 애그리거트까지 이해하고 ddd-start의 예제인 주문 도메인에 대해서도 모델링을 해보자.
강의가 있고 강의를 수강하는 학생들이 있고, 수강을 대기하는 학생들도 있다.
1) 강의 도메인 기능 정리
- 강의 콘텐츠를 관리한다
- 정원과 모집 상태에 따라 수강 신청을 받는다.
- 수강생과 수강 대기자, 리뷰어 관리한다
- 강의는 미션과 상품의 단위가 되기도 한다.
2) 해결 영역 분리를 통해 집중하기 : 바운디드 컨텍스트
하나의 도메인만 딱 만들어두면 그 도메인과 관련된 요구사항들이 너무 많으므로 혼란스럽다. 그래서 같은 도메인을 사용하더라도 그 관심사를 분리하고 격리해서 풀어야 할 문제마다 그에 집중할 수 있는 범위로 쪼개야 하는데, 그 범위를 DDD에서는 '바운디드 컨텍스트(해결영역)'라고 한다.
예를 들어 똑같이 '강의'라는 단어를 말하더라도 강사들끼리 회의할 때 나온 '강의'는 강의의 콘텐츠에 관심이 있을 것이고, 수강 신청 시에 얘기하는 '강의'는 수강 정원에 관심이 있다. 따라서 도메인 지식에 대한 탐구가 필요하다.
강의라는 도메인을 각각의 해결영역에 두면 아래와 같이 강의 안에서도 관심사가 달라진다.
<교육 과정 및 강의 콘텐츠 관리> '강의' 중에서도 [강의 계획서, 콘텐츠]에 관심 |
<미션 진행> '강의' 중에서도 [리뷰어 배정 타입, 미션 저장소]에 관심 |
<수강신청 및 수강생 관리> '강의' 중에서도 [정원, 모집 상태]에 관심 |
<결제 및 환불> '강의' 중에서도 [환불 타입,가격]에 관심 |
3) 바운디드 컨텍스트에 따른 비즈니스 규칙
해결 영역 중에서 <수강신청 및 수강생 관리> 에 집중해서 비즈니스 규칙을 세우면 아래와 같다.
- 수강 후보자 수가 정원보다 많다면 수강신청할 수 없다.
- 모집 상태가 모집 중이 아니라면 수강신청할 수 없다.
- 모집 상태가 모집 중이 아니라면 수강 대기도 신청할 수 없다.
- 수강 승인된 수강생 수가 수강 정원보다 많을 수 없다.
- 승인 받은 수강 대기자는 수강생이 될 수 있다.
- 수강 대기자는 대기 번호를 알 수 있다.
- 모집 상태가 모집 중이 아니더라도 초대 코드를 통해 수강 신청이 가능하다.
비즈니스 규칙을 지키지 못하면 예상치 못하게 강의 자료가 부족하다든지, 리뷰어를 추가로 더 모집해야 하는 등 여러가지 문제가 발생할 것이다. 그러니 잘 지켜야 함!
4) 서비스에 문제가 없는지 시뮬레이션 해보자
- 회원A가 수강 신청을 하면 위 비즈니스 규칙에 따라 <강의> 엔티티의 [정원, 모집상태] 컬럼을 본다.
- 모집 상태가 모집 중이면서 정원이 아직 차지 않았다면 지나공은 수강 신청을 할 수 있다.
✖️ 비즈니스 규칙을 어기는 문제 발생 ✖️
이렇게만 이루어지면 강의의 정원이 30명인데 수강 인원은 그 이상이 되기도 한다.
5) 왜 비즈니스 규칙을 지키지 못했을까?!
한 명의 사용자 입장에서는 주문에 대한 불변식(정원이 아직 차지 않았다면 수강 신청 가능)이 지켜졌지만, 다중 사용자 환경에서는 별다른 조치가 없다면 동시성 제어에 실패해서 전체 시스템에서는 무결성이 깨질 수 있다.
여러 사용자가 수강 신청을 하려고 하나의 강의를 동시에 조회해버리면 그 각각의 상황에서의 강의 정보(수강 신청 가능 인원)가 동기화되지 않는다. 모든 상황에서 일관성을 갖도록 조치를 취해야 한다. 이때 애그리거트로 엔티티들을 나누면 된다.
6) Aggregate의 등장
애그리거트란,
- 시스템이 기대하는 책임을 수행하면서 일관성을 유지하는 단위
- 명령을 수행하기 위해 함께 조회하고 업데이트해야 하는 최소 단위
즉, 애그리거트는 실제 구현된 클래스들의 계약을 정리하는 방법이고, 내부의 구현은 자유롭게 할 수 있다. 일종의 '캡슐화'다. 애그리거트를 비즈니스 규칙이라고 볼 수 있다.
강의 애그리거트를 짜기 전에, 보다 직관적인 주문을 예시로 애그리거트의 특징을 보자.
- 애그리거트 단위로 일관성을 관리하기 때문에 복잡한 도메인을 단순한 구조로 만들 수 있고, 복잡도가 낮아진 만큼 도메인 기능을 확장하고 변경하는 데 필요한 노력도 줄어든다.
- 애그리거트는 경계를 가지기 때문에 한 애그리거트에 속한 객체는 다른 애그리거트에 속하지 않는다.
- 그 경계를 설정하는 데에 기본이 되는 것은 도메인 규칙과 요구사항이다. 도메인 규칙에 따라 함께 생성되거나 함께 변경되는 구성요소는 같은 애그리거트에 속할 가능성이 높다.
✖️ 같은 애그리거트에 속하는 예시 ✖️
- [주문할 상품 개수, 배송지 정보, 주문자 정보]는 모두 주문 시점에 함께 생성되니까 하나의 애그리거트에 속한다.
- 주문 상품 개수를 변경하면 도메인 규칙에 따라 주문 엔티티의 '총 주문 금액'을 새로 계산해야 한다. 그리고 사용자 요구에 따라 주문 상품 개수 말고도 배송지를 함께 변경하기도 한다. 이렇게 함께 변경되는 빈도가 높은 객체들도 한 애그리거트에 속할 가능성이 높다.
✖️ 다른 애그리거트에 속하는 예시 ✖️
- Product(상품 상세 정보) 와 Review(사용자리뷰)는 상품 상세 화면에 같이 있으므로 한 애그리거트에 속한다고 생각이 들 수 있는데, 아니다. 이 둘은 함께 생성되지 않고, 함께 변경되지도 않는다.
- 게다가 Product를 변경하는 주체랑 Review를 변경하는 주체도 다르고, 서로의 변경이 서로에게 영향을 주지 않기 때문에 서로 다른 애그리거트에 속한다.
이번에는 위에서부터 계속 봐 왔던 강의 도메인에 대해 Aggregate를 구성한 모습이다.
7) Aggregate Root와 전역 식별자
애그리거트 루트란,
- 도메인 규칙을 지키려면 애그리거트에 속한 모든 객체가 정상적인 상태를 가져야 하는데, 이 전체를 일관적인 상태로 관리하는 책임을 지는 엔티티이다.
- 가장 중요한 역할은 애그리거트의 일관성이 깨지지 않게 하는 것이다.
- 애그리거트 루트가 강제하는 도메인 규칙을 적용하려면, 애그리거트 외부에서 애그리거트에 속한 내부 객체를 직접 변경하면 안된다. 즉, 애그리거트 루트 엔티티의 식별자를 통해서만 접근하게 해야 한다.안 그러면 모델링의 일관성을 깨는 원인이 되니까!
애그리거트 루트의 역할은 애그리거트의 일관성이 깨지지 않게 하는 것이다. 그러므로 애그리거트 외부에서 애그리거트에 속한 내부 객체를 직접 변경하면 안되고, 루트 엔티티의 식별자를 통해서만 접근하게 해야 한다. 그렇지 않으면 애그리거트 루트가 강제하는 애그리거트의 내부 기능을 숨겨서 캡슐화를 할 때 애그리거트 루트가 도움이 된다. 루트 엔티티의 식별자를 애그리거트 외부에서 접근하는 방식으로 애그리거트가 구현해야 하는 기능을 만들 수 있어서다.
루트 엔티티의 식별자를 애그리거트 외부에서 사용할 수 있는 전역 식별자라고 하고, 이 전역 식별자를 통해 애그리거트를 조회할 수 있다. (밑에서 JPA Repository로 좀 더 알아볼 예정이다! )
8) '잠금' 개념 : 애그리거트로 잠금 구현하기
잠금이란, 다중 사용자 환경에서도 비즈니스 규칙이 깨지지 않게 하는 방법을 말한다.
- 비관적 잠금
- 한번에 한 명의 사용자만 처리하도록 데이터베이스의 레코드를 독점하는 방법
- 성능이 저하되고 교착상태가 발생할 수 있어서 비추.
- 낙관적 잠금
- 애그리거트 내에서 하나라도 변경을 하면 버전을 반드시 증가시키는 방법
- OPTIMISTIC_FORCE_INCEMENT를 사용하거나 버전을 수동으로라도 증가시켜야 한다.
우리는 낙관적 잠금을 사용하자.
9) Aggregate 내 자식 엔티티들과 지역 식별자
'강의' 애그리거트에서는 강의가 root이고, 그 외 수강생이나 수강 대기자 엔티티는 자식 엔티티가 된다. 그리고 이 자식 엔티티들의 식별자인 회원ID는 '지역식별자'다.
✖️회원 ID 가 전역식별자가 될 수 없는 이유✖️
회원 애그리거트에서는 회원 ID가 전역 식별자가 될 수 있겠지만 강의 애거리거트에서는 될 수 없다. 한 회원이 여러 강의를 들을 수 있고, 실제로 여러 강의를 신청했으면 그 회원의 ID가 여러 개 보일테니까, '회원 ID만 가지고 어떤 회원인지 식별'하는 게 불가능하다. 따라서 회원 ID로 수강생을 식별하려면 어떤 강의인지 특정 강의 ID가 필요하고, 이런 경우에 회원ID를 지역식별자라고 부른다.
10) 서비스 점검 후 애그리거트 분리
다시 수강신청 시작을 가정하고 서비스가 잘 되나 확인해보자.
- 다시 회원A가 수강신청을 한다.
- 강의 애그리거트로 진입해서 처음에는 root인 강의 엔티티의 [정원, 모집상태]를 확인하고, 조건이 만족되면 수강생 엔티티로 지나공이 등록된다.
- 이번에는 회원B가 들어와서 [정원, 모집상태, 자동승인여부]를 변경할 수 있다.
- 그 후 기존의 수강생이었던 회원C가 이름과 이메일을 수정하려고 한다. 이를 위해 강의 애그리거트의 root인 강의 엔티티에 접근한다.
- 그러면서 A가 수강신청할 때 조회한 강의 Entity(root)의 내용과 B가 수강정보를 수정하기 위해 조회한 강의 Entity(root)의 내용이 서로 달라진다.
✖️ 문제 발생 ✖️
트랜잭션 오류가 발생한다. optimistic_lock exception이 발생한다.
버전 때문이다. 동시에 접근하는 경우 문제가 발생하고, 처음 접근했던 버전과 나중에 다른 사람이 접근했을 때 데이터베이스 내용 버전이 달라서 생긴다.
현재의 애그리거트 모습에서 다시 한번 규칙들을 점검해보자.
- 여러 강사가 동시에 하나의 강의 정보를 수정하면 안되는데, 어차피 강의 하나 당 강사 하나라서 이런 경우는 있을 수 없으니 괜찮고.
- 수강생들은 강의가 열렸을 때 동시에 수강신청할 수 있어야 하는데, 이때 강사가 강의 정보를 변경하지 않으면 괜찮고.
- 수강생들이 수강생 정보를 수정할 때 전부가 잠금당해서 다른 애들이 수강신청을 못하면 안된다.
따라서 현재처럼 강의 애그리거트 안에 수강생 정보가 함께 묶여있어서 하나의 수강생 정보를 변경할 때마다 루트인 강의 엔티티를 타고 들어가서 모든 수강생의 정보를 조회하는 이 구조를 개선해야 한다. 즉, 수강생을 별도의 애그리거트로 분리해야 한다.
그러면 이제 강의 애그리거트와 수강생 애그리거트로 분리하게 되고, 각각 root entity가 생긴다.
수강생은 정원에 맞아야 수강이 가능하다는 비즈니스 규칙을 지키고 강의를 통해 수강생이 생성되므로 두 애그리거트는 연관관계를 가진다.
그리고 동시성 제어는 강의 수강생이라는 값 객체를 새로 두어서 구현할 수 있다. 이름, 이메일은 불변식을 지키는 데 필요없어서 분리됐고, 강의 수강생은 개념적으론 엔티티지만 실제 구현은 값 객체로 할 수도 있다.
큰 애그리거트와 작은 애그리거트
애그리거트를 쪼개면 동시성을 극대화할 수 있으나, 애그리거트를 너무 크게 설계하거나 잘못 설게하면 성능 상의 문제가 생긴다. 따라서 설계단계에서부터 소프트웨어 성능을 생각해야 한다.
애그리거트마다 서로 다른 데이터베이스를 사용할 수도 있고, 각 애그리거트마다 각자 다른 최적의 데이터벵이스를 선택해서 사용할 수도 있고, 극단적으로 하나의 애그리거트를 한개의 마이크로서비스에 연동할 수도 있고, 경우에 따라 낙관적 잠금을 사용할 수도 있고 비관적 잠금을 사용할 수도 있으며 중요하지 않은 영역은 그냥 잠금을 사용하지 않는 식으로도 구현할 수 있으니 필요에 따라 동시성을 끌어올릴 수 있다.
계층형 아키텍처의 계층을 나누는 것만큼 해결영역을 나누는 것도 중요하고, 데이터베이스의 동시성 제어보다 애플리케이션에서 동시성 제어하는 것이 더 한눈에 들어온다.
DDD, Aggregate Root와 JPA Repository
DDD에서 엔티티(Entity)마다 리파지토리(Repository)를 만드는 경우가 많은데 이럴 때 여러 엔티티를 묶어서 하나의 애그리거트로 만들어서 사용할 수 있다. 이러한 연관 객체의 묶음을 Aggregate라 하고, 특히 여러 엔티티를 묶어서 DB로부터 조회해오는 경우가 많을 땐 Aggregate Root에 해당되는 Entity에 대해서만 Repository를 만들 수 있다.
예를 들어 아래와 같이 각 엔티티별로 연관관계를 가지도록 설계했다고 하자.
Order(주문) 엔티티는 Delivery Address (배송지) 필드를 가지고, Payment(지불정보) 엔티티, Item(상품정보) 엔티티와 연관관계를 가진다. Payment(지불정보)엔티티는 지불 방식에 대한 정보를 가진 Method of Payment 엔티티와 연관관계를 가진다고 하자.
DDD에서는 위 그림에서 보이는 것과 같은 연관 객체의 묶음을 Aggregate라고 한다. 그리고 Aggregate Root인 Order에 대해서 레포지토리를 만들었다.
즉, Aggregate는 우리가 데이터 변경의 단위로 다루는 연관 객체의 묶음이고, 여기엔 루트(Root)와 경계(Boundary)가 있다. 루트는 하나만 존재하고, Aggregate 내의 특정 엔티티를 가리킨다. 경계 안의 객체는 서로 참조할 수 있지만, 경계 바깥의 객체는 해당 Aggregate의 구성요소 중 루트에만 참조할 수 있다. (다 위에서 설명했음~)
현재 Aggregate에 포함된 엔티티들 여러 개인데 있는데 이 중에 왜 Root가 Order일까?
DDD에서는 Global Identity(전역 식별성)을 지닌 엔티티가 루트 엔티티가 된다. 주문 파트는 배송지와, 지불방식, 상품 등의 데이터와 주고받는 부분이 많아서 우리가 보기에 Order가 루트임을 직관적으로 알 수 있다.
DDD에서는 도메인 규칙을 지키고 일관성을 잘 유지하기 위해서 외부에서 애그리거트 내부의 객체를 직접 접근할 수 없도록 한다. 항상 루트의 식별자로 접근하게 한다. 따라서 실질적으로 직접 접근해야 하는 Aggregate root에 대해서만 repository를 제공하고, 모든 객체 저장과 접근은 Repository에 위임해서 클라이언트가 모델에 집중하게 한다.
영속성 전이 속성 CascadeType과 DDD 속 Aggregate 연관 지어 생각하기
CascadeType.ALL + orphanRemoval = true
두 옵션을 간단하게 복습하면, cacadeType이 all이면 모든 옵션을 적용한 것이므로 persist를 통해 부모를 영속상태로 만들 때, 혹은 삭제할 때 병합할 때 등 부모의 생명주기에 따라 자식의 생명주기도 함께 변한다.
앞서 같은 애그리거트에 속한 엔티티들은 생명주기가 동일한 경우가 많다고 했다.
orphanRemoval이 true면 부모가 자식 객체를 컬렉션으로 가지고 있는 상황에서 어떤 한 객체를 그 컬렉션에서만 삭제해도 DB에서 그 자식객체에 대한 데이터가 삭제된다.
따라서 이 옵션들은 Aggregate Root를 구현할 때 유용하다. 부모와 자식 관계에서 부모가 Aggregate Root가 되고 부모 레포지토리만 만들어서 개발하는 경우에서 사용된다고 볼 수 있다.
'Tech > JPA' 카테고리의 다른 글
JPA Auditing 사용하기, BaseEntity로 생성일, 수정일 자동화 (0) | 2021.02.16 |
---|---|
JPA 8 -3 : JPQL 타입 표현과 기타식, JPQL 기본 함수 (0) | 2020.12.20 |
JPA 8 -2 : 조인, ON절, 서브쿼리, JPA 서브쿼리 한계 (0) | 2020.12.14 |
JPA 8 -1 : 프로젝션, 페이징 (0) | 2020.12.13 |
JPA 8 : JPQL 기본 문법과 쿼리 API (0) | 2020.12.09 |