의존성을 이용해 설계 진화시키기
- 의존성을 어떻게 관리하는 것이 좋은가?
- 의존성을 어떻게 관리하느냐에 따라 설계가 어떻게 변하는지 알아보기
설계가 뭔가요?
- 어떤 프로젝트에, 어떤 패키지에, 어떤 클래스에 어떤 코드를 넣을 것인지 결정하는 것이다.
- 핵심은 변경에 초점을 맞추는 것이다. 함께 변하는 코드를 같은 클래스에 넣는 것이다.
- 예) B가 변경될 때 A도 함께 변경되는 가능성이 있다 = A는 B에 의존한다. A는 B의 변경에 의한 영향을 받는다.
- 이것을
의존성
이라고 부르는 것이다.
의존성
클래스 의존성의 종류
- 연관관계
Association
1
2
3
| class A {
private B b;
}
|
- 의존관계
Dependency
1
2
3
4
5
| class A {
public B method(B b) { return b;
return new B();
}
}
|
- 상속관계
Inheritance
: 구현이 바뀌더라도 영향을 받을 수 있다.
1
2
3
| class A extends B {
}
|
- 실체화관계
Realization
: interface의 operation signature가 바뀌었을 때만 영향을 받을 수 있다.
1
2
3
| class A implements B {
}
|
패키지 의존성
- 패키지에 포함된 클래스 사이의 의존성
- 예)
package A
의 A
클래스가 package B
의 B
에 의존하고 있다 = A
depends on B
좋은 설계의 원칙
- 양방향 의존성을 피하라
- 양방향 의존:
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 {
}
|
- 다중성이 적은 방향을 선택하라.
- 일대다 관계:
A
가 B
를 Collection으로 갖고 있다
1
2
3
4
5
6
7
| class A {
private Collection<B> bs;
}
class B {
}
|
1
2
3
4
5
6
7
| class A {
}
class B {
private A a;
}
|
-
의존성이 없다면 제거하라.
-
패키지 사이의 의존성 사이클을 제거하라.
- package사이의 양방향 의존성이 있으면 안된다.
- 설계에서 가장 중요한 것은 변경이다. 패키지간 의존성이 있다면 하나의 패키지였어야 할 것을 여러개로 나눈 꼴이 된다.
예제) 배달앱을 간추린 배달주문과정
주문 플로우
- 가게선택 => 메뉴선택 => 장바구니 담기 => 주문완료
가게, 메뉴 Domain
Domain Concept
가게 - 메뉴 - 옵션그룹 - 옵션
Domain Objects
런타임에서는 아래와 같이 작동한다.
주문 Domain
Domain Concept
주문 - 주문항목 - 주문옵션그룹 - 주문옵션
Domain Objects
런타임에서는 아래와 같이 작동한다.
메뉴, 주문 Domain
Domain Objects
발생할 수 있는 문제
-
사용자가 장바구니에 상품을 넣는다
-> 가게에서 메뉴 내용을 변경한다
-> 사용자가 주문하기를 눌렀을 때, 메뉴와 주문항목의 데이터가 불일치한다.
-
해당 문제를 해결하기 위한 주문 validation
- 가게가 영업중인지 확인
- 주문 금액이 최소 주문 금액 이상인지 확인
- 메뉴의 가격과 주문 항목의 가격 비교
- 메뉴의 이름과 주문 항목의 이름 비교
- 옵션 그룹의 이름과 주문 옵션 그룹의 이름 비교
- 옵션의 이름과 가격을 주문 옵션의 이름과 가격 비교
협력 설계하기
- 의존관계: 협력을 위해 일시적으로 필요한 의존성 (파라미터, 리턴타입, 지연변수 등)
연관관계 = 탐색가능성
구현 예제
- 주문
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() {
}
}
|
- 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() {
}
}
|
- 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());
}
}
|
전체 플로우
레이어 아키텍처 중에서 Domain 영역에 해당
reference
[우아한테크세미나] 190620 우아한객체지향 by 우아한형제들 개발실장 조영호님
우아한 객체지향-2019년 6월 우아한 Tech 세미나 참석 후기
우아한 객체지향
생각하라, 객체지향처럼