❐ Description
"에릭 에반스 - 도메인 주도 설계" 6장의 Factory 내용 정리.
여기서 만든 예제는 스스로 이해를 돕기 위해 만든 간단한 예제.
❐ Factory와 Factory의 위치 선정
1. Aggregate 내부 Factory Method
Aggregate의 루트 엔티티에 팩토리 메서드를 배치하면, Aggregate의 무결성을 보장하는 책임을 루트가
담당하게 된다.
public class PurchaseOrder {
private final String orderId;
private final List<PurchaseItem> items;
// 생성자 생략
// ...
// FACTORY METHOD: PurchaseItem을 생성하고 Aggregate에 추가
public PurchaseItem addItem(String catalogPartId, int quantity) {
if (quantity <= 0) {
//🔥 Exception
}
// 새로운 PurchaseItem 생성
String itemNumber = generateUniqueItemNumber();
PurchaseItem newItem = new PurchaseItem(itemNumber, catalogPartId, quantity);
// Aggregate 무결성 검사: 중복된 아이템이 있는지 확인 (생략)
// ...
// Aggregate에 추가
items.add(newItem);
return newItem;
}
}
위의 예제에서 PurchaseOrder는 루트 엔티티이며, 무결성을 유지하기 위해 아래의 검증을 해주었다.
- quantity가 0 이상인지 검증
- 동일한 catalogPartId를 가진 PurchaseItem이 이미 존재하는지 확인.
2. 외부로 Factory Method 위임
책에서 나온 예제에서 TradeOrder(거래 주문)는 Brokerage Account와 별개의 Aggregate에 속하지만,
생성은 Brokerage Account가 담당하고 있다. 이는 Brokerage Account가 Trade Order에 들어갈 정보를
갖고 있으며, 어떠한 거래를 허용할지 판단하는 규칙을 담고 있기 때문이다.
public class BrokerageAccount {
private final String accountNumber;
private final String customerName;
private final Money balance;
public BrokerageAccount(String accountNumber, String customerName, Money balance) {
this.accountNumber = accountNumber;
this.customerName = customerName;
this.balance = balance;
}
public TradeOrder createTradeOrder(String security, int numberOfShares, TradeOrderType orderType) {
// 무결성 보장 : 계좌에 잔고가 없다면 주문을 생성할 수 없음.
if (balance.isZero()) {
// 🔥Exception
}
// TradeOrder 생성
return new TradeOrder(...);
}
}
public class TradeOrder {
private final String orderId; // 주문 ID
private final String brokerageAccountId; // BrokerageAccount ID
private final String security; // 거래 대상
private final int numberOfShares; // 주식 수량
private final TradeOrderType orderType; // 주문 유형 (BUY/SELL)
//...
}
현실 세계에서도 계좌에 잔고가 없다면 거래를 할 수 없다. 위의 예제는 이 경우를 코드로 표현한 것이다.
이런 경우 TradeOrder 생성에 대한 권한은 Brokerage Account가 가지고 있다.
따라서 Brokerage Account Aggregate에서 TradeOrder를 생성해주는 것이 자연스러운 설계다.
3. 독립형 Factory
독립형 팩토리는 특정 Aggregate의 루트 엔티티를 생성하는데 사용되며, Aggregate 내부에 팩토리를
두는 것이 부적절할 때 활용된다. 이 방식은 Aggregate의 생성 과정을 외부로 분리하여 불변식을 유지하고,
생성 로직을 관리하는 역할을 한다.
💭 불변식이란?
불변식(Invariant)은 소프트웨어 개발 및 프로그래밍에서, 특정 조건이 프로그램의 실행 과정에서
항상 참(True)으로 유지되어야 하는 속성을 말한다. 이는 주로 데이터 무결성을 보장하거나,
프로그램의 논리적 올바름을 유지하기 위해 사용된다.
* 개인적인 생각으로 그냥 validation logic이라고 본다.
public class BrokerageAccountFactory {
private int accountNumberSequence = 1000;
public BrokerageAccount create(String customerName, boolean marginApproved) {
// 고유한 계좌 번호 생성
String accountNumber = generateAccountNumber();
// BrokerageAccount 생성
BrokerageAccount account = new BrokerageAccount(accountNumber, customerName);
// 마진 계좌 생성 여부에 따른 추가 처리
if (marginApproved) {
MarginAccount marginAccount = new MarginAccount(accountNumber);
account.setMarginAccount(marginAccount);
}
return account;
}
}
위와 같이 독립형 팩토리를 사용하는 이유는 아래와 같다.
- Aggregate 생성 로직이 복잡하기 때문
- accountNumber를 관리하거나 MarginAccount를 동적으로 생성하는 과정을 팩토리에서 처리
- Aggregate 생성 로직을 캡슐화하여 불변성을 유지
이렇게 하면 다음의 장점을 챙길 수 있다.
- 책임 분리 :BrokerageAccount는 생성 과정에서 독립적으로 처리해야 할 책임을 가지지 않는다.
- 유연성: 팩토리를 통해 추가적인 로직(예: 계좌 번호 시퀀스 관리, 정책 적용)을 쉽게 구현할 수 있다.
- 재사용성 : 팩토리는 다양한 조건에 따라 Aggregate를 일관되게 생성할 수 있다.
❐ 생성자만으로 충분한 경우
☑️ 클래스가 단순한 타입인 경우
클래스가 계층구조를 형성하지 않으며, 인터페이스를 구현하거나 다형성을 활용하지 않은 경우.
☑️ 클라이언트가 구현체에 관심이 있는 경우
STRATEGY 패턴과 같은 경우, 클라이언트가 특정 구현체를 직접 사용하려고 선택할 때.
또는 아래와 같이 구현체를 명시적으로 사용하는 경우
List<String> list = new ArrayList<>();
☑️ 생성자가 단순하거나 복잡하지 않은 경우
☑️ 생성자가 불변식을 보장할 수 있는 경우
생성자 내부에서 값 검증 및 초기화가 끝나는 경우
☑️ 팩토리를 도입할 필요성이 크지 않은 경우
❐ 인터페이스 설계
팩토리 설계 시 인터페이스와 매개변수를 어떻게 설계해야 하는지에 대한 가이드
1. 원자적 연산 (Atomic Operation)
팩토리의 메서드는 단일 작업을 완결적으로 수행해야 하며, 다음 사항을 고려해야 한다.
☑️ 모든 필요한 데이터를 한 번에 전달
// 나쁜 설계: 생성 시 추가 호출이 필요
public PurchaseOrder createOrder() {
PurchaseOrder order = new PurchaseOrder();
order.setCustomer(customer); // 별도의 호출로 설정
order.addItems(items); // 별도의 호출로 추가
return order;
}
// 좋은 설계: 필요한 모든 데이터를 매개변수로 전달
public PurchaseOrder createOrder(Customer customer, List<Item> items) {
return new PurchaseOrder(customer, items);
}
☑️ 실패 처리 : 객체의 생성이 실패할 경우, 이를 어떻게 처리할지 명확히 해야 한다.
public PurchaseOrder createOrder(Customer customer, List<Item> items) {
if (customer == null || items.isEmpty()) {
throw new IllegalArgumentException("Invalid input for creating PurchaseOrder");
}
return new PurchaseOrder(customer, items);
}
☑️ 매개변수의 결합 : 결합도를 최소화하거나 자연스러운 형태로 만드는 것이 중요
1. 결합의 정도
- 낮은 결합 : 매개변수를 그대로 객체의 속성으로 사용하는 경우
- 높은 결합 : 매개변수를 이용해 내부적으로 다른 객체를 생성하거나, 추가적인 로직에 사용
2. 적절한 매개변수 선택
// 가장 안전한 매개변수는 하위 계층에서 전달된 데이터
// 기본 데이터 타입 또는 도메인 객체
public PurchaseOrder createOrder(Customer customer, List<CatalogPart> catalogParts) {
List<Item> items = catalogParts.stream()
.map(part -> new Item(part.getId(), part.getName()))
.collect(Collectors.toList());
return new PurchaseOrder(customer, items);
}
3. 구상 클래스가 아닌 추상 타입 사용
// 구상 클래스에 결합된 나쁜 설계
public PurchaseOrder createOrder(Customer customer, ArrayList<Item> items) { ... }
// 추상 타입에 결합된 좋은 설계
public PurchaseOrder createOrder(Customer customer, List<Item> items) { ... }
❐ 불변식 로직의 위치
validation 체크를 어디에서 검사하고 책임질 것인지에 대한 가이드
1. Factory에 불변식을 두는 경우
public class OrderFactory {
public static Order createOrder(Customer customer, List<Item> items) {
if (customer == null) {
throw new IllegalArgumentException("Customer cannot be null");
}
if (items == null || items.isEmpty()) {
throw new IllegalArgumentException("Order must contain at least one item");
}
return new Order(customer, items);
}
}
- Factory는 생성 과정에서 불변식을 검사하고, 객체가 복잡해지는 것을 방지
- Factory는 생성물의 내부 구조를 알기 때문에, 불변성을 검사하기에 적합
- 특히, Aggregate처럼 여러 객체가 관련된 전체적인 규칙을 검증하는 데 적합
2. 객체 내부에 불변식을 두는 경우
public class Order {
private final Customer customer;
private final List<Item> items;
public Order(Customer customer, List<Item> items) {
if (customer == null) {
throw new IllegalArgumentException("Customer cannot be null");
}
if (items == null || items.isEmpty()) {
throw new IllegalArgumentException("Order must contain at least one item");
}
this.customer = customer;
this.items = new ArrayList<>(items); // 방어적 복사
}
}
- 객체가 스스로 불변식을 관리하므로, 팩토리 외의 다른 곳에서 객체가 생성되더라도 항상 유효한 상태를 유지
- Entity 처럼 식별 속성이 중요한 경우
- 객체가 스스로 유효성을 유지해야 하는 경우
'Architecture > DDD' 카테고리의 다른 글
Bounded Context (0) | 2025.03.17 |
---|---|
7. 언어의 사용 (확장 예제) (0) | 2025.03.05 |
6-3. Repository (0) | 2024.12.06 |
ReadMe.md (0) | 2024.12.03 |