역할이나 책임을 어떻게 분리하느냐에 대한 이야기는 많이 하지만 실질적으로 이것은 의존성
을 어떻게 관리하느냐의 문제이다.
어떻게 의존성을 관리하는 것이 좋은가? 의존성을 어떻게 관리하느냐에 따라 코드가 어떻게 바뀌는가?
설계 : 코드를 어떻게 배치할 것인가에 대한 의사결정(클래스, 패키지, 프로젝트..)
어디에 어떤 코드를 넣어야 하는가?
핵심은 변경
에 초점을 두어야 한다. 같이 변경되는 코드는 같이 넣어야 하고 같이 변경되지 않는 코드는 따로 넣어야 한다.
변경의 핵심은 의존성이다.
의존성이라는 것은 A가 B에 의존할 경우 B가 변경될 때 A도 변경될 수 있는 가능성이 있다는 뜻이다. 결국 의존성이란 변경에 의해 영향을 받을 가능성을 말한다.
의존성에는 클래스 의존성, 패키지 의존성 등이 있다.
다른 패키지의 내용이 바뀌었을때 의존성이 있으면 영향을 받음, import가 있으면 dependency가 있다고 볼 수 있다.
양방향 : 원래 하나의 클래스라고 볼 수 있다, 특히 두 클래스의 Setter에 존재하면 늘 두 클래스가 동기화 되어야 한다.
Collection, List 등으로 참조 변수를 가질바에야 반대 방향으로 단뱡향 참조 변수를 갖도록 한다.
패키지에서 cycle이 돈다는 의미는 원래 하나의 패키지라는 뜻이다.
런타임시
런타임시
메뉴와 주문의 개념을 붙이면..
장바구니에 담긴 내용은 앱, 사용자 휴대폰 로컬에 담김
그런데,
장바구니에 사용자가 메뉴를 담은 사이에 업소 메뉴가 변경될 수 있다. 장바구니에 담긴 메뉴랑 실제로 파는 메뉴의 불일치가 발생한다.
그래서 실제로 파는 메뉴와 장바구니에 담은 메뉴가 같은지 검증하는 작업이 필요하다.
주문하기라고 하는 메시지가 전송, 이제부터 검증 작업이 시작된다.
가게쪽으로 메시지를 보냄
주문항목에서 메뉴항목으로 이름이 같은지 비교하는 메시지를 보냄
옵션그룹의 이름과 주문옵션그룹의 이름을 비교한다.
옵션그룹의 가격과 주문옵션그룹의 가격을 비교한다.
이게 다 통과를 하면 주문이 완료된다.
개발자들은 동적인 메모리상의 움직임을 정적인 코드로 바꾸어주어야 한다.
런타임시에 클래스의 인스턴스가 다른 클래스의 인스턴스가 어떤식으로 협력이 맺어지는가를 의사결정하고, 이를 코드상에 구현해주어야 한다.
객체는 관계의 방향성이 필요하다. 즉 의존성의 방향을 결정해주어야 한다.
연관관계, 의존관계, 상속관계, 실체화관계
협력이 빈번하게 일어날 때, 인스턴스 변수(객체 참조)
로 잡자!
협력이 일시적일 때, 파라미터, 리턴타입, 지역변수(내부에서 new())
로 잡자!
연관 관계를 구현하는 대표적인 방법 중 하나는 객체 참조를 이용한 것이다.
Order라는 객체에 place라는 메시지를 보낸다. place()는 주문의 상태를 검증(validate())하는 메소드 하나와 주문의 상태를 바꾸는(ordered()) 메소드로 구현되어 있다.
현재의 상태 : Order라는 객체가 place라고 하는 메시지를 받았다.
주문(Order)이 가게(Shop - 영업 중인지, 최소금액 이상인지 검증)와 주문항목(OrderLineItem - 이름, 갯수등이 올바른지 검증)으로 메시지를 보낼 수 있어야 한다.
이 관계는 매우 밀접하고 빈번하므로 연관 관계이다.
현재 상태 : 연관 관계 구현
shop에게 검증하라는 메시지를 보낸다. 영업 중인가? 최소 금액 이상인가?
현재 상태
OrderLineItem에게 각각의 항목이 매칭되는지 검증하라는 메시지를 보낸다. 이름과 갯수가 정확한가? 다시 Menu쪽으로 검증하라고 메시지를 보낸다. 이때 파라미터로 자기 자신을 넘긴다.
현재 상태
메뉴의 이름과 주문항목의 이름이 같은지 Menu에서 비교한다. 그리고 OptionGroup에게 옵션그룹의 이름과 주문옵션그룹의 이름이 같은지 검증하라고 메시지를 보낸다.
OptionGroup에서 옵션그룹의 이름과 주문옵션그룹의 이름을 비교한다. 이후에 Option에 검증하라고 메시지를 보낸다.
이후에 Option에 메시지를 보내서 옵션의 이름과 주문옵션 이름 비교, 옵션 가격과 주문옵션의 가격을 비교한다.
설계를 개선할 때 Dependency가 어떻게 되어 있나요?
자바에서는 패키지
로 레이어를 구현한다.
문제점 : 사이클이 돈다. (domain 패키지 내에 shop-order 사이)
Shop 패키지에 OptionGroup과 Option이라고 하는 중간 객체를 둔다. 이렇게 되면 Order -> Shop으로 양방향이 아닌 단방향으로 의존성이 흐르고 Dependency가 끊긴다.
의존 역전 원칙 : 클래스들이 구체적인 것이 의존하지 말고 추상화된 것에 의존하라.
추상화 된 것은 인터페이스나 추상클래스일 필요는 없다. 잘 변하지 않기만 하면 추상화 된 것이다. OptionGroup과 Option은 OptionGroupSpecification, OptionSpecification, OptionGroup, Option보다는 추상화된 레벨에서 구현되었다.
이렇게 바꿀 때의 장점은 재사용성이 증가
하는 것이다. 동일한 로직을 장바구니에 담을 때도 적용할 수 있고, 주문을 할 때에도 적용할 수 있다.
이슈 1.
Lazy Load 이슈
이슈 2.
객체를 어디서부터 어디까지 수정해야 하는가?
결국은 트랜잭션 경계도 모호해진다.
@Transaction 어노테이션으로 묶여있다.
그러나 이 세 개의 객체는 변경되는 주기가 다르다!
객체참조가 꼭 필요한 건가? 불필요한 건가? 언제 필요한 건가?
객체참조로 인한 문제의 해결방법
-> Repository를 통한 탐색(약한 결합법)
Repository는 연관 관계를 구현할 수 있는 오퍼레이션이 들어간 인터페이스여야 한다. 실제로는 비즈니스 로직 외에도 사용자에게 데이터를 보여주기 위한 조회로직이나 Admin 로직이 여기저기 들어가게 된다. 하지만 비즈니스 로직만 놓고 보면 연관 관계를 구현할 수 있는 오퍼레이션으로 제한을 두어야 한다.
그렇다면 모든 객체 참조가 불필요한가? 그렇지는 않다.
트랜잭션 내에서 같이 변경되는 것을 넣어야 한다. 즉, 비즈니스(도메인)와 관련이 있다. 같이 생성되고 같이 제거되는 것은 결합도가 높기 때문에 하나의 트랙잭션으로 묶어야 한다.
장바구니와 장바구니 안에 들어가는 항목은 생성 주기가 다르다. 그래서 일반적으로 장바구니와 장바구니 항목 객체를 일반적으로 분리한다. 그러나 일반적인 이커머스와 다르게 배달의 민족의 장바구니의 경우에는 한 업소의 품목만 장바구니에 넣을 수 있기 때문에 도메인 제약사항을 공유한다. 따라서 한 객체그룹으로 묶을 수 있다.
경계 안의 객체는 연관 관계를 통해 객체 참조를 통해 접근
경계 밖의 객체는 ID를 이용해 접근, ID를 가지고 있으면 Repository를 통해 접근이 가능하다.
어디서부터 어디까지는 한번에 읽어도 되는지, Lazy Loading을 해도 되는지 경계가 잡힙
테이블을 조인해서 가져오든, mongo DB를 써서 단문 처리를 해서 가져오든 한번에 가져올 수 있다.
shop -> shopId로 바뀜
menu -> menuId로 바뀜
컴파일 에러가 나는 것을 옮김
place() 할 때 OrderService에서 OrderValidator를 파라미터로 넘김
이것은 좋은 설계일까?
처음의 코드에는 여러 객체를 오가며 Validation 작업을 수행해야 했다.
한 군데에 모으면 한 곳에 볼 수 있다.
응집도는 같이 변경되는 것들이 모여있을 때 높다. Validation 주기와 주문 처리의 주기가 다르다. Valication 로직 코드는 Validation 처리가 바뀌었을 때 변경된다. 주문 처리 로직 코드는 주문 처리가 바뀌었을 때 변경된다. 변경에 따른 주기가 다른 코드가 섞여있다.
전체 flow를 한번에 보기에는 절차지향이 더 좋을 수가 있다. 이렇게 코드를 쪼개는 것이 결합도는 높이고 응집도는 낮출 수 있다.
OrderService가 delivery 객체를 호출하면 shop에 수수료를 부과한다. 하지만 이제 shop -> shopId로 바뀌면서 컴파일 에러가 발생한다.
어떤 객체가 바뀔 때 그 결과로 다른 객체가 바뀌는 전후 관계의 순서, 도메인 제약사항이 있다.
이렇게 바꾸었을 때 어떤 점이 좋을까?
원래는 Order와 Deliver, Shop으로 쪼개져 있던 비즈니스 Flow가 한번에 보인다.
OrderService가 OrderDeliveryService를 호출할 때 파라미터로 orderId를 넘긴다.
그런데, 의존성을 그려보니
Shop, Delivery, Order의 Repository를 모두 참조하면서 Dependy가 발생한다.
Order -> Delivery로 가는 의존성도 있고, Delivery -> Order로 가는 의존성도 있다.
인터페이스는 Order에 존재하고 실제로 이를 실행하는 Impl은 Delivery에 있다. 이렇게 되면 Delivery -> Order 의존성이 한 방향으로만 흐른다.
패키지 의존성이 존재할 때,
실무에서는 도메인 이벤트를 활발하게 사용한다.
위의 절차지향 로직 방법은 객체간의 결합도는 낮아지지만 로직간의 결합도는 높아졌다. 그러나 도메인 이벤트 퍼블리싱 방법은 A가 실행되면 B, C가 차례대로 실행되어야 하지만 최대한 이 순서는 느슨하게 만들었으면 좋겠다는 것이 동기이다.
Order가 발생하면 이벤트를 발행한다.
스프링에서 제공하는 AbstractAggregateRoot를 상속받으면 registerEvent()를 제공한다. 여기에 내가 원하는 이벤트를 register 하면 DB가 commit을 할 때 이 이벤트가 발행된다.
Order 쪽에서 상태가 바뀌었을 때 이벤트 핸들러에서 작업을 처리한다.
그렇다면 의존성을 그려보면?
Shop과 Order 사이에 사이클이 발생한다.
이벤트 핸들러(OrderDeliverdEventHandler - shop 패키지 내 존재)가 파라미터로 이벤트(OrderDeliverdEvent - order 패키지 내 존재)를 받고 있기 때문이다.
위에서 패키지 의존성이 존재할 때,
라고 했다.
3번째 방법이 있다.
패키지 간 사이클이 사라졌다.
서비스나 레이어 단위로 패키지 쪼갬
도메인 단위로 패키지를 분리하면(바깥으로 꺼내면) 의존성 사이클이 발생한다. 그래서 한 패키지에 넣으면 같은 레이어 안에서만 Dependency가 발생하므로 쉽게 Dependency 이슈를 없앨 수 있다.
이벤트를 활용하면 도메인 단위로 모듈화 할 수 있다.
참고