[우아한테크세미나] 우아한객체지향 By 우아한형제들 개발실장 조영호님

SungBum Park·2020년 1월 13일
15

세미나 정리

목록 보기
3/3
post-thumbnail

의존성을 이용해 설계 진화시키기

개요

  • 설계의 핵심은 의존성 이다.
  • 좋은 설계는 의존성을 어떻게 하는지에 달려있다.
  • 의존성에 따라 설계가 바뀐다.

의존성(Dependency)

  • 설계란?
    • 코드를 어떻게 배치할 것인가에 대한 의사결정
    • 어떤 클래스에 어떤 코드? 어떤 패키지에 어떤 코드? 어떤 프로젝트에 어떤 코드?
      • 그에 따라 설계가 바뀜
    • 변경에 초점을 맞춘다.
      • 같이 변경되는 코드를 같이 모아둔다.
      • 변경에 대한 것은 의존성이다.

  • 의존성은 점선으로 나타낸다.
  • B가 변경될 때 A도 변경될 수 있다.
  • 의존성은 변경과 관련있다.
    • 무조건 변경되는 것은 아니다.
  • 의존성은 변경에 의해서 영향받을 가능성을 말한다.

클래스 의존성의 종류

1. 연관관계

  • A에서 B로 이동할 수 있다.
  • 연관관계는 영구적인 관계를 맺는다.
  • 구현방법 중 하나는 객체 참조가 있다.(위 코드)

2. 의존관계

  • 파라미터, 리턴 타입에 타입이 나오거나 매서드 안에 타입을 생성한다면 의존관계가 성립된다.
  • 의존관계는 일시적으로 관계를 맺는다.

3. 상속관계

  • B가 바뀔 때 A가 바뀐다.
  • 구현이 변경되면 영향을 받는다.

4. 실체화 관계(인터페이스)

  • 시그니처가 바뀌면 영향을 받는다.

패키지 의존성

  • 간단히 import에 다른 객체가 존재하면 의존성을 가진다고 생각할 수 있다.

설계 가이드

양방향 의존성을 피하라.

  • A가 바뀔 때 B가 바뀌고 A가 또 바뀐다.
    • 이는 한 클래스를 억지로 분리한 것으로 생각할 수 있다.
  • 성능 이슈 및 여러 문제점이 발생한다.

다중성이 적은 방향을 선택하라.

  • 컬렉션을 인스턴스 변수로 가지지 않도록 한다.
  • 성능 이슈 등 다양한 이슈가 생긴다.

의존성이 필요없다면 제거하라.

패키지 사이의 의존성 사이클을 제거하라.

  • 양방향이면 같이 바뀌므로 하나의 패키지로 만드는 것이 낫다.
  • 패키지 3개가 싸이클이라는 것은 하나의 패키지라고 보는 것이 맞다.

예제 살펴보기

  • 배달앱 예제
    • 실제 배달의민족 코드와 다름
    • 재구성하고 매우 단순화함

주문플로우

  1. 가게 선택
  • 가게 상세
  1. 메뉴 선택
  • 1개 담기
  1. 장바구니담기
  2. 주문완료

Domain Concept - 가게 & 메뉴

  • 영업여부: 준비중인지 확인
  • 최소주문금액: 얼마이상담아야 주문가능
  • 1인 세트가 메뉴
  • 한 메뉴에 여러 옵션이 있음
  • 옵션이 그룹으로 묶여있음
  • Specification은 뒤에 따로 설명함

Domain Object - 가게 & 메뉴

  • 왼쪽 그림을 예제로 객체의 상태로 표현

Domain Concept - 주문

  • 메뉴를 선택이 끝나면 주문이 발생한다.

Domain Object - 주문

  • 옆에 있는 그림을 실제 주문하면 오른쪽 상태가 됨

Domain Object - 메뉴 & 주문

메뉴 선택 문제점

  • 사용자는 메뉴 선택 후 장바구니에 담는다.
  • 배달의민족 앱은 장바구니 정보를 사용자 로컬에 저장한다.(위 예제도 동일)
    • 서버에 저장하는 것이 아닌 사용자 로컬에 저장함
    • 스마트폰이 바뀌면 장바구니 데이터가 없어진다.
  • 만약 사용자가 1인세트를 장바구니에 담은 후, 사장님이 메뉴를 바꾸면 문제가 발생한다.

  • 예를 들어 1인세트를 0.5인세트로 바꾸면 사용자 장바구니의 메뉴와 불일치가 발생한다.
  • 주문을 했을 때, 실제로 주문 데이터와 사장님 데이터가 같은지 검증해야한다.

주문 Validation

  • 위와 같은 유효성 검사가 필요하다.

Validation에 대한 협력 설계하기

  1. 주문하기 메시지가 전송됨
  2. 가게가 영업중인지, 최소주문금액보다 큰지 확인
  3. 메뉴 이름과 주문항목 비교
  4. 옵션그룹 이름과 주문옵션그룹 이름과 비교
  5. 옵션의 이름과 가격이 같은지 비교

클래스 다이어그램

  • 위 그림은 협력을 클래스 다이어그램으로 나타낸 것이다.
  • 코드는 위 구조로 구현되어있다.
  • 개발이 어려운 점은 런타임에 동적인 구조를 정적인 코드로 담는것이다.
    • 변경되는 많은 것들을 정적인 것으로 만들어줘야한다.
    • 정적인 무언가를 찾아내야함(관계)

  • 관계에는 방향성이 필요하다.
  • 협력을 정적인 코드로 나타내야한다.
  • 의존성은 소스와 타겟이 필요하다.
    • 데이터베이스는 방향성이 없다.
    • Foreign Key를 설정해놓으면 양방향으로 움직임
  • 코드는 방향성을 결정해야한다.
  • 관계의 방향 = 협력의 방향 = 의존성의 방향
  • 런타임의 협력 방향에 따라 설계해야 한다.
  • 상속과 실체화는 매우 명확하다.
  • 연관관계와 의존관계를 설정해야한다.

  • 연관관계는 영구적
    • 객체참조로 구현 가능
    • 데이터의 흐름을 따라갈 수 밖에 없다.
    • OrderShop으로 빈번하게 협력한다면 연관관계로 하는 것이 좋다.
    • 어떤 객체가 어떤 객체로 빈번하게 가야한다면 연관관계
  • 연관관계에는 이유가 필요하다.

  • 의존관계는 일시적
  • 관계의 종류보다는 방향성이 중요하다.

연관관계 = 탐색가능성

  • 연관관계는 어떤 객체가 있을 때, 이 객체를 알면 내가 원하는 다른 객체를 찾아갈수 있다.
  • 연관관계의 협력

  • 일반적으로 연관관계는 객체참조로 구현한다.
    • 물론 다른 방법도 있음
  • 연관관계 개념과 객체참조 구현을 구분해야 한다.
    • 개념과 이를 구현할 수 있는 방법을 구분해야한다.
    • 일대일로 매칭하는 오류를 자주 범한다.
    • 연관관계를 구현하는 방법 중 객체참조가 있다.

구현 시작하기

public class Order {
    public void place() {
        validate();
        ordered();
    }

    private void validate() {
    }

    private void ordered() {
    }
}
  • 어떤 객체가 어떤 메시지를 받는다는 것은 public 메서드로 구현된다는 것이다.
    • 순서는 메시지를 결정하고 그에 맞는 메서드를 만드는 것이다.
  • place()
    • validate(): 주문이 올바른지
    • ordered(): 주문을 변경하는
@Entity
@Table(name="ORDERS")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="ORDER_ID")
    private Long id;

    @ManyToOne
    @JoinColumn(name="SHOP_ID")
    private Shop shop;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name="ORDER_ID")
    private List<OrderLineItem> orderLineItems = new ArrayList<>();

    //...

    public void place() {
        validate();
        ordered();
    }

    private void validate() {
    }

    private void ordered() {
    }
}
  • 주문은 가게와 주문 항목과 연관관계
    • 주문은 항상 가게의 주문이고, 항목이 있으므로 영구적으로 연결되어 있어야 한다고 판단함
private void validate() {
    if (orderLineItems.isEmpty()) {
        throw new IllegalStateException("주문 항목이 비어 있습니다.");
    }

    // 1)
    if (!shop.isOpen()) {
        throw new IllegalArgumentException("가게가 영업중이 아닙니다.");
    }

    // 2)
    if (!shop.isValidOrderAmount(calculateTotalPrice())) {
        throw new IllegalStateException(String.format("최소 주문 금액 %s 이상을 주문해주세요.", shop.getMinOrderAmount()));
    }

    // 3)
    for (OrderLineItem orderLineItem : orderLineItems) {
        orderLineItem.validate();
    }
}

private void ordered() {
    this.orderStatus = OrderStatus.ORDERED;
}
public class OrderLineItem {
  // 3)
  public void validate() {
    menu.validateOrder(name, convertToOptionGroups());
  }
}
  • 1) 가게가 영업중인지 확인
  • 2) 최소주문금액보다 큰 지 확인
  • 3) 주문한 메뉴와 가게 메뉴가 일치하는지 확인
    • Order -> OrderLineItem -> Menu

  • 자세한 코드는 위 전체 코드 링크 참조

  • 위 유효성 검사 코드는 레이어드 아키텍처에서 도메인 레이어에 속한다.
  • 도메인을 구현하려면 데이터베이스에서 조회하는 등 다른 레이어 코드를 구현해야한다.
    • 현재 예제 코드는 서비스와 인프라 레이어까지 구현되어 있음

  • 문제가 발생하면 예외를 발생하고 종료시킴
  • 정상적이면 데이터베이스에 저장함

설계 개선하기

  • 설계를 개선하는 방법을 물어볼 때 클래스 하나만 보여주는 경우가 많다.
    • 이 때는 말해줄 수 있는게 적다.
    • 객체의 협력을 보여주어야한다.
  • 설계를 개선하려면 의존성을 봐야한다.
    • 조영호님은 반드시 의존성을 그려본다고 한다.
    • 찝찝한 부분이 보인다.
    • 초반에는 절차적으로 짤 수도 있다.(일단 구현해보는 것도 중요하다.)
    • 구현한 후에 설계를 개선하는 것도 방법이다.

  • 여기서는 위 두 가지 문제만 본다.

의존성 살펴보기

  • 레이어는 개념이다.
    • 자바에서는 레이어를 패키지로 구현한다.

  • 문제는 서비스레이어에서 ShopOrder 사이에서 사이클이 만들어진다.
    • Order는 가게가 열려있는지와 최소주문금액 유효성을 검사해야하므로 가게와 깊게 의존하고 있다.
    • 따라서 객체 참조로 구현하였다.
    • OptionGroupSpecificationOptionSpecificationOrderGroupOrderOption에서 데이터를 가져와야하는데, 이 때 사이클이 생성된다.
  • 양방향인 경우 shop 패키지를 변경하면 order 패키지도 변경해야한다.
    • 양쪽의 패키지를 같이 변경되야한다.
    • 이는 잘못된 거임

  • 양방향으로 의존하는 것을 보여준다.
  • 지금부터 양방향으로 패키지가 의존하는 3가지 케이스가 나온다.
    • 이 3가지의 해결방법이 모두 다르다.

첫 번째 해결방법 - 중간 객체를 이용한 의존성 사이클 끊기

  • 위와 같이 구현하면 의존성이 한쪽 방향으로 흐른다.
  • 이는 조금 이상해 보일 수 있다.
    • 의존성 역전 원리를 이용한 것임.
  • 추상화라고 하면 추상 클래스나 인터페이스를 떠올린다.
    • 개발쪽에서 추상화는 잘 안변하는 것이다.
    • OptionGroupOption은 추상화를 한 것이다.
    • DIP의 변형으로 볼 수 있다.
  • 이를 통한 장점은 추후에 장바구니에서 같은 유효성 검사를 위의 두 객체를 재사용할 수 있다.
    • 장바구니뿐아니라 주문과 다른 로직에서도 사용할 수 있다.
  • 의존성을 보며 개선할 부분이 있지 않을까 생각할 수 있는 것이 중요하다.

객체 참조의 문제점

성능 문제 - 어디까지 조회할 것인가?

  • 단순히 메모리를 사용할 때는 문제가 되지 않는다.
  • ORM을 사용하는 순간 큰 문제점이 생긴다.
    • 대표적으로 Lazy 로딩문제가 있다.
    • Open Session In View도 연관관계 문제이다.

  • 객체 그룹의 조회 경계가 모호하다.
  • 모든 객체가 연결되어 있는 것이 근본적인 문제이다.

수정 시 도메인 규칙을 함께 적용할 경계는?

  • Order의 상태를 변경할 때 연관된 도메인 규칙을 함께 적용해야하는 객체의 범위는?
  • 위 문제는 트랜잭션의 경계는 어디까지인가? 문제로 이어진다.
    • 어떤 테이블에서 어떤 테이블까지 하나의 단위로 잠금할 것인가?
  • A 객체에 대한 객체참조가 없는 문제가 생길 때 A 객체를 인스턴스 변수로 넣어버리는 방법으로 쉽게 해결하려고 한다.
    • 이는 트랜잭션이 점점 더 길어진다.

결제 완료

public class OrderService {
  @Transactional
   public void payOrder(Long orderId) {
       Order order = orderRepository.findById(orderId).orElseThrow(IllegalArgumentException::new);
       // 1)
       order.payed();

       // 2)
       Delivery delivery = Delivery.started(order);
       deliveryRepository.save(delivery);
   }
}
public class Order {
    public enum OrderStatus { ORDERED, PAYED, DELIVERED }

    @Enumerated(EnumType.STRING)
    @Column(name="STATUS")
    private OrderStatus orderStatus;

    // 1)
    public void payed() {
        this.orderStatus = OrderStatus.PAYED;
    }
}
@Entity
@Table(name="DELIVERIES")
public class Delivery {
    enum DeliveryStatus { DELIVERING, DELIVERED }

    @OneToOne
    @JoinColumn(name="ORDER_ID")
    private Order order;

    @Enumerated(EnumType.STRING)
    @Column(name="STATUS")
    private DeliveryStatus deliveryStatus;

    // 2)
    public static Delivery started(Order order) {
        return new Delivery(order, DeliveryStatus.DELIVERING);
    }
}
  • 1) 주문이 완료되면 결제를 위해 Order의 상태를 결제중으로 변경한다.
  • 2) 배송을 위해 Delivery 객체를 만들고 배송을 시작한다.

배달 완료

public class OrderService {
  @Transactional
    public void deliverOrder(Long orderId) {
        Order order = orderRepository.findById(orderId).orElseThrow(IllegalArgumentException::new);
        // 1)
        order.delivered();

        Delivery delivery = deliveryRepository.findById(orderId).orElseThrow(IllegalArgumentException::new);
        // 3)
        delivery.complete();
    }
}
public class Order {
    public enum OrderStatus { ORDERED, PAYED, DELIVERED }
    // 1)
    public void delivered() {
        this.orderStatus = OrderStatus.DELIVERED;
        // 2)
        this.shop.billCommissionFee(calculateTotalPrice());
    }
}
@Entity
@Table(name="SHOPS")
public class Shop {
    @Column(name="COMMISSION_RATE")
    private Ratio commissionRate;

    @Column(name = "COMMISSION")
    private Money commission = Money.ZERO;

    // 2)
    public void billCommissionFee(Money price) {
        commission = commission.plus(commissionRate.of(price));
    }
}
public class Delivery {
  // 3)
  public void complete() {
       this.deliveryStatus = DeliveryStatus.DELIVERED;
   }
}
  • 1) Order 상태를 배달 완료로 변경한다.
  • 2) 배달 수수료 추가를 Shop 객체에 요청한다.
  • 3) Delivery 상태를 배달 완료로 변경한다.

트랜잭션 범위

  • 배달 완료의 트랜잭션 범위이다.

  • 이 트랜잭션의 문제점은 객체 3개 모두 변경의 빈도가 다르다.
  • Long 트랜잭션으로 묶여있는 것은 새로운 것이 추가될 수록 트랜잭션 주기가 달라진다.
    • Long 트랜잭션 안의 lock이 걸린 것들로 큰 문제가 발생할 수 있다.
    • 성능이 저하된다.

객체참조가 꼭 필요한가?

  • 객체참조의 문제점은 모든 것을 연결시킨다.
    • 어느 객체라도 접근 가능하다고 생각한다.
  • 객체참조는 결합도가 가장 높은 의존성
  • 필요한 경우 객체참조는 모두 끊어버려야한다.

객체참조를 통한 탐색(강한 결합도)

Repository를 통한 탐색(약한 결합도)

  • 가장 흔히 해결하는 방법 중 하나이다.
  • Repository라는 인터페이스를 일반적으로 중구난방으로 만든다.
    • Repository는 파라미터로 받은 타입으로 이 객체를 찾을 수 있다는 오퍼레이션을 기본적으로 가지고 있어야한다.
    • 하지만 조회가 섞이면서 이가 깨진다.
    • 사용자에게 보여줘야할 정보가 많아지면서 복잡해진다.
  • 조회 로직때문에 양방향관계가 늘어난다.

어떤 객체들을 묶고 어떤 객체들을 분리할 것인가?

  • 간단한 규칙
    • 함께 생성되고 삭제되는 객체끼리 묶는다.
    • 도메인 제약사항을 공유하는 객체끼리 묶는다.
      • 객체를 묶는 기준은 서비스마다 다르다.
    • 가능하면 분리한다.
  • 트랜잭션 안에 있는 것은 같이 변경되는 것들이 있어야 한다.

  • 각 객체들의 라이프사이클을 생각해야한다.
    • 주문과 배달의 라이프사이클은 완전히 다르므로 독립적으로 묶어야 한다.

  • 경계 안의 객체는 참조를 이용해 접근한다.
    • FetchType, Cascade 옵션을 준다.

  • 경계 밖의 객체는 ID를 이용해 접근한다.
  • 객체참조는 객체지향을 설명하는데 많이 사용된다. 간단하고 객체지향을 잘 나타낼 수 있기 때문이다.
  • 현업으로 가면 객체참조는 최대한 자제하고 객체를 어떻게 분리하고 합치는지가 중요하다.
  • 개념과 현업의 괴리가 있다.

  • 객체를 분리한 단위가 트랜잭션 단위가 되고 조회 단위가 된다.
    • 같은 경계안에 있는 객체끼리는 한 번에 조회가 되고 Lazy 로딩을 할 수 있다.
    • 객체 분리 단위가 Lazy와 Eager의 경계를 구분할 수도 있다.

컴파일에러 1

  • 경계밖의 객체를 id로 참조하므로 기존 코드에서 직접 객체를 사용한 부분에서 컴파일 에러가 발생한다.

해결방법1 - 객체를 직접 참조하는 로직을 다른 객체로 옮긴다.

@Component
public class OrderValidator {
    public OrderValidator(ShopRepository shopRepository,
                          MenuRepository menuRepository) {
        this.shopRepository = shopRepository;
        this.menuRepository = menuRepository;
    }

    public void validate(Order order) {
        validate(order, getShop(order), getMenus(order));
    }

    void validate(Order order, Shop shop, Map<Long, Menu> menus) {
        if (!shop.isOpen()) {
            throw new IllegalArgumentException("가게가 영업중이 아닙니다.");
        }

        if (order.getOrderLineItems().isEmpty()) {
            throw new IllegalStateException("주문 항목이 비어 있습니다.");
        }

        if (!shop.isValidOrderAmount(order.calculateTotalPrice())) {
            throw new IllegalStateException(String.format("최소 주문 금액 %s 이상을 주문해주세요.", shop.getMinOrderAmount()));
        }

        for (OrderLineItem item : order.getOrderLineItems()) {
            validateOrderLineItem(item, menus.get(item.getMenuId()));
        }
    }

    private void validateOrderLineItem(OrderLineItem item, Menu menu) {
        if (!menu.getName().equals(item.getName())) {
            throw new IllegalArgumentException("기본 상품이 변경됐습니다.");
        }

        for(OrderOptionGroup group : item.getGroups()) {
            validateOrderOptionGroup(group, menu);
        }
    }

    private void validateOrderOptionGroup(OrderOptionGroup group, Menu menu) {
        for(OptionGroupSpecification spec : menu.getOptionGroupSpecs()) {
            if (spec.isSatisfiedBy(group.convertToOptionGroup())) {
                return;
            }
        }

        throw new IllegalArgumentException("메뉴가 변경됐습니다.");
    }

    // ...
}
@Service
public class OrderService {
    private OrderRepository orderRepository;
    private OrderValidator orderValidator;
    private OrderMapper orderMapper;

    @Transactional
    public void placeOrder(Cart cart) {
        Order order = orderMapper.mapFrom(cart);
        order.place(orderValidator);
        orderRepository.save(order);
    }
}
public class Order {
  public void place(OrderValidator orderValidator) {
        orderValidator.validate(this);
        ordered();
    }
}
  • 조영호님은 이가 괜찮은 설계라고 생각한다고 함.
    • 기존 로직은 유효성 로직을 찾으러 가야하지만 변경된 위 코드는 한 번에 유효성 검사 로직을 볼 수 있다.

  • 응집도가 높은 코드는 변경되는 코드가 같이 있는 것이다.
  • 기존의 코드는 validation 로직과 주문 처리 로직이 같이 있고 이는 같이 변경되는 코드가 아니다.(응집도가 낮은 코드)
    • 변경 주기가 다른 코드가 한 곳에 있다.
  • 때로는 절차지향이 객체지향보다 좋을 때가 있다.
    • 절차지향: OrderValidator()
    • 객체 결합도는 높아질 수 있지만 응집도가 낮아지는 경우가 있다.

컴파일 에러 2

  • 위는 컴파일 에러 1과는 다른 경우이다.

  • 위 로직은 A의 상태 변경에 따라 B의 상태가 변하는 순차적인 로직으로 구성되어 있다.
  • 이렇게 도메인 제약사항으로 도메인 로직이 순차적으로 진행되는 경우가 있다.
  • 도메인이 순차적으로 진행될 때 어떻게 끊어야 할까?

해결방법 1 - 절차지향 로직(OrderValidator와 동일)

public class OrderService {
    private OrderDeliveredService orderDeliveredService;

    @Transactional
    public void deliverOrder(Long orderId) {
        orderDeliveredService.deliverOrder(orderId);
    }
}
@Component
public class OrderDeliveredService {
    public void deliverOrder(Long orderId) {
        Order order = orderRepository.findById(orderId).orElseThrow(IllegalArgumentException::new);
        Shop shop = shopRepository.findById(order.getShopId()).orElseThrow(IllegalArgumentException::new);
        Delivery delivery = deliveryRepository.findById(orderId).orElseThrow(IllegalArgumentException::new);

        order.delivered();
        shop.billCommissionFee(order.calculateTotalPrice());
        delivery.complete();
    }
}
  • 간단하게 해결할 수 있는 장점이 있다.
  • 기존의 코드는 클래스 2개로 분리되어 있어 비지니스 플로우가 한 눈에 보이지 않는다.

  • 설계를 개선한 후에 다시 의존성을 그려보면서 정말로 잘된 설계인지 확인해야한다.
  • 설계 이후 또 다시 의존성 사이클이 생성되었다.

인터페이스를 이용해서 의존성을 역전시킨다.

  • DIP를 적용한 모습이다.
  • 패키지 의존성 싸이클 해결방법
    • 추상적인 중간 객체를 만들어 변환
    • DIP

해결방법 2 - 도메인 이벤트(Domain Event) 퍼블리싱

  • 전체 예제 코드
  • 우아한형제들에서는 도메인 이벤트를 매우 자주 사용한다.(모듈이 잘게 나눠져 있기 때문)
  • 1번 방법과 달리 잘게 나누는 방법이다.(결합을 느슨하게 만드는 방법)
  • 이벤트를 만들어 발행하고 이벤트 핸들러에서 받아 처리한다.
  • 이벤트를 직접 만드는 것이 좋지만, 예제는 간단히 하기 위해 스프링에서 제공하는 추상 클래스를 사용함
    • 스프링에서 제공하는 것은 많이 사용하지 않는 방법임
  • 이벤트 핸들러는 스프링에서 제공하는 것을 사용하면 된다.

  • 여기서도 의존성 사이클이 생긴다.
  • Billing이라는 패키지로 분리한다.
    • 주문과 정산은 서로 다른 도메인 로직으로 볼 수 있다.

  • 패키지를 분리할 때는 도메인에 대한 관점이 바뀔 때가 많다
    • 의존성 사이클을 제거하다가 보면 도메인이 새로 만들어져야할 때가 빈번히 있고, 이 때 새로운 패키지로 분리해낸다.

(정리) 패키지 의존성 사이클을 제거하는 3가지 방법

1. 새로운 객체로 변환

2. 의존성 역전

  • 의존성을 추상 클래스나 인터페이스로 역전시킨다.

3. 새로운 패키지 추가

  • 위 세 가지 방법의 선택은 상황에 따라 다르다.

의존성과 시스템 분리

  • 의존성을 관리하다보면 시스템을 쉽게 분리할 수 있다.

  • Domain Event를 사용하기 전에는 레이어별로 도메인을 나누었다.
  • 도메인 단위로 분리하면 의존성에 문제가 생긴다.
    • 모든 도메인을 다 돌 수도 있다.
  • 의존성을 관리하지 않으면 레이어별로 패키지를 나누고 그에 맞는 도메인로직을 담으면 된다.
    • 패키지별로 의존성 관리를 해주면 되므로 간단하다.

  • 의존성을 관리하면 패키지를 분리해낼 수 있다.
    • 도메인 이벤트 퍼블리싱을 적용한 코드가 이와 같은 구조로 되어있다.(코드 링크)
  • 각각은 도메인 이벤트로 협력을 한다.

  • Order, Delivery, Billing이 비즈니스 로직으로는 한 번에 돌아가지만 분리해낼 수 있다.
    • 도메인 이벤트(내부), 인터널 이벤트, 시스템 이벤트(외부) 등 용어가 있다.
  • 시스템끼리 비동기적인 메시지를 가지고 통신하는 경우가 많다.
    • 특히, 배달의민족이 이렇게 많이 되어 있음
    • 동기적으로 하면 시스템이 한 번에 다 터질 수 있어서 위험하다.

결론

  • 시스템을 볼 때는 의존성을 봐라.
  • 의존성에 따라 시스템을 진화시켜라!
profile
https://parker1609.github.io/ 블로그 이전

1개의 댓글

comment-user-thumbnail
2021년 1월 1일

잘 읽었습니다 감사합니당 : )

답글 달기