List
Search
1. 애그리거트 경계 정하기
1.1. 애그리거트 설계
•
기능 구현과 불변식을 고려하여 설계
•
경계는 어떻게 정하냐?
◦
보통 크게 얘기하는 단어들
1.2. 배달앱 예제 불변식 분석
1.2.1. 도메인 객체와 불변식
•
불변식
◦
판매 중인 Menu에는 OptionGroup이 한 개 이상 존재해야 한다.
◦
판매 중인 Menu에는 가격이 0원 이상인 Option이 한 개 이상 존재해야 한다.
◦
판매 중인 Menu에는 필수 OptionGroup이 한 개 이상 존재해야 한다.
◦
판매 중인 Menu에는 필수 OptionGroup이 세 개 이하로 존재해야 한다.
◦
Menu 안에서 OptionGroup의 이름은 중복되지 말아야 한다.
◦
OptionGroup에는 Option이 한 개 이상 존재해야 한다.
1.2.2. 기능 (1) 메뉴 공개하기
•
메뉴를 공개하기 위해서는 아래 4가지 불변식을 반드시 만족시켜야함
◦
판매 중인 Menu에는 OptionGroup이 한 개 이상 존재해야 한다.
◦
판매 중인 Menu에는 가격이 0원 이상인 Option이 한 개 이상 존재해야 한다.
◦
판매 중인 Menu에는 필수 OptionGroup이 한 개 이상 존재해야 한다.
◦
판매 중인 Menu에는 필수 OptionGroup이 세 개 이하로 존재해야 한다.
•
불변식 위반
◦
OptionGroup이 존재하지 않는 경우
◦
판매 중인 메뉴에 있는 모든 Option 가격이 0원인 경우
◦
필수 OptionGroup이 존재하지 않는 경우
◦
필수 OptionGroup이 네 개 이상 존재하는 경우
1.2.3. 기능 (2) 옵션 그룹 추가하기
•
옵션 그룹을 추가하기 위해서는 아래 2가지 불변식을 반드시 만족시켜야함
◦
판매 중인 Menu에는 필수 OptionGroup이 세 개 이하로 존재해야 한다.
◦
Menu 안에서 OptionGroup의 이름은 중복되지 말아야 한다.
•
불변식 위반
◦
필수 OptionGroup이미 3개 존재하는데, 필수 옵션 그룹을 추가하려는 경우
◦
동일한 옵션 그룹 이름으로 신규 옵션 그룹을 추가하려는 경우
1.2.4. 기능 (3) 옵션 그룹 삭제하기
•
옵션 그룹을 삭제하기 위해서는 아래 3가지 불변식을 반드시 만족시켜야함
◦
판매 중인 Menu에는 OptionGroup이 한 개 이상 존재해야 한다.
◦
판매 중인 Menu에는 가격이 0원 이상인 Option이 한 개 이상 존재해야 한다.
◦
판매 중인 Menu에는 필수 OptionGroup이 한 개 이상 존재해야 한다.
•
불변식 위반
◦
판매 중인 OptionGroup이 존재하지 않는 경우
◦
남은 Option 가격이 모두 0원인 경우
◦
남은 OptionGroup이 전부 필수가 아닌 경우
1.3. 배달앱 예제 애그리거트 설계
•
불변식 기반으로 애그리거트 경계 결정
1.3.1. 애그리거트 경계 및 루트 정하기
•
경계
1.
메뉴 - 옵션그룹 - 옵션
2.
가게
•
루트
◦
메뉴 & 가게
◦
루트는 애그리거트 전체의 불변식을 보장
1.3.2. 엔티티와 값 객체 정하기
•
식별성, 연속성, 속성 등을 고려하여 설정
1.3.3. 연관관계 설정 및 구현
•
연관관계는 기능을 구현하는데 적합한 방향으로 결정
2. 애그리거트 구현하기
2.1. 애그리거트 루트
•
Base AggregateRoot
public abstract class AggregateRoot
<T extends DomainEntity<T, TID>, TID> extends DomainEntity<T, TID> {
}
Java
복사
•
Menu Aggregate
public class Menu extends AggregateRoot<Menu, MenuId> {
private MenuId id;
// 외부는 ID
private ShopId shopId;
private String name;
private String description;
private boolean open;
// 내부 엔티티는 객체 참조
private Set<OptionGroup> optionGroups = new HashSet<>();
public Menu(ShopId shopId, String name, String description) {
this(null, shopId, name, description, false, new HashSet<>());
}
}
Java
복사
•
Shop Aggregate
public class Shop extends AggregateRoot<Shop, ShopId> {
private ShopId id;
private String name;
private Money minOrderPrice;
// 내부 값 객체는 객체 참조
private Map<DayOfWeek, TimePeriod> operatingHours;
public Shop(
String name,
boolean open,
Money minOrderPrice,
Map<DayOfWeek, TimePeriod> operatingHours) {
this(null, name, open, minOrderPrice, operatingHours);
}
}
Java
복사
2.2. 기능 (1) 메뉴 공개하기
•
불변식 통제는 루트의 책임
◦
Menu Aggregate
•
기능은 애그리거트 내부의 객체들 사이의 협력으로 구현
◦
책임 주도 설계
2.2.1. 불변식
•
판매 중인 Menu에는 OptionGroup이 한 개 이상 존재해야 한다.
•
판매 중인 Menu에는 가격이 0원 이상인 Option이 한 개 이상 존재해야 한다.
•
판매 중인 Menu에는 필수 OptionGroup이 한 개 이상 존재해야 한다.
•
판매 중인 Menu에는 필수 OptionGroup이 세 개 이하로 존재해야 한다.
2.2.2. 구현
•
Menu Aggregate
◦
메뉴 판매 시작 open
▪
불변식 validation 진행
◦
countOfFreeOptionGroups
▪
OptionGroup Entity에 협력 요청
public class Menu extends AggregateRoot<Menu, MenuId> {
public void open() {
if (optionGroups.isEmpty()) {
throw new IllegalStateException(
"옵션그룹은 하나 이상 존재해야 합니다.");
}
if (countOfFreeOptionGroups() < 1) {
throw new IllegalStateException(
"금액이 설정된 옵션그룹이 최소 1개는 등록되어 있어야 합니다.");
}
if (countOfMandatoryOptionGroups() == 0) {
throw new IllegalStateException(
"필수 옵션그룹은 최소 1개는 등록되어 있어야 합니다.");
}
if (countOfMandatoryOptionGroups() > 3) {
throw new IllegalStateException(
"필수 옵션그룹은 3개까지만 등록가능 합니다.");
}
open = true;
}
private long countOfFreeOptionGroups() {
return optionGroups.stream().filter(group -> group.isFree()).count();
}
}
Java
복사
•
OptionGroup Entity
◦
isFree - 가격이 0원인 옵션이 있는지를 Option VO에게 협력 요청
@Getter
public class OptionGroup extends DomainEntity<OptionGroup, OptionGroupId> {
private OptionGroupId id;
private String name;
private boolean mandatory;
private Set<Option> options = new HashSet<>();
public OptionGroup(
OptionGroupId id,
String name,
boolean mandatory,
Set<Option> options) {
this.id = id;
this.mandatory = mandatory;
setName(name);
setOptions(options);
}
private void setName(String name) {
if (name == null || name.length() < 2) {
throw new IllegalArgumentException(
"옵션그룹명은 길이는 최소 2글자 이상이어야 합니다.");
}
this.name = name;
}
private void setOptions(Set<Option> options) {
if (options == null || options.size() < 1) {
throw new IllegalArgumentException(
"옵션의 길이는 최소 1개 이상이어야 합니다.");
}
this.options.clear();
this.options = options;
}
public boolean isFree() {
return options.stream().allMatch(Option::isFree);
}
}
Java
복사
•
Option Value Object
@Getter
public class Option extends ValueObject<Option> {
private String name;
private Money price;
public Option(String name, Money price) {
if (name == null || name.length() < 2) {
throw new IllegalArgumentException("옵션명은 2글자 이상이어야 합니다.");
}
if (price == null) {
throw new NullPointerException("옵션 가격은 null이여서는 안됩니다.");
}
this.name = name;
this.price = price;
}
public boolean isFree() {
return Money.ZERO.equals(price);
}
@Override
protected Object[] getEqualityFields() {
return new Object[] { name, price };
}
}
Java
복사
2.3. 기능 (2) 옵션 그룹 추가하기
2.3.1. 불변식
•
판매 중인 Menu에는 필수 OptionGroup이 세 개 이하로 존재해야 한다.
•
Menu 안에서 OptionGroup의 이름은 중복되지 말아야 한다.
2.3.2. 구현
•
OptionGroup Entity에 협력 요청
◦
isMandatory
◦
countOfMandatoryOptionGroups
public class Menu extends AggregateRoot<Menu, MenuId> {
public void addOptionGroup(OptionGroup optionGroup) {
if (optionGroup == null) {
throw new IllegalArgumentException();
}
if (isOpen() &&
optionGroup.isMandatory() &&
countOfMandatoryOptionGroups() >= 3) {
throw new IllegalArgumentException("...");
}
if (groups.stream().anyMatch(
group -> group.getName().equals(optionGroup.getName()))) {
throw new IllegalArgumentException("...");
}
groups.add(optionGroup);
}
private long countOfMandatoryOptionGroups() {
return groups.stream().filter(group -> group.isMandatory()).count();
}
}
Java
복사
2.4. 기능 (3) 옵션 그룹 삭제하기
2.4.1. 불변식
•
판매 중인 Menu에는 OptionGroup이 한 개 이상 존재해야 한다.
•
판매 중인 Menu에는 가격이 0원 이상인 Option이 한 개 이상 존재해야 한다.
•
판매 중인 Menu에는 필수 OptionGroup이 한 개 이상 존재해야 한다.
2.4.2. 구현
•
옵션그룹삭제 removeOptionGroup
◦
불변식 validation 진행
public class Menu extends AggregateRoot<Menu, MenuId> {
public void removeOptionGroup(OptionGroupId optionGroupId) {
OptionGroup optionGroup = optionGroups.stream()
.filter(group -> group.getId().equals(optionGroupId))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException());
if (!isOpen()) {
optionGroups.remove(optionGroup);
return;
}
if (optionGroups.size() == 1) {
throw new IllegalArgumentException(
"옵션그룹은 최소 1개는 등록되어 있어야 합니다.");
}
if (!optionGroup.isFree() && countOfFreeOptionGroups() == 1) {
throw new IllegalArgumentException(
"금액이 설정된 옵션그룹이 최소 1개는 등록되어 있어야 합니다.");
}
if (optionGroup.isMandatory() && countOfMandatoryOptionGroups() == 1) {
throw new IllegalArgumentException(
"필수 옵션그룹은 최소 1개는 등록되어 있어야 합니다.");
}
optionGroups.remove(optionGroup);
}
}
Java
복사
•
OptionGroup 엔티티 삭제 시 지역 식별자 이용하여 탐색
@Getter
public class OptionGroup extends DomainEntity<OptionGroup, OptionGroupId> {
// OptionGroup은 엔티티
private OptionGroupId id;
...
}
public class Menu extends AggregateRoot<Menu, MenuId> {
// Menu 애그리거트는 지역 식별자를 이용하여 OptionGroup 탐색
public void removeOptionGroup(OptionGroupId optionGroupId) {
OptionGroup optionGroup = optionGroups.stream()
.filter(group -> group.getId().equals(optionGroupId))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException());
...
}
}
Java
복사
2.4. 기능 (4) 옵션 그룹 & 옵션 이름 변경
2.4.1. 옵션 그룹 이름 변경
•
OptionGroup 기본 의 이름을 반찬추가 선택 으로 변경하고 싶다면?
•
OptionGroup은 엔티티로 모델링
public class OptionGroup extends DomainEntity<OptionGroup, OptionGroupId> {
private OptionGroupId id;
...
}
Java
복사
•
OptionGroup은 엔티티이므로 연속성과 식별성을 의미함
◦
지역 식별자를 이용하여 엔티티를 찾은 후 이름 변경
public class Menu extends AggregateRoot<Menu, MenuId> {
// 지역 식별자를 이용하여 해당 엔티티 탐색
public void changeOptionGroupName(OptionGroupId optionGroupId, String name) {
if (groups.isEmpty()) {
throw new IllegalArgumentException("옵션그룹이 비어있습니다.");
}
if (groups.stream().anyMatch(group -> group.getName().equals(name))) {
throw new IllegalArgumentException("이름이 동일한 옵션그룹이 이미 존재합니다.");
}
// 지역 식별자를 이용하여 해당 엔티티 탐색
OptionGroup optionGroup =
groups.stream()
.filter(group -> group.getId().equals(optionGroupId))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException());
// 엔티티 자체의 상태 변경
optionGroup.changeName(name);
}
}
Java
복사
•
OptionGroup은 엔티티이므로 ID로 추적
@RestController
public class MenuController {
@PutMapping("/menus/{menuId}/optionGroups/{optionGroupId}")
public void changeOptionGroupName(@PathVariable("menuId") Long menuId,
@PathVariable("optionGroupId") Long optionGroupId,
@RequestBody OptionGroupNameChangeRequest request) {
menuService.changeOptionGroupName(new MenuId(menuId),
new OptionGroupId(optionGroupId),
request.getName());
}
}
class OptionGroupNameChangeRequest {
private String name;
// getter/setter
...
}
Java
복사
2.4.2. 옵션 이름 변경
•
Option은 값 객체로 모델링
public class Option extends ValueObject<Option> {
private String name;
private Money price;
...
}
Java
복사
•
Option의 이름을 변경하고 싶은 경우?
◦
값 비교를 통해 동일한 객체 찾음
◦
값 객체는 연속성이 없기 때문에 새로운 객체로 교체
•
Menu Aggregate
public class Menu extends AggregateRoot<Menu, MenuId> {
// OptionGroup은 식별자로 탐색
// Option 값 객체는 값으로 탐색
public void changeOptionName(OptionGroupId optionGroupId,
Option target,
String optionName) {
if (groups.isEmpty()) {
throw new IllegalArgumentException("옵션그룹이 비어있습니다.");
}
// 지역 식별자를 이용해 OptionGroup 탐색
OptionGroup optionGroup =
groups.stream()
.filter(group -> group.getId().equals(optionGroupId))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException());
// 엔티티의 상태 변경
optionGroup.changeOptionName(target, optionName);
}
}
Java
복사
•
OptionGroup Entity
public class OptionGroup extends DomainEntity<OptionGroup, OptionGroupId> {
// Option 값 객체는 값으로 탐색
public void changeOptionName(Option target,
String optionName) {
Option option = options.stream()
// 값만 같으면 같은 객체
.filter(each -> each.equals(target))
.findFirst()
.orElseThrow(IllegalArgumentException::new);
// 기존의 Option 객체는 삭제 후 이름이 변경된 새로운 객체 추가
options.remove(option);
options.add(target.changeName(optionName));
}
}
Java
복사
•
Option Value Object
public class Option extends ValueObject<Option> {
public Option changeName(String name) {
// 새로운 객체 생성
return new Option(name, this.price);
}
}
Java
복사
•
Option의 이름 변경
◦
MenuId, OptionGroupID는 엔티티 이므로 아이디로 추적
◦
Option은 값 객체이므로 값으로 추적
@RestController
public class MenuController {
// 엔티티 이므로 아이디로 추적
@PutMapping("/menus/{menuId}/optionGroups/{optionGroupId}/options")
public void changeOptionName(@PathVariable("menuId") Long menuId,
@PathVariable("optionGroupId") Long optionGroupId,
@RequestBody OptionNameChangeRequest request) {
menuService.changeOptionName(
new MenuId(menuId),
new OptionGroupId(optionGroupId),
// 값 객체이므로 Option 값을 이용하여 추적
new Option(request.getCurrentName(), Money.wons(request.getCurrentPrice())),
request.getNewName());
}
}
class OptionNameChangeRequest {
private String currentName;
private Long currentPrice;
private String newName;
// getter/setter
...
}
Java
복사
2.5. ORM Entity와 DDD Entity
•
ORM 엔티티로 DDD 엔티티 구현 가능
◦
침투적이지 않음
▪
테스트 쉽게 할 수 있는지? / 외부 의존성이 있는지?
◦
레이지 로딩 (런타임에서 도는거) 신경써야함
▪
성능 튜닝을 위한 뭔가를 밀어넣는순간 신경써야함
▪
최대한쓰지말 것
•
엔티티를 분리하는 것은 선호하지 않음
◦
함께 여러번 바꾸는게 힘들다