spring

도메인 λͺ¨λΈ νŒ¨ν„΄ vs νŠΈλžœμž­μ…˜ 슀크립트 νŒ¨ν„΄

devJK93 2024. 11. 25.

πŸ”₯ 도메인 λͺ¨λΈ νŒ¨ν„΄μ΄λž€?

도메인 λͺ¨λΈ νŒ¨ν„΄(Domain Model Pattern)은 μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ 도메인 객체(μ—”ν‹°ν‹°) 내뢀에 ν¬ν•¨μ‹œν‚€λŠ” 섀계 νŒ¨ν„΄μž…λ‹ˆλ‹€. 즉, λΉ„μ¦ˆλ‹ˆμŠ€ κ·œμΉ™, 데이터 λ³€κ²½, 데이터 검증 등을 도메인 객체가 직접 μˆ˜ν–‰ν•˜λ„λ‘ μ„€κ³„ν•©λ‹ˆλ‹€.

이 νŒ¨ν„΄μ€ DDD(Domain-Driven Design)의 핡심 κ°œλ… 쀑 ν•˜λ‚˜μ΄λ©°, 객체지ν–₯의 μž₯점을 μ΅œλŒ€ν•œ ν™œμš©ν•©λ‹ˆλ‹€.

νŠΉμ§•

  1. 도메인 객체에 μ±…μž„ λΆ€μ—¬:객체가 μžμ‹ μ˜ μƒνƒœλ₯Ό κ΄€λ¦¬ν•˜κ³ , κ΄€λ ¨λœ λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ μˆ˜ν–‰ν•©λ‹ˆλ‹€.
  2. 예: Order 객체가 μ£Όλ¬Έ 생성, μ£Όλ¬Έ μ·¨μ†Œ 등을 μˆ˜ν–‰.
  3. λΉ„μ¦ˆλ‹ˆμŠ€ 둜직의 μΊ‘μŠν™”:데이터와 이λ₯Ό μ²˜λ¦¬ν•˜λŠ” 둜직이 ν•œ 객체 내뢀에 μΊ‘μŠν™”λ©λ‹ˆλ‹€.
  4. μ—”ν‹°ν‹° 쀑심 섀계:μ—”ν‹°ν‹° 객체가 λ‹¨μˆœνžˆ 데이터λ₯Ό μ €μž₯ν•˜λŠ” μ—­ν• λ§Œ ν•˜μ§€ μ•Šκ³ , λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ λ‹΄κ³  μžˆμŠ΅λ‹ˆλ‹€.

도메인 λͺ¨λΈ νŒ¨ν„΄μ˜ ꡬ쑰

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 및 λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ— 적합
μœ μ§€λ³΄μˆ˜ 객체지ν–₯적 μ„€κ³„λ‘œ 응집도가 λ†’μŒ ꡬ쑰가 λ‹¨μˆœν•˜μ§€λ§Œ λΉ„μ¦ˆλ‹ˆμŠ€ 둜직이 뢄산될 수 있음
μž¬μ‚¬μš©μ„± μ—”ν‹°ν‹° λ‘œμ§μ„ μž¬μ‚¬μš©ν•˜κΈ° 용이 μ„œλΉ„μŠ€ κ³„μΈ΅μ—μ„œ 같은 둜직이 반볡될 κ°€λŠ₯μ„± 있음
섀계 및 κ΅¬ν˜„ 초기 섀계 λΉ„μš©μ΄ 큼 κ΅¬ν˜„μ΄ κ°„λ‹¨ν•˜κ³  빠름

κ²°λ‘ 

νŠΈλžœμž­μ…˜ 슀크립트 νŒ¨ν„΄μ€ λ‹¨μˆœν•œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ— μ ν•©ν•˜λ©°, λΉ„μ¦ˆλ‹ˆμŠ€ 둜직이 λ³΅μž‘ν•˜μ§€ μ•Šμ„ λ•Œ λΉ λ₯΄κ³  효율적인 μ„ νƒμž…λ‹ˆλ‹€.

ν•˜μ§€λ§Œ 둜직이 λ³΅μž‘ν•΄μ§€κ±°λ‚˜ μž¬μ‚¬μš©μ„±μ΄ μ€‘μš”ν•΄μ§„λ‹€λ©΄ 도메인 λͺ¨λΈ νŒ¨ν„΄μœΌλ‘œ μ „ν™˜μ„ κ³ λ €ν•΄μ•Ό ν•©λ‹ˆλ‹€.

λŒ“κΈ€