똑같은 삽질은 2번 하지 말자

Domain Driven Design 도메인 모델 패턴 본문

Spring/Spring Boot

Domain Driven Design 도메인 모델 패턴

곽빵 2020. 3. 7. 15:58

음..오늘 강의를 들으면서 강사님이 주로 즐겨 쓰시는 패턴에 대해 설명을 받았다.

domain클래스에 다 떄려박는 느낌?의 코딩방식으로 유지보수할때, 꽤 용이하게 쓰여진다고 한다.

domain클래스에 이미 다 정의되어(단, protected로)있으면, 각자 원하는 방식으로 바꾸는건 안되니깐

밑의 코드는 예시이다.

package jpabook.jpashop.domain;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "orders")
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {
    @Id
    @GeneratedValue
    @Column(name = "order_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id") // foreign key 이름이 member_id가 된다.
    private Member member;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<>();

    @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "delivery_id")
    private Delivery delivery;

    private LocalDateTime orderDate; // 주문시간

    @Enumerated(EnumType.STRING)
    private OrderStatus status; // 주문 상태 order cancel

    // == 연관관계 메서드 == //
    public void setMember(Member member){
        this.member = member;
        member.getOrders().add(this);
    }
    public void addOrderItem(OrderItem orderItem){
        orderItems.add(orderItem);
        orderItem.setOrder(this);
    }
    public void setDelivery(Delivery delivery){
        this.delivery = delivery;
        delivery.setOrder(this);
    }
    // Setting Methods
    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.setStatus(OrderStatus.ORDER);
        order.setOrderDate(LocalDateTime.now());
        return order;
    }
    // Business Logic
    public void cancel(){
        if(delivery.getStaus() == DeliveryStatus.COMP){
            throw new IllegalStateException("Yon can't cancel products that have already been shipped");
        }
        this.setStatus(OrderStatus.CANCEL);
        for(OrderItem orderItem : this.orderItems){
            orderItem.cancel();
        }
    }
    // Check Logic
    public int getTotalPrice(){
        int totalPrice = 0;
        for(OrderItem orderItem : orderItems){
            totalPrice += orderItem.getTotalPrice();
        }
        return totalPrice;
//        return orderItems.stream().mapToInt(OrderItem::getTotalPrice).sum();
    }

}

그럼 본격적으로 DDD란 무엇인가? 

도메인

도메인 : 문제 해결하고자 하는 문제 영역. 도메인은 여러 하위 도메인으로 구성한다.

SW는 도메인의 모든 기능을 제공하지 않는다.(예를 들어 결제는 외부 시스템(PG)을 사용하는 것처럼)

 

도메인 모델

도메인 모델 : 특정 도메인을 개념적으로 표현. 도메인 자체를 이해하기 위한 개념 모델.

기능과 데이터를 함께 보여주는 객체 모델은 도메인을 모델링하기 적합함. 하지만 도메인 모델은 객체로만 모델링하지 않는다. 즉, 클래스/상태 다이어그램과 같은 UML(Unified Modeling Language) 표기법만 사용해야 하는건 아님. 관계가 중요한 도메인이라면 그래프를 이용해서 도메인을 모델링. 계산 규칙이 중요하다면 수학 공식을 활용해서 도메인 모델. 도메인을 이해하는데 도움이 된다면 표현 방식은 중요하지 않다.

도메인 모델은 개념 모델을 이용해서 바로 코드를 작성하지 않는다. 구현 기술에 맞는 구현 모델 필요. 개념모델이 구현모델과 같지 않지만 구현모델이 개념 모델을 최대한 따르도록 할 수 있다.

  • 객체 기반 모델을 이용해서 도메인 표현 = 객체 지향 언어를 사용하여 개념 모델 구현
  • 수학적인 모델을 사용 = 함수를 이용해서 도메인 모델 구현

하위 도메인이 다루는 영역은 서로 다르기 때문에 같은 용어라도 하위 도메인 마다 의미가 달라짐

모델의 각 구성요소는 특정 도메인을 한정할 때 비로소 의미가 완전해지기 때문에 하위 도메인마다 별도로 모델을 만들어야한다.

개념 모델 : 순수하게 문제를 분석한 결과물. DB, 트랜젝션, 성능을 고려X. 따라서 개념 모델은 구현 가능한 형태의 모델로 전환하는 과정을 거침. 완벽한 도메인을 표현하는 모델을 만드는 것은 불가능하므로 전반적인 개요를 알 수 있는 수준으로 개념 모델을 작성

 

도메인 모델 패턴

도메인 모델은 결국 아키텍처상의 도메인 계층을 객체 지향 기법으로 구현하는 패턴

도메인 계층은 도메인의 핵심 규칙을 구현. 예를 들어 주문 도메인의 경우 '출고 전에 배송지를 변경할 수 있다'는 규칙을 구현한 코드가 도메인 계층에 위치함. 이런 도메인 규칙을 객체 지향 기법으로 구현하는 패턴이 도메인 모델 패턴

 

도메인 모델 도출

도메인 모델링 기본 작업 : 모델을 구성하는 핵심 구성요소, 규칙, 기능을 찾는 것(요구사항에서 출발)

요구사항에서 메서드, 데이터 등을 정의하고 찾아내는 것.

 

엔티티와 밸류(Entity & Value)

모델은 크게 엔티티와 밸류로 구분

 

엔티티

엔티티란 서로 구별되는 하나하나의 대상. 주로 개체를 의미함.

엔티티는 객체마다 고유한 다른 식별자를 갖는다.

 

엔티티의 식별자 생성

식별자는 다음 중 한가지 방식으로 생성

  • 특정 규칙에 따라 생성 - 주문번호, 카드번호
  • UUID 사용 - 개발 언어에서 지원하는 UUID 생성기 사용
  • 값을 직접 입력 - 회원 아이디, 이메일
  • 일련번호 사용 - 시퀀스나 DB의 자동 증가 칼럼 사용(auto increment)

 

밸류타입

밸류타입은 코드의 의미를 더 잘 이해할 수 있도록한다.

  • 받는 사람 이름(String), 받는 사람 폰번호(String) → 받는 사람(Receiver Class)
    (Receiver Class는 받는 사람이라는 도메인 개념을 표현)
  • 주소1(String), 주소2(String), 우편번호(String) → 주소(Address Class)
    (Address Class는 주소라는 도메인 개념을 표현)

 

엔티티 식별자와 밸류 타입

대부분 엔티티 식별자의 실제 데이터는 String과 같은 문자열로 구성됨.

Money(Class)가 단순 숫자가 아닌 도메인의 '돈'을 의미. 식별자는 단순 문자열이 아니라 도메인에서 특별한 의미를 지니는 경우가 많음. 식별자를 위한 밸류타입을 사용해서 의미가 잘 드러나도록 할 수 있다.

주문(Order)의 식별자 타입으로 String 대신 OrderNo 밸류 타입을 사용하면 타입을 통해서 주문 번호라는 것을 나타낼 수 있음.

 

도메인 모델에 set 메서드 넣지 않기

get/set 메서드는 습관적으로 추가하는 메서드. set 메서드를 구현해야할 특별한 이유가 없다면 불변 타입의 장점을 살릴 수 있도록 밸류 타입은 불변으로 구현

set 메서드의 2가지 문제

  • 도메인의 핵심 개념이나 의도를 코드에서 사라지게 한다.

changeShippingInfo() → setShippingInfo()
배송지 정보를 새로 변경한다 → 단순 배송지 값을 설정

  • 도메인 객체를 생성할 때 완전한 상태가 아닐 수 있다.

생성자를 통한 초기화가 아닌 set 메서드를 통해 초기화할 경우 값이나 검사 코드가 누락될 수 있다.
도메인 객체가 불완전한 상태로 사용되는 것을 막으려면 생성 시점에 필요한 것을 전달해 주어야 한다.

 

 

Comments