본문 바로가기
공부/Spring

객체지향과 의존성

by JERO__ 2022. 11. 1.

객체지향에서 제시하는 다양한 설계원칙과 개념이 있다.

실제 프로젝트나 현업에서 사용하고 고려할 수 있는 중요한 요소인 의존성에 대해 공부해보고자 한다.

이전부터 듣고싶었던 조영호님의 객체지향과 의존성에 대한 강의를 듣고 해당 내용을 정리해보고자 한다!

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. 패키지 사이의 의존성 사이클을 제거하라

 

예제로 살펴보자. 배달의민족 샘플 코드로 보자.

문제상황은 다음과 같다.

다음 플로우가 있다면 어떻게될까?

  1. 사용자가 주문할 메뉴를 장바구니에 담는다.
    • 서버에 저장되는 것이 아닌 사용자에 저장되어있다.
  2. 사장이 메뉴 옵션을 변경한다.

→ 불일치가 발생한다. 검증을 해야한다.

 

검증과정을 다음과 같다.

  1. 가게가 영업중인지 비교
  2. 최소금액 비교
  3. 주문항목과 메뉴를 이름 비교
  4. 주문옵션그룹과 메뉴옵션그룹을 이름 비교
  5. 주문옵션과 메뉴옵션을 이름 비교
  6. 주문가격과 메뉴가격을 비교

 

검증과정에서 관계의 방향은 어떻게될까?

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

위와 같다. 결국 의존성이 꼬이게 될 수 있다!

현재 레이어 아키텍처는 다음과 같다.

의존성은 다음과 같다.

사이클이 돌고있다…! 개선해보자!

 

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

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

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가지방법은 다음과 같다.

  • 중간 객체 만들기 (좋은지 모르겠다. 더 공부해보자)
  • 의존성 역전
  • 새로운 패키지 추가

트레이드 오프이다. 잘 선택해서 사용하자.

의존성에 따라 시스템을 진화시키자!

댓글