ADP(Acyclic Dependecies Principle) : 의존성 비순환 원칙
-
"컴포넌트 의존성 그래프에 순환(cycle)이 있어서는 안 된다."
개발자가 동일한 소스 파일을 수정하는 환경에서는 숙취 증후군(morning after syndrome)이 발생한다.
잘 작동하던 코드가 다음날 작동하지 않는 것이다.
이에 대한 해결책은 두 가지가 있다.
한 가지는 "주 단위 빌드(weekly build)"이며, 두 번째 해결책은 '의존성 비순환(Acyclic Dependencies Principle, ADP) 이다.
* 주 단위 빌드 (Weekly Build)
-
개발자 모두 코드를 개인적으로 복사하여 작업하며, 전체적인 기준에서 작업을 어떻게 통합할지는 걱정하지 않는다.
그런 후 금요일이 되면 변경된 코드를 모두 통합하여 시스템을 빌드한다.
단점은 금요일에 통합과 관련된 막대한 업보를 치러야 한다.
프로젝트가 커지면 프로젝트 통합은 금요일 하루 만에 끝마치는 게 불가능해진다.
통합이라는 짐은 점점 커지고, 결국 토요일까지 넘어가기 시작한다.
추후에는 개발보다 통합에 드는 시간이 늘어나면서 팀의 효율성도 서서히 나빠진다.
통합과 테스트는 점점 더 어려워진다.
* 순환 의존성 제거하기
-
이 문제의 해결책은 개발 환경을 릴리스 가능한 컴포넌트 단위로 분리하는 것이다.
이를 통해 컴포넌트는 개별 개발자 또는 단일 개발팀이 책임질 수 있는 작업 단위가 된다.
개발자가 해당 컴포넌트가 동작하도록 만든 후, 해당 컴포넌트를 릴리스하여 다른 개발자가 사용할 수 있도록 만든다.
담당 개발자는 이 컴포넌트에 릴리스 번호를 부여하고, 다른 팀에서 사용할 수 있는 디렉터리로 이동시킨다.
그런 다음 개발자는 자신만의 공간에서 해당 컴포넌트를 지속적으로 수정한다.
나머지 개발자는 릴리스된 버전을 사용한다.
컴포넌트가 새로 릴리스되어 사용할 수 있게 되면, 다른 팀에서는 새 릴리스를 당장 적용할지를 결정해야 한다.
적용하지 않기로 했다면 그냥 과거 버전의 릴리스를 계속 사용한다.
-
이 작업 절차는 단순하며 합리적이어서 널리 사용되는 방식이다.
하지만 이 절차가 성공적으로 동작하려면 컴포넌트 사이의 의존성 구조를 반드시 관리해야 한다.
의존성 구조에 순환이 있어서는 안 된다.
의존성 구조에 순환이 생기면 '숙취 증후군'을 피해 갈 수 엇다.
-
의존성 관계를 그래프로 그리면 컴포넌트를 수정했을 떄 영향을 받는 컴포넌트를 쉽게 볼 수 있다.
의존성 화살표를 거꾸로 따라가면 된다.
* 순환이 컴포넌트 의존성 그래프에 미치는 영향
-
순환 의존성은 즉각적인 문제를 일으킨다.
Cycle 이 되어 있는 컴포넌트들이 사실상 하나의 거대한 컴포넌트가 되어 버린다.
순환이 생기면 컴포넌트를 분리하기가 상당히 어려워진다.
단위 테스트를 하고 릴리스를 하는 일도 굉장히 어려워지며 에러도 쉽게 발생한다.
게다가 모듈의개수가 많아짐에 따라 빌드 관련 이슈는 기하급수적으로 증가한다.
컴포넌트를 어떤 순서로 빌드해야 올바른지 파악하기가 상당히 힘들어진다.
사실 순환이 생기면 올바른 순서라는 것 자체가 없을 수 있다.
* 순환 끊기
-
컴포넌트 사이의 순환을 끊고 의존성을 다시 DAG(Directed Acyclic Graph) 로 원상복구하는 일은 언제라도 가능하다.
1. 의존성 역전 원칙(DIP)을 적용한다.
2. 순환 의존성을 만들어내는 컴포넌트 중간에 그 둘이 의존하는 새로운 컴포넌트를 만든다.
그리고 두 컴포넌트가 모두 의존하는 클래스들을 새로운 컴포넌트로 이동시킨다.
* 흐트러짐 (Jitters)
-
요구사항이 변경되면 컴포넌트 구조도 변경될 수 있다.
실제로 앱이 성장함에 따라 컴포넌트 의존성 구조는 서서히 흐트러지며 또 성장한다.
따라서 의존성 구조에 순환이 발생하는지를 항상 관찰해야 한다.
순환이 발생하면 어떤 식으로든 끊어야 한다.
하향식(top-down) 설계
-
컴포넌트 구조는 하향식으로 설계될 수 없다.
컴포넌트는 시스템에서 가장 먼저 설계할 수 있는 대상이 아니며, 오히려 시스템이 성장하고 변경될 때 함께 진화한다.
-
컴포넌트 의존성 다이어그램은 앱의 기능을 기술하는 일과는 거의 관련이 없다.
오히려 의존성 다이어그램은 앱의 빌드 가능성(buildability)과 유지보수성(maintainability)을 보여주는 지도와 같다.
이러한 이유 때문에 컴포넌트 구조는 프로젝트 초기에 설계할 수 없다.
-
의존성 구조와 관련된 최우선 관심사는 변동성을 격리하는 일이다.
우리는 변덕스러운 이유로 자주 변경되는 컴포넌트로 인해, 그렇지 않았다면 안정적이었을 컴포넌트가 영향받는 일을 원치 않는다.
결국 컴포넌트 의존성 그래프는 자주 변경되는 컴포넌트로부터 안정적이며 가치가 높은 컴포넌트를 보호하려는 아키텍트가 만들고 가다듬게 된다.
-
앱이 계속 성장함에 따라 우리는 재사용 가능한 요소를 만드는 일에 관심을 기울이기 시작한다.
이 시점이 되면 컴포넌트를 조합하는 과정에 공통 재사용 원칙(CRP)이 영향을 미치기 시작한다.
결국 순환이 발생하면 ADP 가 적용되고, 컴포넌트 의존성 그래프는 조금씩 흐트러지고 또 성장한다.
SDP(Stable-Dependencies Principle) : 안정된 의존성 원칙
-
"안정성의 방향으로(더 안정된 쪽에) 의존하라."
설계는 결코 정적일 수 없다.
설계를 유지하다 보면 변경은 불가피하다.
공통 폐쇄 원칙을 준수함으로써, 컴포넌트가 다른 유형의 변경에는 영향받지 않으면서도 특정 유형의 변경에만 민감하게 만들 수 있다.
-
변경이 쉽지 않은 컴포넌트가 변동이 예상되는 컴포넌트에 의존하게 만들어서는 절대로 안 된다.
한 번 의존하게 되면 변동성이 큰 컴포넌트도 결국 변경이 어려워진다.
모듈을 만들 때는 변경하기 쉽도록 설계했지만, 이 모듈에 누군가가 의존성을 매달아 버리면 그 모듈도 변경하기 어려워진다.
안정된 의존성 원칙(Stable Dependency Principle, SDP)을 준수하면 변경하기 어려운 모듈이 변경하기 쉽게 만들어진 모듈에 의존하지 않도록 만들 수 있다.
* 안정성
-
안정성은 변경을 만들기 위해 필요한 작업량과 관련된다.
소프트웨어 컴포넌트를 변경하기 어렵게 만드는 데는 많은 요인이 존재하며, 그 예로는 컴포넌트의 크기, 복잡도, 간결함 등을 들 수 있다.
소프트웨어 컴포넌트를 변경하기 어렵게 만드는 확실한 방법 하나는 수많은 다른 컴포넌트가 해당 컴포넌트에 의존하게 만드는 것이다.
사소한 변경이더라도 의존하는 모든 컴포넌트를 만족시키면서 변경하려면 상당한 노력이 들기 때문이다.
* 안정성 지표
-
컴포넌트로 들어오고 나가는 의존성의 개수를 세어 봄으로써 위치상(positional) 어느 정도의 안정성을 가지는지 계산할 수 있다.
Fan-in : 안으로 들어오는 의존성. 컴포넌트 내부의 클래스에 의존하는 컴포넌트 외부의 클래스 개수.
Fan-out : 바깥으로 나가는 의존성. 외부의 클래스에 의존하는 컴포넌트 내부의 클래스 개수.
I(불안정성): I = Fan-out / (Fan-in + Fan-out). I=0 이면 최고로 안정된 컴포넌트. I=1 이면 최고로 불안정한 컴포넌트라는 뜻이다.
I 값이 1이면 어떤 컴포넌트도 해당 컴포넌트에 의존하지 않지만(Fan-in=0) 해당 컴포넌트는 다른 컴포넌트에 의존한다.(Fan-out>0)는 뜻이다.
이는 최고로 불안정한 상태다.
이 컴포넌트는 책임성이 없으며 의존적이다.
자신에게 의존하는 컴포넌트가 없으므로, 이 컴포넌트는 변경하지 말아야 할 이유가 없다.
반대로 이 컴포넌트가 다른 컴포넌트에 의존한다는 사실은 언젠가는 해당 컴포넌트를 변경해야 할 이유가 생긴다는 뜻이다.
반대로 I값이 0이면 해당 컴포넌트에 의존하는 다른 컴포넌트가 있지만 (Fan-in>0), 해당 컴포넌트 자체는 다른 컴포넌트에 의존하지 않는다.(Fan-out=0)는 뜻이다.
이러한 컴포넌트는 다른 컴포넌트를 책임지며 또 독립적이다.
이러한 상태는 최고로 안정된 상태다.
자신에게 의존하는 컴포넌트가 있으므로 해당 컴포넌트는 변경하기가 어렵지만, 해당 컴포넌트를 변경하도록 강제하는 의존성을 갖지 않는다.
-
의존성 방향으로 갈수록 I 지표 값이 감소해야 한다.
* 모든 컴포넌트가 안정적이어야 하는 것은 아니다.
-
모든 컴포넌트가 최고로 안정적인 시스템이라면 변경이 불가능하다.
실상 불안정한 컴포넌트도 있고 안정된 컴포넌트도 존재할 수밖에 없다.
-
의존성 다이어그램에서 관례적으로 불안정한 컴포넌트를 위쪽에 두고, 안정된 컴포넌트를 아래에 두는데 이는 상당히 유용하다.
위로 향하는 화살표가 있으면 SDP 를 위배하는(ADP 도 위반) 상태가 되기 때문이다.
-
SDP, ADP 를 위반하는 사례가 발생하면 DIP 로 해결할 수 있다.
* 추상 컴포넌트
-
오로지 인터페이스만 포함하는 컴포넌트를 생성하는 방식이 이상해 보일 수도 있다.
이러한 컴포넌트에는 실행 가능한 코드가 전혀 없다.
하지만 자바나 C# 같은 정적 타입 언어를 사용할 때 이 방식은 상당히 흔할 뿐 아니라, 꼭 필요한 전략으로 알려져 있다.
이러한 추상 컴포넌트는 상당히 안정적이며, 따라서 덜 안정적인 컴포넌트가 의존할 수 있는 이상적인 대상이다.
SAP(Stable Abstractions Principle): 안정된 추상화 원칙
-
"컴포넌트는 안정된 정도만큼만 추상화되어야 한다."
* 고수준 정책을 어디에 위치시켜야 하는가?
-
시스템에는 자주 변경해서는 절대로 안 되는 소프트웨어도 있다.
고수준 아키텍처나 정책 결정과 관련된 소프트웨어가 그 예다.
업무 로직이나 아키텍처와 관련된 결정에는 변동성이 없기를 기대한다.
따라서 시스템에서 고수준 정책을 캡슐화하는 소프트웨어는 반드시 안정된 컴포넌트(I=0)에 위치해야 한다.
불안정한 컴포넌트(I=1)는 반드시 변동성이 큰 소프트웨어, 즉 빠르게 변경할 수 있는 소프트웨어만을 포함해야 한다.
하지만 고수준 정책을 안정된 컴포넌트에 위치시키면, 그 정책을 포함하는 소스 코드는 수정하기가 어려워진다.
이로 인해 시스템 전체 아키텍처가 유연성을 잃는다.
컴포넌트가 최고로 안정된 상태이면서도(I=0) 동시에 변경에 충분히 대응할 수 있을 정도로 유연하게 만들 수 있다. 개방 폐쇄 원칙(OCP)를 활용하는 것이다.
OCP 에서는 클래스를 수정하지 않고도 확장이 충분히 가능할 정도로 클래스를 유연하게 만들 수 있다.
추상(abstract) 클래스가 이 원칙을 준수한다.
* 안정된 추상화 원칙
-
안정된 추상화 원칙(Stable Abstractions Principle, SAP)은 안정성(stability)과 추상화 정도(abstractness) 사이의 관계를 정의한다.
이 원칙은 한편으로는 안정된 컴포넌트는 추상 컴포넌트여야 하며, 이를 통해 안정성이 컴포넌트를 확장하는 일을 방해해서는 안 된다고 말한다.
-
안정적인 컴포넌트라면 반드시 인터페이스와 추상 클래스로 구성되어 쉽게 확장할 수 있어야 한다.
안정된 컴포넌트가 확장이 가능해지면 유연성을 얻게 되고 아키텍처를 과도하게 제약하지 않게 된다.
-
SAP(Stable Abstractions Principle) 와 SDP(Stable Dependencies Principle) 를 결합하면 컴포넌트에 대한 DIP 나 마찬가지가 된다.
SDP 에서는 의존성이 반드시 안정성의 방향으로 향해야 한다고 말하며, SAP 에서는 안정성이 결국 추상화를 의미한다고 말한다.
따라서 의존성은 추상화의 방향으로 향하게 된다.
* 추상화 정도 측정하기
-
A 지표는 컴포넌트의 추상화 정도를 측정한 값이다.
이 값은 컴포넌트의 클래스 총 수 대비 인터페이스와 추상 클래스의 개수를 단순히 게산한 값이다.
Nc : 컴포넌트의 클래스 개수
Na : 컴포넌트의 추상 클래스와 인터페이스의 개수
A : 추상화 정도. A = Na / Nc
A지표는 0과 1 사이의 값을 갖는다
A 가 0이면 컴포넌트에는 추상 클래스가 하나도 없다는 뜻이다.
A 가 1이면 컴포넌트는 오로지 추상 클래스만을 포함한다는 뜻이다.
* 주계열 (Main Sequence)
-
A 와 I 의 관계에 대해 말해야 할 때이다.
* 고통의 구역
-
A 와 I 모두 0 주변 구역은 매우 안정적이며 구체적이다.
이는 매우 뻣뻣한 상태이다.
추상적이지 않으므로 확장할 수 없고, 안정적이므로 변경하기도 상당히 어렵다.
제대로 설계된 컴포넌트라면 이 영역에 들어가지 않는다.
이 영역을 고통의 구역(Zone of Pain)이라 부른다.
-
일부 소프트웨어 엔티티는 고통의 구역에 위치하곤 한다.
DB 스키마가 한 예다.
스키마는 변동성이 높기로 악명이 높으며, 극단적으로 구체적이며, 많은 컴포넌트가 여기에 의존한다.
또 다른 예는 구체적인 유틸 라이브러리이다.
I 지표가 0일지라도, 실제로는 변동성이 거의 없다.
String 컴포넌트가 그 예다.
변동성이 없는 컴포넌트는 (0,0) 구역에 위치했더라도 해롭지 않다.
이러한 이유로 고통의 구역에서 문제가 되는 경우는 변동성이 있는 소프트웨어 컴포넌트다.
* 쓸모없는 구역
-
A 와 I 가 모두 1에 가까운 영역은 최고로 추상적이지만, 누구도 그 컴포넌트에 의존하지 않는다는 의미이다.
이러한 컴포넌트는 쓸모가 없다.
따라서 이 영역은 쓸모없는 구역(Zone of Uselessness)이라고 부른다.
* 배제 구역 벗어나기
-
변동성이 큰 컴포넌트 대부분은 두 배제 구역으로부터 가능한 한 멀리 떨어뜨려야 한다.
각 배제 구역으로부터 최대한 멀리 떨어진 점의 궤적은 A 와 I 그래프 중 (1,0)과 (0,1) 을 잇는 선분이다.
이 선분을 주계열(Main Sequence)라고 부를 수 있다.
-
주계열에 위치한 컴포넌트는 자신의 안정성에 비해 '너무 추상적'이지도 않고, 추상화 정도에 비해 '너무 불안정'하지도 않다.
이 컴포넌트는 쓸모없지 않으면서도 심각한 고통을 안겨 주지도 않는다.
컴포넌트가 추상화된 수준에 어울릴 정도로만 다른 컴포넌트가 의존하며, 구체화된 수준에 어울릴 정도로만 다른 컴포넌트에 의존한다.
-
컴포넌트가 위치할 수 있는 가장 바람직한 지점은 주계열의 두 종점이다.
뛰어난 아키텍트라면 대다수의 컴포넌트가 두 종점에 위치하도록 만들기 위해 매진한다.
* 주계열과의 거리
-
이상적인 상태로부터 컴포넌트가 얼마나 멀리 떨어져 있는지 측정하는 지표가 있다.
D : 거리. |A+I-1|.
이 지표의 유효범위는 [0,1]이다.
D 가 0이면 컴포넌트가 주계열 바로 위에 위치한다는 뜻이며, 1이면 주계열로부터 가장 멀리 위치한다는 뜻이다.
-
각 컴포넌트에 대해 먼저 D 지표를 계산하고, D 값이 0에 가깝지 않은 컴포넌트가 있다면 해당 컴포넌트는 재검토한 후 재구성할 수 있다.
설계를 통계적으로 분석하는 일 또한 가능해진다.
설계에 포함된 모든 컴포넌트에 대해 D 지표의 평균과 분산을 구한다.
주계열에 일치하도록 설계되었다면 평균과 분산은 0에 가까워진다.
그리고 분산은 관리 한계(control limit)를 결정하기 위한 목적으로 사용할 수 있는데, 분산을 통해 다른 컴포넌트에 비해 '극히 예외적인' 컴포넌트를 식별할 수 있기 때문이다.
-
D 지표를 활용하는 또 다른 방법으로 각 컴포넌트의 D 값을 시간에 따라 그려볼 수 있다.
결론
-
의존성 관리 지표는 설계의 의존성과 추상화 정도가 내가 훌륭한 패턴이라고 생각하는 수준에 얼마나 잘 부합하는지를 측정한다.
좋은 의존성도 있지만 좋지 않은 의존성도 있다.
그리고 지표는 신이 아니다.
지표는 그저 임의로 결정된 표준을 기초로 한 측정값에 지나지 않는다. 이러한 지표는 아무리 해도 불완전하다. 유용한 뭔가를 찾는데 도움이 될 뿐이다.
'프로그래밍 놀이터 > 디자인 패턴, 리펙토링' 카테고리의 다른 글
[책 정리] 16장. 독립성 - Clean Architecture (0) | 2022.10.22 |
---|---|
[책 정리] 15장. 아키텍처란? - Clean Architecture (0) | 2022.10.21 |
[책 정리] 13장. 컴포넌트 응집도 - Clean Architecture (0) | 2020.04.17 |
[책 정리] 12장. 컴포넌트 - Clean Architecture (0) | 2020.04.16 |
[책 정리] 11장. DIP(Dependency Inversion Principle) : 의존성 역전 원칙 - Clean Architecture (0) | 2020.04.15 |
댓글