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 엔티티 구현 가능
◦
침투적이지 않음
▪
테스트 쉽게 할 수 있는지? / 외부 의존성이 있는지?
◦
레이지 로딩 (런타임에서 도는거) 신경써야함
▪
성능 튜닝을 위한 뭔가를 밀어넣는순간 신경써야함
▪
최대한쓰지말 것
•
엔티티를 분리하는 것은 선호하지 않음
◦
함께 여러번 바꾸는게 힘들다




