감상문
의존성을 이용해 설계 진화시키기
ihl
2021. 9. 12. 16:16
개발을 하다보면 코드를 수정하고 리팩토링하는 일을 자연스럽게 겪게 된다. 코드를 리팩토링하는 기준은 다양하겠지만 그 중 대표적인 것은 의존성이다. 의존성에 따라 하나의 코드를 고쳤을 때 다른 코드도 점검하고 수정해야하는 일이 발생할 수 있기 때문이다. 이러한 의존성을 점검하고, 설계를 진화시키는 방법을 알아보자.
1. 설계와 의존성
- 설계에서 사용하는 역할, 책임이란 말은 '의존성을 어떻게 관리할 것인가' 와 일맥 상통한다.
따라서 의존성을 관리하는 방법에 따라 설계는 변화한다. - 설계란 코드를 어떻게 배치할 것인가를 의미한다.
- 같이 변경되어야할 확률이 높은 코드는 함께 뭉쳐놓고, 그렇지 않은 코드는 분리한다.
- 뭉쳐놓는다는 것은 같은 함수-클래스-패키지-프로젝트에 코드를 함께 넣는다는 것이다.
- 의존성은 변경에 의해 영향을 받을 수 있는 가능성을 의미한다.
따라서 의존성은 코드 배치에 영향을 준다.
- 같이 변경되어야할 확률이 높은 코드는 함께 뭉쳐놓고, 그렇지 않은 코드는 분리한다.
2. 클래스간 연관관계
- Association(연관관계) : A 내부에서 B로 갈 수 있는 영구적인 경로가 있다.
A클래스가 B클래스의 인스턴스를 멤버 변수로 갖는 경우 - Dependency(의존관계) : A 내부에서 B로 갈 수 있는 일시적인 경로가 있다.
A클래스가 파라미터, 리턴으로 B 인스턴스를 받거나, 메소드 내에서 해당 B의 인스턴스를 생성 - Inheritance(상속관계) : B의 구현이 변경되면 A에 영향을 준다.
class A extends B - Realization(실체화관계) : B의 Operation Signiture가 변경되면 A에 영향을 준다.
class A implements B- B는 직접 구현하지 않으므로 B의 구현이 A에 영향을 주지 않는다.
- Operation: UML에서 Visibility( +, -, #, ~). 예를 들어 함수의 private-protect-public 을 의미한다.
- Signature: UML에서 Signature(operation-name '(' [ parameter-list ] ')' [ ':' return-spec ]). 예를 들어 함수의 이름-파라미터-리턴값으로 이루어진 정의를 의미한다.
3. 좋은 의존성의 규칙
- 한쪽 방향으로만 의존하는 것이 좋다.
- 양방향은 두 데이터 간의 Sync를 맞추기 어렵고, 성능상의 문제가 발생한다.
- 의존성이 Cycle을 생성하지 않도록 한다.
- 다중성이 적은 방향이 좋다.
- 일대다 관계 보다는 다대일 관계가 좋다.
- class가 Collection 멤버 변수를 갖지 않는 것이 관리하기 좋다.
- 일대다 관계: class A { private Collection<B> bs }. class B
- 다대일 관계: class A{}. calss B{ private A a; }
- 일대다 관계 보다는 다대일 관계가 좋다.
- 의존성이 필요 없다면 제거한다.
- 위 규칙이 절대적인 것은 아니다. 유연하게 대처하자.
4. 설계하기
- 각 객체들이 어떤 메시지를 서로 주고받는지 정리하여 설계한다.
- 각 객체들 간의 정적인 요소(관계/로직)을 찾는다.
- 객체들은 런타임에서 동적으로 생성-동작-소멸된다.
- 정적인 코드로 동적인 객체들의 가능성/변화를 표현하기 위해 정적인 관계/로직을 찾아야 한다.
- 먼저 메시지를 결정한 후 메소드를 만든다. (메시지를 받아야 한다면 -> 메시지를 처리하는 메소드를 만든다.)
- 관계 = A 객체가 어떤 방식으로 B 객체와 의존할 것인가
- = 런타임에 A 코드와 B 코드는 어떤 방향으로 협력(메시지 전송) 할 것이다.
- 관계에는 방향성이 있다.
- 관계의 방향 = 협력의 방향 = 의존성의 방향
- 연관관계 : 협력을 위해 필요한 영구적인 탐색 구조
- A와 B가 빈번하게 메시지를 주고 받는다. = 차라리 영구적인 관계를 갖게하겠다. => 연관관계
- 연관관계(개념) != 객체참조(연관관계 구현방법 중 하나)
- 의존관계 : 협력을 위해 일시적으로 필요한 의존성
- 파라미터, 리턴타입, 지역변수
- 파라미터, 리턴타입, 지역변수
5. 설계 점검하기
- 코드 작성 -> 종이에 의존성을 그려본다. -> 의존성 측면에서 마음에 걸리는 점이 있는지 확인한다.
- 의존성 사이클이 존재하는지 확인
- 결합도를 개선할만한 부분이 있을지 확인
- -> 의존성을 끊는다!
- 의존성을 끊으면 코드의 재사용성이 증가한다.
- 같은 코드를 다른 데이터를 처리할 때 사용가능
6. 의존성 사이클 끊기
- 중간객체 사용하기
- 의존성 역전의 원리 = 추상적인 것에 의존한다.
- 추상화 != abstract/interface
- 코드에서 잘 변하지 않는 것(수정되지 않는 것)을 의미한다.
- Option은 다른 도메인 보다 필요한 데이터만 존재하므로 잘 변하지 않고, 추상적이다.
7. 객체 참조
- 객체참조(멤버 변수로 직접 다른 클래스를 갖고 있는 경우)는 가장 강한 결합도를 갖는다.
- 모든 것을 연결(접근/수정)하므로, 반드시 필요한 객체참조인지 확인한 후 필요하면 끊어야 한다.
- ORM, DB 개념이 개입되면 성능문제가 발생한다.
- 쿼리를 할 때 어디까지 읽어야할지 트랜젝션 경계에 대한 가이드가 없다.
- 트랜젝션 경합 발생
- = 어떤 테이블에서 어떤 테이블까지를 하나의 잠금 단위로 설정할 것인가?
- ex. 배달완료 = OrderService + Order(주문) + Shop(영수증) + Delivery라 한다면?
- 배달완료를 위해 위 4가지 Table을 Lock했을 때, Shop 변경 메시지가 오는 등의 사건이 발생하면, Lock이 풀릴 때까지 기다려야 한다.(트랜젝션 경합)
- 4개의 요소가 항상 같은 타이밍에 메시지를 받는 것이 아니기 때문이다.
- 긴 트랜젝션으로 인한 성능이슈 (Lazy Loading)
- Order의 상태를 변경할 때 연관된 도메인 규칙을 함께 적용해야하는 객체의 범위는?
- 코드로는 Order부터 시작하여 하나씩 바꾸면 됨
- DB, Infra는 트랜젝션이 길어져 성능이슈가 발생한다.
- Order의 상태를 변경할 때 연관된 도메인 규칙을 함께 적용해야하는 객체의 범위는?
- 쿼리를 할 때 어디까지 읽어야할지 트랜젝션 경계에 대한 가이드가 없다.
8. 객체 분리
- 함께 생성되고 함께 삭제되는 객체(라이프사이클이 같은 객체)를 함께 묶는다.
- ex. 장바구니와 장바구니 항목은 생성되는 타이밍(라이프사이클)이 다르므로 분리한다.
- 같은 도메인 제약사항을 공유하는 객체를 함께 묶는다.
- 비즈니스적으로 함께 변경되어야 하는 것을 하나의 트랜잭션으로 묶는다.
- ex. 같은 가게의 항목을 하나의 장바구니에 넣을 수 있다는 제약조건이 있으므로, 장바구니와 장바구니 항목을 묶는다.
- 그 외에는 가능하면 분리한다.
- 묶인 객체는 연관 관계로 묶는 것이 편하다.
- 묶인 단위로 비즈니스 제약의 단위를 만들고, 트랜젝션을 수행한다.
- 분리된 객체는 Id를 이용하여 접근한다.
- 묶인 객체는 연관 관계로 묶는 것이 편하다.
9. Repository
- 8에서 분리된 객체들을 Id를 이용하여 접근하기 위한 Repository를 생성한다.
- 즉, 객체를 직접 참조하지 않고, 다른 객체를 이용하여 참조한다.
- Id를 참조한 후, 하는 일은 다른 클래스에 넣는다.
10. 절차지향 로직 분리
- id 조회, 하는 일을 따로 분리한 후 이들을 순서대로 호출하는 절차지향적 로직을 한 곳에 모은다.
- 한 곳에서 비즈니스적 흐름을 확인할 수 있다.
- 절차지향 로직 분리 대신 도메인 이벤트 퍼블리싱을 사용할 수 있다.