객체지향에서 제시하는 다양한 설계원칙과 개념이 있다.
실제 프로젝트나 현업에서 사용하고 고려할 수 있는 중요한 요소인 의존성에 대해 공부해보고자 한다.
이전부터 듣고싶었던 조영호님의 객체지향과 의존성에 대한 강의를 듣고 해당 내용을 정리해보고자 한다!
https://www.youtube.com/watch?v=dJ5C4qRqAgA&t=5724s
의존성이란?
의존성은 한마디로 변경에 초점을 두는 것이다. A가 B에 의존할 경우[A → B], B가 변경될 때 A도 같이 변경될 수 있다.
의존성 종류에는 클래스 의존성, 패키지 의존성이 있다.
클래스 의존성 종류
크게 4가지가 있다 다음과 같다.
- 연관관계
class A {
private B b;
}
- 의존관계
class A {
public B method(B b) {
return new B();
}
}
- 상속관계
class A extends B {
}
- 실체화관계
class A implements B {
}
패키지 의존성
패키지에 포함된 클래스 사이의 의존성이다.
어떻게 의존성을 피해야할까?
1. 양방향 의존성을 피하라
양방향이 아닌 단방향으로 바꾸자.
A를 변경하였을 때 B도 변경할 수 있고, 반대도 가능하며 버그 가능성이 크다.
2. 의존성이 필요없다면 제거하라
3. 패키지 사이의 의존성 사이클을 제거하라
예제로 살펴보자. 배달의민족 샘플 코드로 보자.
문제상황은 다음과 같다.
다음 플로우가 있다면 어떻게될까?
- 사용자가 주문할 메뉴를 장바구니에 담는다.
- 서버에 저장되는 것이 아닌 사용자에 저장되어있다.
- 사장이 메뉴 옵션을 변경한다.
→ 불일치가 발생한다. 검증을 해야한다.
검증과정을 다음과 같다.
- 가게가 영업중인지 비교
- 최소금액 비교
- 주문항목과 메뉴를 이름 비교
- 주문옵션그룹과 메뉴옵션그룹을 이름 비교
- 주문옵션과 메뉴옵션을 이름 비교
- 주문가격과 메뉴가격을 비교
검증과정에서 관계의 방향은 어떻게될까?
관계의 방향 = 협력의 방향 = 의존성의 방향
위와 같다. 결국 의존성이 꼬이게 될 수 있다!
현재 레이어 아키텍처는 다음과 같다.
의존성은 다음과 같다.
사이클이 돌고있다…! 개선해보자!
객체 참조로 구현한 연관관계의 문제점
협력을 위해 서로 필요하지만 두 객체 사이의 결합도가 높아진다.
class Order {
private List<OrderLineItem> orderLineItems;
public void place() {
validate();
ordered();
}
private void validate() {
for (OrderLineItem orderLineItem : orderLineItems) {
orderLineItem.validate();
}
}
}
고려할 점은 다음과 같다.
- 성능문제 (어디까지 조회할 것인가?)
- 수정 시 도메인 규칙을 함께 적용할 경계는?
- 트랜잭션 경계는 어디까지인가?
객체참조의 문제점
서로 연결되어 있어서, 어떤 객체라도 접근 가능할 것 같다.
→ 결합도가 가장 높은 의존성
즉, 연관관계를 끊자!
어떤 객치는 묶고 어떤 객체를 분리할 것인가?
- 함께 생성되고 삭제될 때 객체를 묶자
- Order과 OrderLineItem
- 도메인 제약사항을 공유하는 객체는 묶자
- 가능하면 분리하자
연관관계를 끊는 방법을 알아보자
1. Repository를 통한 탐색(약한 결합도)
Order에서 Shop으로 탐색이 가능하다.
경계 밖의 객체는 ID를 이용해 접근하자
1-1. 검증을 위한 Valiudator 구현
@Service
public class OrderService {
private OrderMapper orderMapper;
private OrderValidator orderValidator;
private OrderRepository orderRepository;
@Transactional
public voud placeOrder(Cart cart) {
Order order = orderMapper.mapFrom(Cart);
order.place(orderValidator);
orderRepository.save(order);
}
}
Order에서 검증
public class Order {
public void place(OrderValidator orderValidator) {
orderValidator.validate(this);
ordered();
}
}
2. 도메인 로직의 순차적 실행
위와 같다면, Order는 delivery와 shop을 호출한다.
해결방법: 절차지향 로직, 도메인 이벤트 퍼블리싱
하나씩 살펴보자.
2-1. 절차지향 로직을 통한 해결
- 비즈니스 플로우가 한번에 보인다.
- 하지만 의존성에 사이클이 발생할 수 있다.
2-2. 도메인 이벤트 퍼블리싱
Order가 Domain Event를 발행하도록 수정한다.
AbstractAggregateRoot<Order>를 상속하자.
- registerEvent로 발행할 수 있다. (commit시 이벤트가 발행된다)
- 지원플랫폼에도 사용되었다!
public class Order extends AbstractAggregateRoot<Order> {
public void delivered() {
this.orderStatus = OrderStatus.DELIVERED;
registerEvent(new OrderDeliveredEvent(this));
}
}
이벤트는 이벤트 리스너를 통해 받을 수 있다.
@Component
public class BillShopWithOrderDeliveredEventHandler {
@Async
@EventListener
@Transactional
public void handle(OrderDeliveredEvent event) {
Shop shop = shopRepository.findById(event.getShopId())
...
shop.billCommissionFee(event.getTotalPrice());
}
}
의존성이 꼬일 경우 다음을 진행하자
- 패키지를 분리하고
- 새로 만든 패키지에 의존을 옮기자
결론
패키지 의존성 사이클을 제거하는 3가지방법은 다음과 같다.
- 중간 객체 만들기 (좋은지 모르겠다. 더 공부해보자)
- 의존성 역전
- 새로운 패키지 추가
트레이드 오프이다. 잘 선택해서 사용하자.
의존성에 따라 시스템을 진화시키자!
'공부 > Spring' 카테고리의 다른 글
KAFKA 알아보기(By Spring) (0) | 2023.05.08 |
---|---|
HikariCP란 무엇이고 어떤 풀 사이즈를 적용해야 할까? (0) | 2022.10.20 |
QueryDSL에서 수정, 삭제 벌크연산 하는방법을 알아보자 (0) | 2022.08.26 |
[QueryDSL] 동적쿼리를 해결해보자 (0) | 2022.08.26 |
[QueryDSL] 프로젝션(SELECT 대상)에 따라 다른 결과를 가져와 보자 (0) | 2022.08.26 |
댓글