๐ฅ ๋๋ฉ์ธ ๋ชจ๋ธ ํจํด์ด๋?
๋๋ฉ์ธ ๋ชจ๋ธ ํจํด(Domain Model Pattern)์ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋น์ฆ๋์ค ๋ก์ง์ ๋๋ฉ์ธ ๊ฐ์ฒด(์ํฐํฐ) ๋ด๋ถ์ ํฌํจ์ํค๋ ์ค๊ณ ํจํด์ ๋๋ค. ์ฆ, ๋น์ฆ๋์ค ๊ท์น, ๋ฐ์ดํฐ ๋ณ๊ฒฝ, ๋ฐ์ดํฐ ๊ฒ์ฆ ๋ฑ์ ๋๋ฉ์ธ ๊ฐ์ฒด๊ฐ ์ง์ ์ํํ๋๋ก ์ค๊ณํฉ๋๋ค.
์ด ํจํด์ DDD(Domain-Driven Design)์ ํต์ฌ ๊ฐ๋ ์ค ํ๋์ด๋ฉฐ, ๊ฐ์ฒด์งํฅ์ ์ฅ์ ์ ์ต๋ํ ํ์ฉํฉ๋๋ค.
ํน์ง
- ๋๋ฉ์ธ ๊ฐ์ฒด์ ์ฑ ์ ๋ถ์ฌ:๊ฐ์ฒด๊ฐ ์์ ์ ์ํ๋ฅผ ๊ด๋ฆฌํ๊ณ , ๊ด๋ จ๋ ๋น์ฆ๋์ค ๋ก์ง์ ์ํํฉ๋๋ค.
- ์:
Order
๊ฐ์ฒด๊ฐ ์ฃผ๋ฌธ ์์ฑ, ์ฃผ๋ฌธ ์ทจ์ ๋ฑ์ ์ํ. - ๋น์ฆ๋์ค ๋ก์ง์ ์บก์ํ:๋ฐ์ดํฐ์ ์ด๋ฅผ ์ฒ๋ฆฌํ๋ ๋ก์ง์ด ํ ๊ฐ์ฒด ๋ด๋ถ์ ์บก์ํ๋ฉ๋๋ค.
- ์ํฐํฐ ์ค์ฌ ์ค๊ณ:์ํฐํฐ ๊ฐ์ฒด๊ฐ ๋จ์ํ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ ์ญํ ๋ง ํ์ง ์๊ณ , ๋น์ฆ๋์ค ๋ก์ง์ ๋ด๊ณ ์์ต๋๋ค.
๋๋ฉ์ธ ๋ชจ๋ธ ํจํด์ ๊ตฌ์กฐ
1. ์ํฐํฐ(Entity)
์ ํ๋ฆฌ์ผ์ด์ ์ ๋น์ฆ๋์ค ๋ก์ง๊ณผ ๋ฐ์ดํฐ๋ฅผ ์บก์ํํฉ๋๋ค.
์: Order
, Member
, Product
๋ฑ.
2. ๋ฐธ๋ฅ(Value Object)
ํน์ ๊ฐ์ ํํํ๋ ๊ฐ์ฒด๋ก, ๊ฐ ์์ฒด๊ฐ ์ค์ํ๋ฉฐ ๋ถ๋ณ์ฑ์ ๊ฐ์ง๋๋ค.
์: Address
, Money
๋ฑ.
3. ์ ๊ทธ๋ฆฌ๊ฒ์ดํธ(Aggregate)
์ํฐํฐ์ ๋ฐธ๋ฅ ๊ฐ์ฒด๊ฐ ํ๋๋ก ๋ฌถ์ธ ์งํฉ์ ๋๋ค.
์ ๊ทธ๋ฆฌ๊ฒ์ดํธ ๋ฃจํธ(์ฃผ๋ก ์ํฐํฐ)๊ฐ ์ธ๋ถ์์ ์ํธ์์ฉ์ ๋ด๋นํ๋ฉฐ, ๋ด๋ถ ๊ตฌ์ฑ ์์๋ฅผ ๊ด๋ฆฌํฉ๋๋ค.
๋๋ฉ์ธ ๋ชจ๋ธ ํจํด์ ์ฅ์
- ๊ฐ์ฒด์งํฅ์ ์ฅ์ ํ์ฉ: ๋ฐ์ดํฐ์ ํ์๋ฅผ ํ๋์ ๊ฐ์ฒด๋ก ๋ฌถ์ด ๊ฐ์ฒด์งํฅ์ ์ค๊ณ๋ฅผ ์ด์งํฉ๋๋ค.
- ๋น์ฆ๋์ค ๋ก์ง ์ง์คํ: ๋น์ฆ๋์ค ๋ก์ง์ด ๊ฐ์ฒด์ ์ง์ค๋์ด ์ฝ๋๋ฅผ ์ดํดํ๊ธฐ ์ฝ์ต๋๋ค.
- ์์ง๋ ํฅ์: ๊ด๋ จ๋ ๋ก์ง๊ณผ ๋ฐ์ดํฐ๊ฐ ํ ๊ณณ์ ๋ชจ์ฌ ์ ์ง๋ณด์๊ฐ ์ฌ์์ง๋๋ค.
- ์ค๋ณต ์ฝ๋ ๊ฐ์: ๊ฐ์ฒด๊ฐ ์ํ๋ฅผ ์ค์ค๋ก ๊ด๋ฆฌํ๋ฏ๋ก, ์๋น์ค ๊ณ์ธต์ ๋ก์ง ์ค๋ณต์ ์ค์ผ ์ ์์ต๋๋ค.
๋๋ฉ์ธ ๋ชจ๋ธ ํจํด์ ๋จ์
- ์ด๊ธฐ ์ค๊ณ ๋น์ฉ: ๋๋ฉ์ธ ๋ชจ๋ธ์ ์ค๊ณํ๊ณ ๊ตฌํํ๋ ๋ฐ ๋ง์ ์๊ฐ์ด ํ์ํฉ๋๋ค.
- ๋ณต์ก์ฑ ์ฆ๊ฐ: ๊ฐ์ฒด ๊ฐ ๊ด๊ณ์ ๋น์ฆ๋์ค ๋ก์ง์ด ๋ณต์กํ ๊ฒฝ์ฐ, ๊ด๋ฆฌ๊ฐ ์ด๋ ค์์ง ์ ์์ต๋๋ค.
- ์๋ จ๋ ๊ฐ๋ฐ์ ํ์: ๊ฐ์ฒด์งํฅ ์ค๊ณ์ ๋๋ฉ์ธ ์ฃผ๋ ์ค๊ณ(DDD)์ ๋ํ ๊น์ ์ดํด๊ฐ ํ์ํฉ๋๋ค.
์์: Order ํด๋์ค
@Entity
public class Order {
@Id
@GeneratedValue
private Long id;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List orderItems = new ArrayList<>();
@OneToOne(cascade = CascadeType.ALL)
private Delivery delivery;
private LocalDateTime orderDate;
@Enumerated(EnumType.STRING)
private OrderStatus status; // ์: ORDERED, CANCELED
//== ๋น์ฆ๋์ค ๋ก์ง ==//
// ์ฃผ๋ฌธ ์์ฑ
public static Order createOrder(Member member, Delivery delivery, OrderItem... orderItems) {
Order order = new Order();
order.setMember(member);
order.setDelivery(delivery);
for (OrderItem orderItem : orderItems) {
order.addOrderItem(orderItem);
}
order.setOrderDate(LocalDateTime.now());
order.setStatus(OrderStatus.ORDERED);
return order;
}
// ์ฃผ๋ฌธ ์ทจ์
public void cancel() {
if (delivery.getStatus() == DeliveryStatus.COMPLETE) {
throw new IllegalStateException("์ด๋ฏธ ๋ฐฐ์ก ์๋ฃ๋ ์ฃผ๋ฌธ์ ์ทจ์ํ ์ ์์ต๋๋ค.");
}
this.setStatus(OrderStatus.CANCELED);
for (OrderItem orderItem : orderItems) {
orderItem.cancel(); // ์ฃผ๋ฌธ์ํ๋ ์ทจ์
}
}
// ์ด ์ฃผ๋ฌธ ๊ฐ๊ฒฉ ๊ณ์ฐ
public int getTotalPrice() {
return orderItems.stream()
.mapToInt(OrderItem::getTotalPrice)
.sum();
}
}
๋ถ์
1. createOrder
๋ฉ์๋
์ ์ ๋ฉ์๋๋ก ์ฃผ๋ฌธ์ ์์ฑํ๋ฉฐ, Member
, Delivery
, OrderItem
์ ์ค์ ํฉ๋๋ค.
์ฃผ๋ฌธ ์์ฑ ๊ณผ์ ์์์ ๋ณต์กํ ๋ก์ง์ ์บก์ํํ์ฌ ์๋น์ค ๊ณ์ธต์ ๋จ์ํํฉ๋๋ค.
2. cancel
๋ฉ์๋
์ฃผ๋ฌธ ์ํ๋ฅผ ์ทจ์๋ก ๋ณ๊ฒฝํ๋ฉฐ, ์ฐ๊ด๋ OrderItem
๋ ์ทจ์ํฉ๋๋ค.
๋น์ฆ๋์ค ๊ท์น(์: ๋ฐฐ์ก ์๋ฃ ํ ์ทจ์ ๋ถ๊ฐ)์ ์บก์ํํฉ๋๋ค.
3. getTotalPrice
๋ฉ์๋
์ฃผ๋ฌธ์ํ์ ์ด ๊ฐ๊ฒฉ์ ๊ณ์ฐํฉ๋๋ค.
๋ชจ๋ ๊ณ์ฐ ๋ก์ง์ด Order
๋ด๋ถ์ ์์ผ๋ฏ๋ก, ์ธ๋ถ์์ ๊ณ์ฐ ๋ก์ง์ ์ ํ์๊ฐ ์์ต๋๋ค.
์๋น์ค ๊ณ์ธต์ ๋จ์ํ
๋๋ฉ์ธ ๋ชจ๋ธ ํจํด์ ์ฌ์ฉํ๋ฉด ์๋น์ค ๊ณ์ธต์ ์ฝ๋๊ฐ ๊ฐ๊ฒฐํด์ง๋๋ค.
// ์๋น์ค ๊ณ์ธต
@Transactional
public Long order(Member member, Delivery delivery, OrderItem... orderItems) {
Order order = Order.createOrder(member, delivery, orderItems);
orderRepository.save(order);
return order.getId();
}
์๋น์ค ๊ณ์ธต์์๋ ๋น์ฆ๋์ค ๋ก์ง์ ์ํํ๋ ๋์ , ๋๋ฉ์ธ ๊ฐ์ฒด๋ฅผ ์กฐํฉํ๊ณ ์ ์ฅํ๋ ์ญํ ๋ง ์ํํฉ๋๋ค.
๋๋ฉ์ธ ๋ชจ๋ธ ํจํด๊ณผ JPA
JPA๋ ์ํฐํฐ ๊ฐ์ฒด๋ฅผ ๊ด๋ฆฌํ๋ฉฐ, ๋๋ฉ์ธ ๋ชจ๋ธ ํจํด๊ณผ ์ ๋ง์ต๋๋ค.
ํนํ JPA์ ์์์ฑ ์ปจํ ์คํธ๋ฅผ ํ์ฉํ๋ฉด, ๋๋ฉ์ธ ๊ฐ์ฒด๊ฐ ์ํ๋ฅผ ๋ณ๊ฒฝํ๋๋ผ๋ JPA๊ฐ ์ด๋ฅผ ๊ฐ์งํ๊ณ ์๋์ผ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ฐ์ํฉ๋๋ค.
๋๋ฉ์ธ ๋ชจ๋ธ ํจํด์ ํ์ฉ
1. ์ ํฉํ ๊ฒฝ์ฐ
- ๋น์ฆ๋์ค ๋ก์ง์ด ๋ณต์กํ๊ณ , ๊ฐ์ฒด ๊ฐ ๊ด๊ณ๊ฐ ๋ง์ ๋๋ฉ์ธ.
- ์ฝ๋์ ์ฌ์ฌ์ฉ์ฑ๊ณผ ์์ง๋๊ฐ ์ค์ํ ๋๊ท๋ชจ ์ ํ๋ฆฌ์ผ์ด์ .
2. ์ ํฉํ์ง ์์ ๊ฒฝ์ฐ
- ๊ฐ๋จํ CRUD ์ ํ๋ฆฌ์ผ์ด์ .
- ๋น์ฆ๋์ค ๋ก์ง์ด ๋จ์ํ ๊ฒฝ์ฐ, ํธ๋์ญ์ ์คํฌ๋ฆฝํธ ํจํด์ด ๋ ์ ํฉํ ์ ์์ต๋๋ค.
๐ฅ ํธ๋์ญ์ ์คํฌ๋ฆฝํธ ํจํด ์์
ํธ๋์ญ์ ์คํฌ๋ฆฝํธ ํจํด์ ๋น์ฆ๋์ค ๋ก์ง์ ์ํฐํฐ๊ฐ ์๋ ์๋น์ค ๊ณ์ธต์์ ์ฒ๋ฆฌํ๋ ๋ฐฉ์์ ๋๋ค.
์ด ํจํด์ ๋จ์ํ ๋น์ฆ๋์ค ๋ก์ง๊ณผ CRUD ์์ ์ ์ ํฉํ๋ฉฐ, ๋๋ฉ์ธ ๋ชจ๋ธ ํจํด๋ณด๋ค ๊ตฌํ์ด ๊ฐ๋จํฉ๋๋ค.
1. ์ํฐํฐ ๊ฐ์ํ
์ํฐํฐ ํด๋์ค๋ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๊ณ ๊ฐ์ ธ์ค๋ ์ญํ ๋ง ์ํํ๋ฉฐ, ๋น์ฆ๋์ค ๋ก์ง์ด ์์ต๋๋ค.
@Entity
public class Order {
@Id
@GeneratedValue
private Long id;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List orderItems = new ArrayList<>();
@OneToOne(cascade = CascadeType.ALL)
private Delivery delivery;
private LocalDateTime orderDate;
@Enumerated(EnumType.STRING)
private OrderStatus status; // ์: ORDERED, CANCELED
// Getter, Setter๋ง ์กด์ฌ
public void addOrderItem(OrderItem orderItem) {
this.orderItems.add(orderItem);
orderItem.setOrder(this);
}
}
2. ์๋น์ค ๊ณ์ธต์์ ๋ชจ๋ ๋น์ฆ๋์ค ๋ก์ง ์ฒ๋ฆฌ
@Service
@Transactional
public class OrderService {
private final OrderRepository orderRepository;
private final MemberRepository memberRepository;
public OrderService(OrderRepository orderRepository, MemberRepository memberRepository) {
this.orderRepository = orderRepository;
this.memberRepository = memberRepository;
}
// ์ฃผ๋ฌธ ์์ฑ
public Long createOrder(Long memberId, Delivery delivery, List orderItems) {
// ํ์ ์กฐํ
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new IllegalArgumentException("ํ์์ด ์กด์ฌํ์ง ์์ต๋๋ค."));
// ์ฃผ๋ฌธ ์์ฑ
Order order = new Order();
order.setMember(member);
order.setDelivery(delivery);
for (OrderItem orderItem : orderItems) {
order.addOrderItem(orderItem);
}
order.setOrderDate(LocalDateTime.now());
order.setStatus(OrderStatus.ORDERED);
// ์ฃผ๋ฌธ ์ ์ฅ
orderRepository.save(order);
return order.getId();
}
// ์ฃผ๋ฌธ ์ทจ์
public void cancelOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new IllegalArgumentException("์ฃผ๋ฌธ์ด ์กด์ฌํ์ง ์์ต๋๋ค."));
if (order.getDelivery().getStatus() == DeliveryStatus.COMPLETE) {
throw new IllegalStateException("์ด๋ฏธ ๋ฐฐ์ก ์๋ฃ๋ ์ฃผ๋ฌธ์ ์ทจ์ํ ์ ์์ต๋๋ค.");
}
// ์ํ ๋ณ๊ฒฝ
order.setStatus(OrderStatus.CANCELED);
// ์ฃผ๋ฌธ์ํ ์ทจ์
for (OrderItem orderItem : order.getOrderItems()) {
orderItem.cancel(); // ์ฃผ๋ฌธ์ํ ์ทจ์ ๋ก์ง ํธ์ถ
}
}
// ์ด ์ฃผ๋ฌธ ๊ฐ๊ฒฉ ๊ณ์ฐ
public int getTotalPrice(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new IllegalArgumentException("์ฃผ๋ฌธ์ด ์กด์ฌํ์ง ์์ต๋๋ค."));
return order.getOrderItems().stream()
.mapToInt(OrderItem::getTotalPrice)
.sum();
}
}
ํธ๋์ญ์ ์คํฌ๋ฆฝํธ ํจํด์ ํน์ง
- ์ํฐํฐ๋ ๋จ์ํ ๋ฐ์ดํฐ์ ๊ด๊ณ๋ฅผ ํํํ๋ ์ญํ ๋ง ์ํ.
- ๋น์ฆ๋์ค ๋ก์ง์ ๋ชจ๋ ์๋น์ค ๊ณ์ธต์์ ์ฒ๋ฆฌ.
- ๋ฐ์ดํฐ ํ๋ฆ์ด ๊ฐ๋จํ ํ๋ก์ ํธ์์ ํจ๊ณผ์ .
๋๋ฉ์ธ ๋ชจ๋ธ ํจํด๊ณผ ๋น๊ต
ํน์ง | ๋๋ฉ์ธ ๋ชจ๋ธ ํจํด | ํธ๋์ญ์ ์คํฌ๋ฆฝํธ ํจํด |
---|---|---|
๋ก์ง ์์น | ์ํฐํฐ ๋ด๋ถ (๋๋ฉ์ธ ๊ฐ์ฒด) | ์๋น์ค ๊ณ์ธต |
๋น์ฆ๋์ค ๋ก์ง ๋ณต์ก๋ | ๋ณต์กํ ๋น์ฆ๋์ค ๋ก์ง์ ์ ํฉ | ๊ฐ๋จํ CRUD ๋ฐ ๋น์ฆ๋์ค ๋ก์ง์ ์ ํฉ |
์ ์ง๋ณด์ | ๊ฐ์ฒด์งํฅ์ ์ค๊ณ๋ก ์์ง๋๊ฐ ๋์ | ๊ตฌ์กฐ๊ฐ ๋จ์ํ์ง๋ง ๋น์ฆ๋์ค ๋ก์ง์ด ๋ถ์ฐ๋ ์ ์์ |
์ฌ์ฌ์ฉ์ฑ | ์ํฐํฐ ๋ก์ง์ ์ฌ์ฌ์ฉํ๊ธฐ ์ฉ์ด | ์๋น์ค ๊ณ์ธต์์ ๊ฐ์ ๋ก์ง์ด ๋ฐ๋ณต๋ ๊ฐ๋ฅ์ฑ ์์ |
์ค๊ณ ๋ฐ ๊ตฌํ | ์ด๊ธฐ ์ค๊ณ ๋น์ฉ์ด ํผ | ๊ตฌํ์ด ๊ฐ๋จํ๊ณ ๋น ๋ฆ |
๊ฒฐ๋ก
ํธ๋์ญ์ ์คํฌ๋ฆฝํธ ํจํด์ ๋จ์ํ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ํฉํ๋ฉฐ, ๋น์ฆ๋์ค ๋ก์ง์ด ๋ณต์กํ์ง ์์ ๋ ๋น ๋ฅด๊ณ ํจ์จ์ ์ธ ์ ํ์ ๋๋ค.
ํ์ง๋ง ๋ก์ง์ด ๋ณต์กํด์ง๊ฑฐ๋ ์ฌ์ฌ์ฉ์ฑ์ด ์ค์ํด์ง๋ค๋ฉด ๋๋ฉ์ธ ๋ชจ๋ธ ํจํด์ผ๋ก ์ ํ์ ๊ณ ๋ คํด์ผ ํฉ๋๋ค.
๋๊ธ