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

우아한테크세미나

Posted by Yan on December 10, 2021

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

  • 의존성을 어떻게 관리하는 것이 좋은가?
  • 의존성을 어떻게 관리하느냐에 따라 설계가 어떻게 변하는지 알아보기

설계가 뭔가요?

  • 어떤 프로젝트에, 어떤 패키지에, 어떤 클래스에 어떤 코드를 넣을 것인지 결정하는 것이다.
  • 핵심은 변경에 초점을 맞추는 것이다. 함께 변하는 코드를 같은 클래스에 넣는 것이다.
    • 예) B가 변경될 때 A도 함께 변경되는 가능성이 있다 = A는 B에 의존한다. A는 B의 변경에 의한 영향을 받는다.
  • 이것을 의존성이라고 부르는 것이다.

의존성

클래스 의존성의 종류

  1. 연관관계 Association
1
2
3
class A {
    private B b;
}
  1. 의존관계 Dependency
1
2
3
4
5
class A {
    public B method(B b) { return b;
    return new B();
    }
}
  1. 상속관계 Inheritance: 구현이 바뀌더라도 영향을 받을 수 있다.
1
2
3
class A extends B {

}
  1. 실체화관계 Realization: interface의 operation signature가 바뀌었을 때만 영향을 받을 수 있다.
1
2
3
class A implements B {

}

패키지 의존성

  • 패키지에 포함된 클래스 사이의 의존성
    • 예) package AA 클래스가 package BB에 의존하고 있다 = A depends on B

좋은 설계의 원칙

  1. 양방향 의존성을 피하라
  • 양방향 의존: class A가 바뀔 때 class B도 바뀐다. class B가 바뀔 때 class A도 바뀐다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A {
  private B b;

  public void setA (B b) {
    this.b = b;
    this.b.setA(this);
  }
}

class B {
  private A a;

  public void setA(A a){
    this.a = a;
  }
}
  • 단뱡향 의존을 선택하라.
1
2
3
4
5
6
7
8
9
10
11
class A {
    private B b;

    public void setA(B b) {
        this.b = b;
    }
}

class B {

}
  1. 다중성이 적은 방향을 선택하라.
  • 일대다 관계: AB를 Collection으로 갖고 있다
1
2
3
4
5
6
7
class A {
    private Collection<B> bs;
}

class B {

}
  • 다대일 관계: B가 A를 참조하고 있다
1
2
3
4
5
6
7
class A {

}

class B {
    private A a;
}
  1. 의존성이 없다면 제거하라.

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

  • package사이의 양방향 의존성이 있으면 안된다.
  • 설계에서 가장 중요한 것은 변경이다. 패키지간 의존성이 있다면 하나의 패키지였어야 할 것을 여러개로 나눈 꼴이 된다.

예제) 배달앱을 간추린 배달주문과정

주문 플로우

  • 가게선택 => 메뉴선택 => 장바구니 담기 => 주문완료

가게, 메뉴 Domain

Domain Concept

가게 - 메뉴 - 옵션그룹 - 옵션
https://t1.daumcdn.net/cfile/tistory/99F504455D0B8F901C

Domain Objects

런타임에서는 아래와 같이 작동한다.
https://t1.daumcdn.net/cfile/tistory/997DCB345D0B8F920B

주문 Domain

Domain Concept

주문 - 주문항목 - 주문옵션그룹 - 주문옵션 https://t1.daumcdn.net/cfile/tistory/99CDAD4E5D0B8F9124

Domain Objects

런타임에서는 아래와 같이 작동한다. https://t1.daumcdn.net/cfile/tistory/99187D355D0B8F9219

메뉴, 주문 Domain

Domain Objects

https://t1.daumcdn.net/cfile/tistory/99DC25505D0B8F9215

발생할 수 있는 문제

  • 사용자가 장바구니에 상품을 넣는다
    -> 가게에서 메뉴 내용을 변경한다
    -> 사용자가 주문하기를 눌렀을 때, 메뉴와 주문항목의 데이터가 불일치한다.

  • 해당 문제를 해결하기 위한 주문 validation

    • 가게가 영업중인지 확인
    • 주문 금액이 최소 주문 금액 이상인지 확인
    • 메뉴의 가격과 주문 항목의 가격 비교
    • 메뉴의 이름과 주문 항목의 이름 비교
    • 옵션 그룹의 이름과 주문 옵션 그룹의 이름 비교
    • 옵션의 이름과 가격을 주문 옵션의 이름과 가격 비교

협력 설계하기

https://3781094515-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MZhbHwSZ3XN9bJ6CE09%2Fuploads%2Fgit-blob-6463dc78880953171d578ed0226f5ef36f7446ff%2Fimage.png?alt=media

https://3781094515-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MZhbHwSZ3XN9bJ6CE09%2Fuploads%2Fgit-blob-266fc0907d2d5e19c0651270f76bf54b9d98af2b%2Fimage.png?alt=media

  • 의존관계: 협력을 위해 일시적으로 필요한 의존성 (파라미터, 리턴타입, 지연변수 등)
연관관계 = 탐색가능성
  • 연관관계: 협력을 위해 필요한 영구적인 탐색 구조 (배치 참조 등)

    • 한 객체에서 다른 객체로 빈번하게 가야하고, 이것을 아예 영구적으로 만들고 싶을 때 사용
  • 예) 주문Order가 뭔지 알면 원하는 주문항목OrderLineItem을 찾을 수 있다.

    • Order에서 OrderLineItem으로 탐색 가능
  • 객체 참조를 이용한 연관관계 구현

https://t1.daumcdn.net/cfile/tistory/997828445D0B8F9123

구현 예제
  1. 주문Order객체 만들기
  • 메세지를 받기 위해서 주문 객체를 만든다
  • 주문 객체는 place()라는 메세지를 받기로 한다.
  • 주문 객체는 검증과 주문을 수행한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Order {
    public void place() {
        validate();
        ordered();
    }

    private void validate() {

    }

    private void ordered() {

    }
}
  1. Shop & OrderLineItem 연관관계 연결
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@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() {

    }
}
  1. shop validation
Order
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Order {
    public void place() {
        validate();
        ordered();
    }

    private void validate() {
        if (orderLineItems.isEmpty()) {
            throw new IllegalStateException("주문 항목이 비어있습니다.");
        }

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

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

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

    private void ordered() {
        this.orderStatus = OrderStatus.ORDERED;
    }
}
Shop
1
2
3
4
5
6
7
8
9
10
11
12
public class Shop {

    // 가게가 영업중인지 확인
    public boolean isOpen() {
        return this.open;
    }

    // 주문 금액이 최소 주문 금액 이상인지 확인
    public boolean isValidOrderAmount(Money amount) {
        return amount.isGreaterThanOrEqual(minOrderAmount);
    }
}
OrderLineItem
1
2
3
4
5
6
public class OrderLineItem {

    public void validate() {
        menu.validateOrder(name, this.orderOptionGroups);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Menu {

    public void validateOrder(String MenuName, List<OrderOptionGroup> groups) {

        // 메뉴의 이름과 주문 항목의 이름 비교
        if (!this.name.equals(MenuName)) {
            throw new IllegalArgumentException("기본상품이 변경됐습니다.");
        }

        if (!isSatisfiedBy(groups)) {
            throw new IllegalArgumentException("메뉴가 변경됐습니다.");
        }
    }

    private boolean isSatisfiedBy(List<OrderOptionGroup> groups) {
        return cartOptionGroups.stream()
                            .anyMatch(this::isSatisfiedBy);
    }

    private boolean isSatisfiedBy(OrderOptionGroup group) {
        return optionGroupSpecs.stream()
                            .anyMatch(spec -> spec.isSatisfiedBy(group));
    }
}
OptionGroupSpecification
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class OptionGroupSpecification {

    // 옵션 그룹의 이름과 주문 옵션 그룹의 이름 비교
    public boolean isSatisfiedBy(OrderOptionGroup group) {
        return !isSatisfied(group.getName(), satisfied(group.getOptions()));
    }

    private boolean isSatisfied(String groupName, List<OrderOption> satisfied) {
        if (!name.equals(groupName) || satisfied.isEmpty() || (exclusive && satisfied.size() > 1)) {
            return false;
        }
        return ture;
    }

    private List<Option> satisfied(List<OrderOption> options) {
        return optionSpecs
                    .stream()
                    .flatMap(spec -> options.stream().filter(spec::isSatisfiedBy))
                    .collect(toList());
    }
}
OptionSpecification
1
2
3
4
5
6
7
8
public class OptionSpecification {

    // 옵션의 이름과 가격을 주문 옵션의 이름과 가격 비교
    public boolean isSatisfiedBy(OrderOption option) {
        return Objects.equals(name, option.getName())
            && Objects.equals(price, option.getPrice());
    }
}
전체 플로우

https://3781094515-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MZhbHwSZ3XN9bJ6CE09%2Fuploads%2Fgit-blob-5ed23439039990c6c450137af4e0bba36073d264%2Fimage.png?alt=media

레이어 아키텍처 중에서 Domain 영역에 해당

reference

[우아한테크세미나] 190620 우아한객체지향 by 우아한형제들 개발실장 조영호님
우아한 객체지향-2019년 6월 우아한 Tech 세미나 참석 후기
우아한 객체지향
생각하라, 객체지향처럼