이번에는 유튜브에서 조영호님이 발표하신 강의 우아한 객체지향을 정리해보려고 합니다.
객체지향과 해당 예제에 관심이 많아서..ㅎㅎ
영상 링크와 발표 깃허브 링크는 포스팅 맨 아래에 있습니다.
1. 의존성
1.1 설계란
- 코드를 어떻게 배치할 것인가. 즉 어떤 클래스에 어떤 코드가 들어가고, 어떤 패키지에 어떤 클래스가 들어갈 것인지, 프로젝트에 어떤 코드를 작성할 것인가에 대한 내용이 설계다.
- 설계의 핵심은 바로 변경에 초점을 맞추는 것이다.
- 같이 변경되는 것들을 같이 넣어야 한다. 같이 변경되지 않는 것들은 따로 넣어야 한다.
1.2 의존성이란
의존성이 있다는 것은 B가 변경되면 A도 같이 변경될 수 있다는 의미다. 중요한 부분은 변경될 수 있다는 점이다.
위 그림의 예시처럼 A가 B에 의존하고 B가 변경되어도 설계를 잘 했다면 A가 변경되지 않을 수 있다.
의존성의 종류는 크게 2가지가 있다.
- 클래스 의존성
- 패키지 의존성
1.3 클래스 의존성
클래스 의존성에는 크게 4개의 종류가 있다.
연관 관계
A에서 B로 이동이 가능하다. 제일 쉽게 생각할 수 있는 연관 관계는 참조 변수를 가지고 있는 것이다.
연관 관계는 영구적으로 이동이 가능하며, 협력을 위해 필요한 영구적인(또는 빈번하게 협력) 탐색 구조를 가진다.
의존 관계
의존 관계는 쉽게 생각하면 파라미터에서 의존하는 타입이 나오거나, 메서드 내부 또는 리턴 타입에서 의존하는 타입이 등장하는 것이다.
의존 관계는 협력을 하는 시점에서 객체들이 일시적으로 관계를 맺는다.
상속 관계
부모 클래스 B가 바뀌면 자식 클래스 A가 결국 바뀌므로 이 또한 의존성을 가진다고 말할 수 있다.
실체화 관계
인터페이스를 구현하는 관계다. 이 또한 클래스 의존성에 포함된다.
1.4 패키지 의존성
패지키 A가 패키지 B에 의존한다. 이는 무슨 의미일까?
- 패키지 B 내부의 클래스가 변경되면, 패키지 A의 클래스가 변경된다는 의미다.
- 의존성을 제일 쉽게 파악하는 방법은 import 구문들을 확인하는 것이다.
1.5 좋은 설계를 위한 가이드
1. 양방향 의존성을 피하고, 단방향 의존성을 구축하라.
- 성능 이슈도 많고, 동기화에 신경 쓸 부분도 많다.
2. 다중성이 적은 방향을 선택해라
- 쉽게 얘기하면 일대다보다는 다대일을 선택하라는 의미다.
3. 의존성이 필요 없다면 의존성을 제거하자.
- 너무나도 당연한 얘기다.
4. 패키지 사이의 의존성 사이클을 제거하자.
- 만약 패키지 3개가 사이클이 있다면 1개의 패키지를 바꾸면 패키지 3개 전부를 바꿔야 한다.
- 설계에서 제일 중요하게 생각할 부분은 변경이다. 따라서 패키지 사이에 싸이클은 없어야 한다.
2. 예제
2.1 가게와 메뉴 도메인
- 가게에는 여러 메뉴가 있다.
- 한 메뉴에는 여러 옵션 그룹이 있다.
- 하나의 옵션 그룹에는 여러 옵션이 있다.
런타임시에는 아래와 같은 모습이다.
2.2 주문 도메인
- 주문에는 여러 주문항목이 있다.
- 하나의 주문 항목에는 여러 주문 옵션 그룹이 있다.
- 하나의 주문 옵션 그룹에는 여러 주문 옵션이 있다.
런타임시에는 아래 이미지와 같다.
2.3 메뉴 & 주문 도메인
메뉴와 주문 도메인들이 런타임 때 아래와 같은 이미지로 엮이는 것을 확인할 수 있다.
그럼 이제부터 메뉴를 선택하고 주문을 진행하는 흐름을 살펴볼 것이다.
참고로 현재 예제는 장바구니 데이터를 로컬 DB에 저장한다고 한다.
우리는 가게에서 메뉴를 담아 장바구니에 넣고 주문을 할 것이다.
근데 여기서 문제점이 있을 수 있다. 앱을 사용하면서 어떤 메뉴를 장바구니에 넣고 주문을 했는데,
사장님이 판매하시는 업소 메뉴를 변경한 것이다. 그럼 메뉴와 주문의 불일치가 발생하는 것이다.
따라서 주문이 발생하면 주문의 데이터와 가게의 메뉴 데이터가 일치하는지 검증을 해야하는 것이다.이제 주문 검증 과정에 대해 살펴보자.
2.4 주문 Validation
크게 검증해야할 항목은 다음과 같다.
- 메뉴의 이름과 주문항목의 이름 비교
- 옵션그룹의 이름과 주문옵션그룹의 이름 비교
- 옵션의 이름과 주문 옵션의 이름 비교
- 옵션의 가겨과 주문 옵션의 가격 비교
- 가게가 영업중인지 확인
- 주문금액이 최소주문금액 이상인지
이제 주문 검증을 위한 객체간의 협력을 설계해보자.
검증 플로우는 다음과 같다.
- 주문이라는 메시지가 주문 객체에 도착한다.
- 먼저 영업 여부를 확인해야 한다. 또한 최소 주문 금액 이상인지 확인한다.
- 메뉴의 이름과 주문항목의 이름을 비교하고 검증한다.
- 다음으로는 옵션 그룹에 메시지를 보내, 옵션그룹과 주문 옵션 그룹의 이름이 같은지 비교하고 검증한다.
- 이번에는 옵션에 메세지를 보내, 주문 옵션과 옵션의 이름과 가격이 동일한지 비교하고 검증한다.
- 이게 다 통과를 해야 주문이 완료된다.
개발자는 협력이라는 동적인 부분을 정적인 코드로 표현해야한다. 이 정적 부분에서 관계라는 것이 들어간다.
관계에는 방향성이 필요하다. 관계를 결정하는 것은 객체의 협력의 방향을 정한다는 것이다.
관계의 방향 == 협력의 방향 ==의존성의 방향
2.5 관계의 종류 결정하기
의존관계
협력을 위해 일시적으로 필요한 의존성이다. 파라미터, 리턴타입, 지역변수 등이 있다.
연관 관계를 사용할 것인지, 의존 관계를 사용할 것인지보다는 방향성이 중요하다.
내가 무엇인가를 참조할 때는 이유가 필요하다.
연관 관계를 넣는 이유, 의존 관계를 넣는 이유가 늘 있어야 하며 그런 이유는 런타임에 객체의 협력에 따라 달라진다.
연관관계
협력을 위해 필요한 영구적인 탐색 구조가 필요하면 연관관계를 넣으면 된다.
Ex: Order와 Shop이 한 번 협력하는게 아니라 굉장히 빈번하게 협력하면 연관관계로 넣는 것이 낫다.
연관 관계는 == 탐색 가능성이다. 일반적인 연관 관계 구현은 객체 참조를 이용한 연관관계 구현이다.
쉽게 말해 의존하는 클래스 타입의 인스턴스 변수를 만드는 것이다.
연관 관계 개념을 객체 참조로 구현할 수 있다. 객체 참조 == 연관 관계는 아니다. 객체 참조 말고 연관 관계를 구현할 방법은 더 있다.
2.6 예제 구현 시작하기
메세지를 결정하고 메서드를 만들자. (객사오에서 많이 본 부분)
주문 객체가 메시지를 받는 place()라는 메서드를 만들었다.
주문을 검증하는 validate() 메서드와 주문의 상태를 바꾸는 ordered() 메서드 2개를 만들었다.
또한 검증을 위해 shop과 orderLineItems(주문 항목)를 연관 관계로 구현했다.
객체와 객체간의 물리적인 경로가 생긴 것이다.
결국 객체들간의 협력을 메서드로 표현한 플로우는 아래와 같다. 코드가 많아서 직접 깃허브나 발표 ppt를 보면 좋을 것 같다.
현재 객체들간의 관계는 레이어드 아키텍처에서 도메인 레이어에 위치한다.
3. 설계 개선하기
이제 예제 코드를 의존성 관점에서 뜯어보겠다. 대부분의 많은 사람이 설계의 개선에 대해 묻는다.
- 설계에 관한 개선은 의존성과 큰 관련이 있다.
- 코드를 짜면 종이에 클래스, 패키지 간의 의존 관계를 그려본다.
- 패키지를 잘 못 나눈건가, 클래스를 잘 못 나눈건가 확인하기.
이번 세미나에서는 두 가지 문제만 확인한다.
- 객체 참조로 인한 결합도 상승
- 패키지 의존성 사이클
현재 설계에서는 어떤 문제가 발생했는가?
- shop 패키지와 order 패키지 사이에 의존성 사이클이 발생했다.
3.1 중간 객체를 이용한 의존성 사이클 끊기
- 중간 객체를 두어 의존성을 단방향으로 흐르게 만들었다.
- 결국 개발에서 추상화란 잘 변하지 않는 것이다. 추상화는 꼭 추상 클래스나 인터페이스는 아니다.
- 장바구니를 로컬이 아닌 서버에 둔다면 장바구니에 담을 때도 이런 검증을 거쳐야한다고 말하셨다.
즉 이 말은 이런 검증을 재사용할 수 있다는 말이다. -> 재사용성이 올라갔다.
3.2 객체 참조로 구현한 연관 관계의 문제점
1. 협력을 위해 필요하지만 두 객체 사이의 결합도가 높아짐
2. 성능 문제 - 어디까지 조회할 것인가?
- 메모리에서는 큰 이슈가 아니다.
- ORM 같은 DB로 매핑을 한다면 헬 게이트가 열린다고 하심. Lazy Loading 이슈.
- 객체 그룹의 조회 경계가 모호해진다.
- 근본적인 문제는 객체가 모두 연결되어있으므로 어디까지 읽고, 어디까지 읽지 말아야하지 라는 가이드가 없다는 점.
3. 수정 시 도메인 규칙을 함께 적용할 경계는?
- Order의 상태를 변경할 때 연관된 도메인 규칙을 함께 적용해야하는 객체의 범위는?
- 다른 말로는 트랜잭션 경계는 어디까지인가??
- 어떤 테이블에서 어떤 테이블까지 하나의 단위로 Lock을 설정할 것인가?
배달 완료 트랜잭션 범위에 대해 아래 이미지를 살펴보자.
사실 shop과 Order과 Delivery는 변경의 빈도가 다르다.
이런 Long한 트랜잭션, 즉 무지성으로 객체를 업데이트하면 트랜잭션 경합으로 인해 성능 저하, 또는 응답 저하가 발생한다고 한다.
3.3 객체 참조가 정말 필요할까?
- 객체 참조는 모든 것을 연결해버린다. 어떤 객체라도 접근가능하다. 어떤 객체라도 함께 수정이 가능하다.
- 이런 문제점이 분명 존재한다.
객체 참조는 결합도가 가장 높은 의존성이다. 필요한 경우에는 객체 참조를 끊어버려야 한다.
현재는 객체 참조를 통한 탐색(강한 결합도)를 진행한다.
그러면 결합도를 낮추면서 연관 관계를 유지하기 위해 Repository를 사용한다고 한다.
3.4 그럼 어떤 객체를 묶고 어떤 객체를 분리할 것인가?
- 함께 생성되고 함께 삭제되는 객체들을 함께 묶어라.
- 도메인 제약사항을 공유하는 객체들을 함께 묶어라.
- 가능하면 분리하라.
- 비즈니스에 의해 결정된다. 트랜잭션으로 묶어야하는 애들은 묶자.
경계 안의 객체는 참조를 이용해 접근한다.
경계 밖의 객체는 ID를 이용해 접근한다.
만약 Order에서 Shop을 검색하고 싶다면 id를 사용해 Repository에서 찾아와야한다.
이제 트랜잭션 관리도 이 단위(경계)로 묶어야하고, 조회도 이 단위로 진행하면 된다.
근데 이렇게 객체 참조를 끊으면 우리 코드, Shop과 Order에서는 첫 번째 컴파일 에러가 발생한다!
3.5 객체를 직접 참조하는 로직을 다른 객체로 옮기자
Validation 로직을 몽땅 모은 코드는 아래와 같다.
- Order의 Validation을 전적으로 책임지는 OrderValidator를 구현했다.
- 객체지향은 여러 객체를 오가면서 검증 로직을 파악해야하는데.. 이게 검증 로직을 따라가기 생각보다 빡세다.
너무 검증 로직이 이곳 저곳 흩어져 있기 때문 - Order에서 검증 로직이 있다면 응집도가 낮다. 변경의 주기가 다른 것들이 모여있으므로 (주문 처리와 검증)
- 이렇게 OrderValidator를 구현하면서 Order의 책임에만 충실해져 응집도가 높아졌다.
- 때로는 절차지향이 객체지향보다 좋다. 특히 이런 검증 로직인 부분에서.
두 번째 컴파일 에러는 배달 완료 부분이다.
이번에는 이전과 다르게 도메인 로직의 순차적 실행에 관해 발생한 컴파일 에러다. ( 전후 관계가 존재해서 이 로직이 생김)
즉 어떤 객체가 바뀌면 다른 객체도 바뀌어야 하는 상황.
여기에는 두 가지 해결방법이 있다고 한다.
- 절차지향 로직
- 도메인 이벤트 퍼블리싱
3.6 절차지향 로직으로 문제 해결
- 먼저 OrderDeliveredService 클래스를 추가한다.
- 그리고 이 클래스에 배달 완료 로직을 전부 넣는다.
- 그러면 절차지향적인 OrderDeliveredService가 탄생한다.
- 그리고 OrderService에 해당 클래스를 의존성 주입한다. 그러면 아래와 같은 이미지다.
이렇게 문제를 해결한 것 같지만, 우리는 의존성을 한번 종이 위에 그려볼 필요가 있다. 그러면 의존성이 다음과 같이 그려진다.
order 패키지와 delivery 패키지에서 양방향 의존 사이클이 생겼다.
이 경우에는 인터페이스를 이용해서 의존성을 역전시키면 된다.
이제와서 보면 절차지향 방식으로 해결한 것이 아니라 의존성을 역전시켜서 문제를 해결한 것 같다.
3.6 도메인 이벤트 퍼블리싱으로 문제 해결
사실 이 부분은 명확하게 잘 이해가 되지는 않았다. 다음에 다시 보면서 정리할 예정.. 그래도 정리를 해보자면!
정의는 도메인 이벤트를 통해 의존성을 제거하는 것이다.
- Order가 Shop을 직접 호출하던 로직을 Order가 도메인 이벤트를 발행하도록 수정하면 된다.
- 이렇게 이벤트를 추가하다보면 2번째 방식도 결국 shop과 order에서 의존성 사이클이 생긴다.
- 이유는 이벤트 핸들러가 shop 패키지에 있기 때문이다.
- 따라서 이벤트 핸들러를 다른 패키지로 분리하면 된다. 이렇게 문제를 해결하므로 의존성을 단방향으로 흐르게 만들었다.
4. 의존성과 시스템 분리
패키지 의존성을 제거하는 3가지 방법.
- 중간 객체를 만든다.
- 의존성을 인터페이스나 추상 클래스를 통해서 역전시킨다.
- 패키지를 찢는 것이다.
도메인 단위 모듈 == 시스템 분리의 기반
도메인 단위로 시스템 분리가 가능하다. System Event를 통한 시스템 통합.
의존성을 따라 시스템을 진화시키자.
마지막 도메인 이벤트 부터는 추후에 공부를 더 하고 내용을 추가하려고 합니다.
이상으로 포스팅을 마칩니다. 감사합니다.
강의 링크 및 강의 자료 깃허브
https://www.youtube.com/watch?v=dJ5C4qRqAgA&t=107s
https://github.com/eternity-oop?tab=repositories
댓글