의존성에 따라서 프로그램의 설계 방법도 매우 달라집니다.
의존성이란 무엇일까요?
설계란 무엇인지 물어보면 다양한 의견이 나옵니다.
그렇다면 어디다가 어떤 코드를 넣어야할까요?? 🤔
👉 변경에 초점을 두어 코드를 넣는 것이 핵심입니다.
의존성이란 A가 B에 의존할 경우
다음과 같이 표시합니다.
의존성이 있다는 것은 B가 변경될 때 A도 같이 변경될 수 있다
는 의미입니다.
연관 관계
아예 A에서 B로 영구적으로 갈 수 있는 경로
의존 관계
파라미터나 리턴 타입에 의존하는 타입이 나오는 경우(의존하는 타입의 인스턴스를 생성하는 경우 등)
일시적으로 관계를 맺고 헤어질 수 있는 관계
상속
B 클래스의 구현을 A가 상속받습니다.
즉 B가 변경될 때 A도 같이 변경됩니다.
실체화 단계
인터페이스를 implements
하는 관계
패키지 B의 클래스가 바뀔 때 패키지 A에 있는 클래스가 영향을 받는 경우
설계를 어떻게 했는지는 👉 [애플리케이션 아키텍처와 객체지향]
만약 왼쪽과 같이 주문했다면, 오른쪽의 그림과 같은 객체들이 생성될 것입니다.
메뉴 선택의 문제점
- 사용자는 메뉴 선택 후 장바구니에 담는다
- 배달의 민족 앱은 장바구니 정보를 사용자 로컬에 저장한다.
- 스마트폰이 바뀌면 장바구니 데이터 없어진다.
- 만약 사용자가 메뉴를 장바구니에 담은 후 사장님이 메뉴를 바꾸면 문제가 발생한다.
- 주문을 했을 때 실제 주문 데이터와 가게 데이터가 같은 지 검증해야한다.
클래스 다이어그램
관계의 방향 = 협력의 방향 = 의존성의 방향
어떤 객체가 있을 때, 이 객체를 알면 내가 원하는 다른 객체를 찾아갈 수 있다.
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
)와 연관관계입니다.Order
가 shop
과 orderLineItems
쪽으로 메시지를 보낼 수 있어야합니다.shop
이 영업중인지 검증, 최소 주문 금액보다 많은지 검증orderLineItems
주문항목들을 비교하여 검증Order
와 shop
, 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에서 조회하는 등 다른 레이어 코드를 구현해야합니다.설계의 대표적인 두가지 문제를 보겠습니다.
지금까지의 의존성을 우선 살펴보겠습니다.
문제점 : shop
과 order
사이의 사이클 발생
Order
는 가게가 열려있는지, 최소주문금액을 만족하는 지에 대한 검증이 필요하기 때문에 shop
과 연관관계로 의존하고 있습니다.OptionGroupSpecification
과 OptionSpecification
은 OrderGroup
과 OrderOption
에서 데이터를 가져와야하는데 이때 사이클이 생성됩니다. (점선) 👉 양방향의존
해결방법 1
중간 객체를 이용한 의존성 사이클 끊기
OptionGroup
과 Option
을 만들어 추상화한 것입니다.협력을 위해 필요하지만 두 객체 사이의 결합도가 높아집니다.
Order
의 상태 변경 시 연관된 도메인 규칙을 함께 적용해야하는 객체 범위는?배달 완료의 트랜잭션 범위
위와 같이 Order
에 shop
객체를 참조하여 탐색하는 경우 강한 결합도를 가지게 됩니다.
이는 Repository
를 이용하여 결합도를 약하게 만들 수 있습니다.
Repository
는 파라미터로 받은 타입으로 이 객체를 찾을 수 있다는 오퍼레이션을 기본적으로 가지고 있어야합니다.그룹을 지정했으면 객체 참조를 통한 연관관계를 모두 제거해야하고,
ID로만 노출을 해주는 것이 좋습니다.
그래서 Repository
를 통해 탐색합니다.
객체를 분리한 단위가 트랜잭션 단위가 되고 조회 단위가 됩니다.
참조가 없는 객체 그룹으로 나누고 나면, 그룹 단위의 영속성 저장소 변경 가능합니다.
경계 밖의 객체를 id로 참조하므로 기존 코드에서 직접 객체를 사용한 부분에서 컴파일 에러가 발생합니다.
👉 해결방법 : 객체를 직접 참조하는 로직을 다른 객채로 옮깁니다.
OrderValidator
shop
에 수수료를 추가합니다. 이 객체 또한 id로 바뀌면서 컴파일 에러가 발생합니다.👉 첫번째 해결방법 절차 지향 로직(validator logic)
👉 두번째 해결방법 도메인 이벤트 퍼블리싱
위에서는 Spring에서 제공하는 클래스를 상속받아서 이벤트를 만들었습니다. (이벤트는 직접 만드는 것이 좋습니다.)
이벤트 핸들러는 스프링에서 제공하는 것을 사용하는 게 좋습니다.
이벤트와 이벤트 핸들러를 넣고 의존성을 그려보았을 때,
이번에도 의존성 사이클이 발생했습니다.
원인은 이벤트 핸들러가 shop 패키지에 있기 때문이므로, 이벤트 핸들러가 의존하는 코드를 shop
에서 분리합니다.
이벤트 핸들러에서 shop
과 billing
을 사용합니다.
중간 객체를 만들어줍니다.
인터페이스나 추상 클래스를 통해 의존성을 끊어주는 방식
의존성을 관리하다보면 시스템을 쉽게 분리할 수 있습니다.
의존성이 관리가 안되어 있으면 도메인 단위 분리시 의존성 사이클이 존재합니다.
의존성을 관리하지 않으면 레이어별로 패키지를 나누고 그에 맞는 도메인 로직을 담으면 됩니다.
난이도가 너무 높아 이해가 확실하게 되지는 않지만, 이미 시작해버려서 끝까지 정리해보았습니다..
아직 저도 완벽히 이해하지못해 글이 많이 횡설수설합니다.
나중에 더 실력을 쌓아 깔끔하게 정리해보겠습니다.