객체 지향 설계

포모·2020년 12월 5일
1

JAVA의 기본

목록 보기
6/9

의존성에 따라서 프로그램의 설계 방법도 매우 달라집니다.
의존성이란 무엇일까요?

🚩 Dependency

설계란 무엇인지 물어보면 다양한 의견이 나옵니다.

  • 코드를 어떻게 배치할 것인지
  • 어떤 패키지에, 어떤 프로젝트에, 어떤 클래스에 어떤 코드를 넣을 것인지에 따라 설계 모양이 바뀝니다.

그렇다면 어디다가 어떤 코드를 넣어야할까요?? 🤔
👉 변경에 초점을 두어 코드를 넣는 것이 핵심입니다.


의존성이란 A가 B에 의존할 경우 다음과 같이 표시합니다.

의존성이 있다는 것은 B가 변경될 때 A도 같이 변경될 수 있다는 의미입니다.

클래스 사이의 의존성

  1. 연관 관계

    아예 A에서 B로 영구적으로 갈 수 있는 경로

  2. 의존 관계

    파라미터나 리턴 타입에 의존하는 타입이 나오는 경우(의존하는 타입의 인스턴스를 생성하는 경우 등)
    일시적으로 관계를 맺고 헤어질 수 있는 관계

  3. 상속

    B 클래스의 구현을 A가 상속받습니다.
    즉 B가 변경될 때 A도 같이 변경됩니다.

  4. 실체화 단계

    인터페이스를 implements하는 관계


패키지 사이의 의존성

패키지 B의 클래스가 바뀔 때 패키지 A에 있는 클래스가 영향을 받는 경우

  • 어떤 클래스를 열었을 때 다른 패키지의 이름이 있는 경우

🚩 의존성 관리의 법칙

1. 양방향 의존성을 피하라

  • A가 B로 가는 의존성과 B에서 A로 가는 의존성이 있습니다. : 사실상 A, B가 하나의 클래스나 다름 없는데 찢어놓은 격
  • 성능 이슈나 싱크를 맞추는 데 많은 버그가 발생합니다.
  • 최대한 단방향 의존성을 맞춰주어야합니다.

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

  • 위와 같이 A가 여러개의 B를 가지는 것보다는,
  • B가 단뱡향의 A 참조를 가지는 것이 낫습니다.

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

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

  • 패키지에서는 양방향 의존성이 있으면 안됩니다.
  • 패키지의 의존성을 따라갔을 때 사이클이 생기는 경우를 피해야합니다.

예제 살펴보기

주문 플로우

  1. 가게 선택
  2. 메뉴 선택
  3. 장바구니 담기
  4. 주문 완료

설계를 어떻게 했는지는 👉 [애플리케이션 아키텍처와 객체지향]


Domain Concept

1. 가게 & 메뉴


2. 주문

만약 왼쪽과 같이 주문했다면, 오른쪽의 그림과 같은 객체들이 생성될 것입니다.



📌 문제점

메뉴 선택의 문제점

  • 사용자는 메뉴 선택 후 장바구니에 담는다
  • 배달의 민족 앱은 장바구니 정보를 사용자 로컬에 저장한다.
    • 스마트폰이 바뀌면 장바구니 데이터 없어진다.
  • 만약 사용자가 메뉴를 장바구니에 담은 후 사장님이 메뉴를 바꾸면 문제가 발생한다.
  • 주문을 했을 때 실제 주문 데이터와 가게 데이터가 같은 지 검증해야한다.
  • 주문 Validation

  • Validation에 대한 협력 설계하기

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

  • 클래스 다이어그램

  • 관계의 방향 = 협력의 방향 = 의존성의 방향

    • 관계에는 방향성이 필요합니다.
    • 협력은 정적인 코드로 나타내야합니다.
    • 의존성은 소스와 타겟이 필요합니다.
      • 데이터베이스는 방향성이 없습니다.
      • 외래키를 설정해놓으면 양방향으로 움직입니다.
    • 런타임의 협력관계를 가지고 의존성의 방향을 잡으면 됩니다
    • 상속과 실체화는 매우 명확합니다.
    • 연관관계와 의존관계를 설정해야합니다.
      • 연관관계 : 빈번한 협력을 위해 필요한 영구적인 탐색 구조 (객체 참조)
      • 의존관계 : 협력을 위해 일시적으로 필요한 의존성 (파라미터, 리턴타입, 지연변수)
    • 관계의 종류보다는 방향성이 중요합니다.

연관관계 = 탐색 가능성


어떤 객체가 있을 때, 이 객체를 알면 내가 원하는 다른 객체를 찾아갈 수 있다.


구현하기

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

    private void validate() {
    }

    private void ordered() {
    }
}
  • 어떤 객체가 메시지를 받는 다는 것은 public 메서드로 구현된다는 것을 의미합니다.

    • 순서는 메시지를 결정하고 그에 맞는 메서드를 만듭니다.
    • 메서드가 필요한 이유는 '메시지를 받기 위해서' 입니다.
  • 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() {
    }
}
  • 주문(Order)은 가게(shop)과 주문 항목(orderLineItems)와 연관관계입니다.
    • OrdershoporderLineItems 쪽으로 메시지를 보낼 수 있어야합니다.
    • shop이 영업중인지 검증, 최소 주문 금액보다 많은지 검증
    • orderLineItems 주문항목들을 비교하여 검증
  • Ordershop, orderLineItems영구적인 관계라고 판단, 연관관계로 설정해주었습니다.

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) : 각각의 주문 항목과 실제 메뉴가 다른지 확인 (실제 메뉴가 주문 사이에 바뀌진 않았는지)

나머지 코드는 생략하겠습니다... (너무 많아요....😥)


전체적인 검증 플로우


  • 위의 유효성 검사 코드는 레이어드 아키텍처에서 Domain에 속합니다.
  • Domain을 구현하기 위해서는 DB에서 조회하는 등 다른 레이어 코드를 구현해야합니다.

설계 개선하기

  • 설계를 개선하기 위해서는 의존성을 봐야합니다.
  • 코드를 짤 때는 의존성을 그려보는 것이 좋습니다.
  • 설계를 어떻게 해야될지 모르겠을 때는 일단 코드를 짜보고, 의존성을 보면서 개선을 하다보면 원하는 구조대로 가는 경우가 많다고 합니다.

설계의 대표적인 두가지 문제를 보겠습니다.

지금까지의 의존성을 우선 살펴보겠습니다.

  • 문제점 : shoporder 사이의 사이클 발생

    • Order는 가게가 열려있는지, 최소주문금액을 만족하는 지에 대한 검증이 필요하기 때문에 shop과 연관관계로 의존하고 있습니다.
    • OptionGroupSpecificationOptionSpecificationOrderGroupOrderOption에서 데이터를 가져와야하는데 이때 사이클이 생성됩니다. (점선) 👉 양방향의존

  • 해결방법 1 중간 객체를 이용한 의존성 사이클 끊기

    • 위와 같이 구현하면 의존성이 한방향으로 흐릅니다.
    • 의존성 역전 원리 : 클래스들이 구체적인 것에 의존하지 않고, 추상화하는 원칙을 이용해 사이클을 끊는 것
    • 추상화라는 것은 잘 안 변하는 것을 의미합니다.
    • OptionGroupOption을 만들어 추상화한 것입니다.

객체 참조로 구현한 연관 관계의 문제점

협력을 위해 필요하지만 두 객체 사이의 결합도가 높아집니다.

  • 성능 문제 : 어디까지 조회할 것인가?
    • ORM을 쓰거나 DB를 쓰면 연관관계가 있는 순간부터 헬게이트가 열린다고 합니다...😂
    • ORM을 사용하는 경우는 Lazy 로딩 이슈
    • 객체 그룹의 조회 경계가 모호 : 쿼리가 굉장히 복잡
  • 수정 시 도메인 규칙을 함께 적용할 경계는 어디까지인가?
    • ex. Order의 상태 변경 시 연관된 도메인 규칙을 함께 적용해야하는 객체 범위는?
    • 이 문제는 트랜잭션 경계는 어디까지인가? 까지 이어집니다. ("어떤 테이블에서 어떤 테이블까지 하나의 단위로 잠금(Lock)을 설정할 것인가?")
    • A 객체에 대한 객체 참조가 없는 문제가 생길 때 A 객체를 인스턴스 변수로 넣어버리는 방법으로 쉽게 해결하려고 하지만, 이는 트랜잭션을 더 길어지게 만듭니다.

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


    • 그러나 이 3개의 객체가 변경되는 주기가 다릅니다.
    • 긴 트랜잭션으로 묶여있는 것은 새로운 것이 추가될수록 트랜잭션 주기가 달라집니다.
      • Long 트랜잭션 안의 lock이 걸린 것들도 문제가 발생할 수 있습니다.
      • 트랜잭션 경합으로 인한 성능이 저하됩니다.

객체 참조가 꼭 필요할까?

  • 객체 참조의 문제점은 '모든 것을 연결시켜버린다'는 것이다.
  • 객체 참조는 어떤 객체라도 접근이 가능하고, 함께 수정이 가능합니다.
  • 객체 참조는 결합도가 가장 높은 의존성입니다.
  • 필요한 경우 객체참조를 끊어야합니다.

위와 같이 Ordershop 객체를 참조하여 탐색하는 경우 강한 결합도를 가지게 됩니다.

이는 Repository를 이용하여 결합도를 약하게 만들 수 있습니다.

  • Repository는 파라미터로 받은 타입으로 이 객체를 찾을 수 있다는 오퍼레이션을 기본적으로 가지고 있어야합니다.
  • 하지만 조회 로직으로 인해 양방향 관계가 생겨날 수 있습니다.

어떤 객체를 묶고, 분리할 것인가?

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

  • 각 객체들의 라이프 사이클을 생각해야합니다.
    • 주문과 배달의 라이프 사이클을 완전히 다르므로 독립적으로 묶어야합니다.
  • 경계 안의 객체들을 연관관계(객체 참조)로 묶는 것이 맞습니다. (같이 읽어야하기 때문에)
  • 경계 밖의 객체들을 ID를 이용하여 접근합니다.

  • 그룹을 지정했으면 객체 참조를 통한 연관관계를 모두 제거해야하고,

  • ID로만 노출을 해주는 것이 좋습니다.

  • 그래서 Repository를 통해 탐색합니다.


  • 객체를 분리한 단위가 트랜잭션 단위가 되고 조회 단위가 됩니다.

    • 같은 경계 안에 있는 객체끼리 한번에 조회가 되고 lazy loading을 할 수 있습니다.
    • 객체 분리 단위가 lazy와 Eager의 경계를 구분할 수도 있습니다.

  • 참조가 없는 객체 그룹으로 나누고 나면, 그룹 단위의 영속성 저장소 변경 가능합니다.



첫번째 컴파일 에러

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

👉 해결방법 : 객체를 직접 참조하는 로직을 다른 객채로 옮깁니다.

  • validator Logic을 모읍니다.
    • compile 에러가 나는 부분을 전부 validator logic에 넣어줍니다.
  • 기존 로직은 유효성 로직을 찾아야하지만, validator logic을 만들어 주면 한눈에 확인할 수 있습니다.

  • 기존 코드는 validation 로직과 주문 처리 로직이 같이 있고 이는 변경되는 코드가 아닙니다. (낮은 응집도)
    • 변경 주기가 다른 코드가 한 곳에 있다는 것을 의미합니다.
  • validator logic을 따로 생성해주고 나면 주문 처리 로직만 남아 높은 응집도의 객체(단일-책임 원칙)만이 남습니다.

  • 때로는 절차 지향이 객체 지향보다 좋습니다.
    • 절차 지향 : OrderValidator
    • 객체의 결합도는 높아지지만 응집도가 낮아지는 경우가 있습니다.

두번째 컴파일 에러

  • 배달이 완료되면 shop에 수수료를 추가합니다. 이 객체 또한 id로 바뀌면서 컴파일 에러가 발생합니다.

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

👉 첫번째 해결방법 절차 지향 로직(validator logic)

  • 비즈니스 플로우가 한 눈에 보입니다.
  • 의존성이 잘 되어 있는지 그려보았을 때 의존성 사이클에 주의해야합니다. 👉 인터페이스를 이용해 의존성을 역전시켜줍니다. (의존성 역전 원리)

👉 두번째 해결방법 도메인 이벤트 퍼블리싱

  • 이벤트를 만들어 발행하고 이벤트 핸들러를 받아 처리합니다.


위에서는 Spring에서 제공하는 클래스를 상속받아서 이벤트를 만들었습니다. (이벤트는 직접 만드는 것이 좋습니다.)
이벤트 핸들러는 스프링에서 제공하는 것을 사용하는 게 좋습니다.


이벤트와 이벤트 핸들러를 넣고 의존성을 그려보았을 때,

이번에도 의존성 사이클이 발생했습니다.
원인은 이벤트 핸들러가 shop 패키지에 있기 때문이므로, 이벤트 핸들러가 의존하는 코드를 shop에서 분리합니다.
이벤트 핸들러에서 shopbilling을 사용합니다.


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

1. 새로운 객체로 변환

중간 객체를 만들어줍니다.


2. 의존성 역전

인터페이스나 추상 클래스를 통해 의존성을 끊어주는 방식


3. 새로운 패키지 추가


의존성과 시스템 분리

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

의존성이 관리가 안되어 있으면 도메인 단위 분리시 의존성 사이클이 존재합니다.
의존성을 관리하지 않으면 레이어별로 패키지를 나누고 그에 맞는 도메인 로직을 담으면 됩니다.

  • 패키지 별로 의존성 관리를 해주면 되므로 간단합니다.

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

🛴 마무리

난이도가 너무 높아 이해가 확실하게 되지는 않지만, 이미 시작해버려서 끝까지 정리해보았습니다..
아직 저도 완벽히 이해하지못해 글이 많이 횡설수설합니다.
나중에 더 실력을 쌓아 깔끔하게 정리해보겠습니다.


참고

0개의 댓글