컴포넌트 의존성 그래프에 순환이 있어서는 안 된다.
내가 의존하고 있던 무언가를 수정한 경우에 갑자기 내가 만든 코드가 동작을 안할 수 있는데 이러한 상황을 "숙취 증후군"이라고 한다.
"숙취 증후군"은 많은 개발자가 동일한 소스 파일을 수정하는 환경에서 발생한다. 이는 개발의 효율성을 매우 저하시키는데 이를 해결하기 위해
- 주 단위 빌드
- 의존성 비순환 원칙
이 등장하였다.
- 일주일 중 첫 4일은 서로의 코드에 대해서 신경쓰지 않고 통합도 고민하지 않는다.
- 이를 통해 개발자가 독립적을 보장 해 줄 수 있다.
- 다만 금요일에 한번에 충격을 크게 받을 수 있다. 프로젝트가 커져감에 따라서 점점 주말을 침범하게 되고 통합을 점점 앞으로 당기거나 격주로 하기도 한다.
- 그러나 이러한 흐름이 결국 효율성의 저하를 가져오고 빠른 피드백이 주는 장점을 잃는다.
- 위 문제를 해결하는 가장 좋은것은 개발 환경을 릴리즈 가능한 컴포넌트 단위로 분리하는 것이다.
- 여기서 컴포넌트는 개별 개발자 또는 단일 개발 팀이 책임질 수 있는 작업 단위가 된다.
- 개발자는 컴포넌트가 동작하도록 만들어 놓고 다른 개발자가 사용하도록 한다.
- 릴리즈 번호를 부여하고 다른팀에서 그 릴리즈 버전을 사용하게 된다.
- 컴포넌트가 새로운 릴리즈 번호로 빌드 된다면 사용하는 측은 이를 도입할지 말지를 고민해서 결정이 가능하다. 즉, 컴포넌트의 변경에 의해서 팀이 움직이지 않게 된다.
- 이를 통해서 "숙취 증후군"을 피할 수 있다.
- 하지만 만약 의존성 구조에 "순환"이 생긴다면 다시 문제가 된다.
- 위 사진은 전형적인 컴포넌트 다이어 그램을 나타내며 각 화살표는 의존성을 나타낸다.
- 위 그림은 화살표를 어떻게 따라가더라도 자신에게 돌아올 수 없는 즉, __비순환 방향 그래프__이다.
- Presenters 컴포넌트가 변경되는 상황을 가정할 때 View와 Main을 개발하는 작업자만 자신의 코드와 언제 통합해야할 지 고민하면 된다.
- Main은 새로 릴리즈 되더라도 영향받는 컴포넌트가 없다.
- 시스템 전체를 릴리즈 해야하는 상황에서는 릴리즈 절차는 상향식으로 진행된다. 가장 먼저 Entitis 컴포넌트를 컴파일하고, 테스트하고 릴리즈 한다. 그리고 나서 역순으로 컴포넌트들 모두 동일한 과정을 거친다. 이렇게 절차가 명료하며 쉽게 처리가 가능하다.
우리는 지금까지 비순환을 알아봤는데 순환이 끼치는 영향을 알아보자.
위 사진을 봤을때 각 컴포넌트 별로 순환참조를 하고 있는 것을 알 수 있다. 이는 바로 문제를 일으키는데 그 이유를 알아보자.
- 예를들어 Database 컴포넌트를 만드는 개발자는 릴리즈를 하기 위해서는 반드시 Entities에 호환되어야 한다는 것을 알고 있을텐데 Entities는 Authorizer에 의존적이다. 그런데 다시 Authorizer은 interators에 의존적인데 다시 interactors는 Entities에 의존적이게 되어버려 결론적으로는 서로 순환되고 있는 컴포넌트들이 "하나의 거대한 " 컴포넌트를 이루게 되어 릴리즈가 어렵고 "숙취 증후군" 역시 각 컴포넌트를 개발하는 개발자들은 떠안게 된다.
- 커지는 문제 뿐만이 아니라 Entities를 테스트 할때에 Authroizer와 Interactors도 통합해서 빌드 통합 해야 하기 때문에 빌드 역시 어려워 진다.
- 이는 모듈이 많아지면 많아질수록 문제가 점점 커진다.
순환이 나쁜걸 알았으니 이제는 끊어보자. 두가지 메커니즘이 존재한다.
- 의존성 역전 원칙을 적용한다. 인터페이스를 이용하는 방법이다. 아래 사진과 같이 직접적으로 의존하는 것이 아닌 User가 필요로 하는 메서드 인터페이스를 정의하여 이 인터페이스는 Entities에 위치시키고 Authroizer에서는 이 인터페이스를 상속받아 구현한다. 이렇게 하면 의존성이 역전되고 순환을 끊을 수 있다.
- Entiteies와 Authorizer가 모두 의존하는 새로운 컴포넌트를 만들고 두 컴포넌트가 모두 의존하는 클래스들을 새로운 컴포넌트로 이동시킨다.
두번째 해결책에서 이야기 하는 바는 요구사항이 변함에 따라 컴포넌트 구조는 언제든 바뀔 수 있다는 점이며 이는 올바른 방향이라 볼 수 있다. 따라서 우리는 컴포넌트의 구조가 바뀔때 마다 순환이 발생하는 것을 잘 봐야 하며 순환은 어떻게든 끊어낼 수 있다.
우리가 지금까지 논의한 부분들을 살펴보면 결론적으로 컴포넌트 구조는 하향식으로 설계될 수 없다는 점을 알 수 있으며 시스템이 설계되고 변화되는 과정에서 변하는 것이지 가장 먼저 설계될 수 있는 대상이 아니다.
기능을 잘게 쪼개다보면 기능적으로 컴포넌트 단위로 표현이 가능할 것이라고 생각하지만 이는 컴포넌트 의존성 다이어 그램이 가진 속성은 보이지 않는다.
컴포넌트 의존성 다이어그램은 프로그램의 기능을 정의하는 것과는 상관 없고 "빌드 가능성"과 "유지보수성"을 보여주는 지도이다. 따라서 컴포넌트는 초기에 설계가 불가능 하다.(유지보수할 소프트웨어가 없으므로 유지보수를 고려가 불가능 함)
개발을 진행해 나가며 우리는 "숙취 증후군"을 최소화 하도록 개발하는 요구가 늘어날 것이며 변경사항이 가능한한 시스템의 작은 부분만이 변경 되기를 원할것이다. 그래서 결국 우리는 SRP와 CCP에 관심을 가지게 된다.
의존성 구조를 컴포넌트가 만약에 변경되더라도 그 여파를 최소화 하기위해 컴포넌트 구조를 지속적으로 손봐야 한다.
또한 어플리케이션이 계속 성장해 감에 따라 우리는 재사용성도 고려하게되며 CRP를 고려하게 된다.
그러다보면 다시 순환이 발생하게 되고 우리는 ADP가 적용되고 컴포넌트 구조는 흐트러지고 성장한다.
계속 이야기 하였지만 컴포넌트 구조는 계속적으로 변경되야 하며 변경이 생기더라도 그 변경에 대한 여파는 최소화 해야한다.
따라서 변경이 쉽지 않은 컴포넌트가 변동이 예상되는 컴포넌트에 의존하게 만들면 변동성이 큰 컴포넌트도 결국 변경이 어려워지게 된다. (의존이 되게 되면 내 코드를 변경하는 것이 많은 것을 고려해야 하게 된다.)
우리는 안정된 의존성 원칙(SDP)를 준수하면 어려운 모듈이 변경하기 쉽게 만들어진 모듈에 의존하지 않도록 만들 수 있다.
안정 성이란 변화가 발생하는 빈도와는 직접적인 관계가 없으며 변경이 발생 했을때 작업량을 최소화 하는데에 있다.
컴포넌트를 변경하기 힘들게 만드는 요소의 예는 다음과 같다.
- 컴포넌트의 크기
- 복잡도
- 간결함
- 등..
이 장에서는 저러한 것들은 배재하고 조금더 특이한 것에 집중하여 이야기 해보자.
컴포넌트 안쪽으로 들어오는 의존성이 많아지면 상당히 안정적이라고 볼 수 있는데 모든 컴포넌트를 만족시키기는 쉽지 않다.
위 그림의 X컴포넌트는 안정적인 컴포넌트 인데 X는 변경하지 말아야할 이유가 3가지나 있으며 X가 변경되도록 만드는 외적인 이유가 없다. 그러므로 X는 "독립적"이다.
위 사진의 Y는 상당히 불안한 컴포넌트인데 어떤 컴포넌트도 Y에 의존하지 않으므로 Y는 책임성은 없다. 그러나 Y는 의존중인 컴포넌트가 3개나 되기 때문에 변경을 야기할 수 있는 요소가 3가지나 된다고 볼 수 있다. 이 경우 Y는 "의존적"이라고 말한다.
컴포넌트의 안정성을 층정할 수 있는 방법은 숫자를 통해(들어오는 개수) 계산이 가능하다.
-
Fan-in : 안으로 들어오는 의존성. 이 지표는 컴포넌트 내부의 클래스에 의존하는 컴포넌트 외부 클래스를 의미
-
Fan-out : 바깥으로 나가는 의존성. 이 지표는 컴포넌트 외부의 클래스에 의존하는 컴포넌트 내부의 클래스 개수를 의미한다.
-
I(불안정성) : I = Fan-out/(Fan-in + Fan-out) I가 0에 가까울 수록 안정화된 컴포넌트라는 의미이다.
-
C++에서는 이러한 의존성을 #include로 이용해서 표현된다. 실제로 소스 파일이 클래스당 하나가 되도록 소스 코드를 구조화하면, I 지표는 정말 쉽게 계산할 수 있으며 Java는 import로 개수를 세어서 계산할 수 있다.
-
I 값이 1이면 어떤 컴포넌트도 해당 컴포넌트에 의존하지 않지만 해당 컴포넌트는 다른 컴포넌트에 의존한다. 이 컴포넌트는 의존하는 컴포넌트가 없으므로 변경하지 말아야할 이유가 없으며 반대로 이 컴포넌트는 다른 컴포넌트에 의해서 변경해야 될 이유가 존재한다. 즉, 불안정 적이다.
-
I 값이 0이면 어떤 컴포넌트는 해당 컴포넌트에 의존하지만 해당 컴포넌트는 다른 컴포넌트에 의존하지 않는다. 이 컴포넌트는 변경하지 말아야할 이유가 생기며, 다른 컴포넌트에 의해 변경해야 할 이유가 없다. 즉, 안정적이다.
-
SDP에서 컴포넌트의 I지표는 그 컴포넌트가 의존하는 다른 컴포넌트들의 I보다 커야 한다고 말한다. 즉, 의존성 방향으로 갈수록 I 지표 값이 감소해야 한다.
모든 컴포넌트가 최고로 안정적인 시스템이라면 그 시스템은 변경이 불가능하다.
우리의 소프트웨어는 늘 변경 가능한 상태여야 하기 떄문에 올바른 방향은 아니다.
아래 그림은 세 컴포넌트로 구성된 시스템이 가질 수 있는 이상적인 모양이다.
하지만 아래는 SDP가 어떻게 위배될 수 있는지를 보여준다.
Flexible 컴포넌트는 변경이 잦은 컴포넌트인데 Flexible에 의해서 Stable해야 할 컴포넌트도 흔들리는 모습을 보여준다.
우리는 이러면 문제가 생기기 때문에 이것을 끊어내야만 하는데 더 구체적인 예를 보자.
예를들어 위 사진 처럼 Stable 내부 클래스 U가 Flexible한 C 클래스를 사용한다고 가정해보자.
이를 DIP를 이용해서 문제를 해결할 수 있다.
중간에 US 인터페이스를 생성하고 UServer 컴포넌트에 넣는다. 이때 US 인터페이스에는 U가 사용하는 모든 메서드가 반드시 선언되어 있어야 한다.
두 컴포넌트는 반드시 US를 의존하도록 한다면 C는 Flexible한 불안정성을 유지할 수 있으며 모든 의존성은 다시 I가 감소하는 방향으로 향한다.
오로지 인터페이스만을 포함하는 컴포넌트를 생성하는 것은 이상해 보일 수 있다.
자바나 C#과 같은 정적 언어는 상당히 흔하게 사용하는 방법이며 루비나 파이썬 같은 동적 타입 언어를 사용할 때는 이러한 추상 컴포넌트가 전혀 존재하지 않을 뿐만 아니라 추상 컴포넌트로 향하는 의존성도 전혀 없기 때문에 의존성 역전이 더 쉽다.
시스템에는 자주 변경해서는 절대 안되는 소프트웨어도 있는데, 고수준 아키텍처나 정책 관련 소프트웨어가 그 예이다. (이러한 업무 로직이나 아키텍처와 관련된 결정에는 변동성이 없기를 기대한다.)
따라서, 고수준 정책을 나타내는 소프트웨어는 반드시 안정적인 컴포넌트에 위치해야 한다.
하지만, 고수준 정책을 안정된 컴포넌트에 위치시키면, 그 정책을 포함한 소스코드는 수정하기 어려워져 소스 코드를 수정하기 어렵게 된다.
따라서 우리는 안정적이며 변경에는 유연하게 대응하기 위해서는 OCP(개방 폐쇄 원칙)을 지켜야 한다. OCP를 이용하면 클래스를 수정하지 않고도 확장이 가능하도록 클래스를 유연하게 만들 수 있다. 이걸 어떻게 하느냐?
추 상 클 래 스
이 원칙은 안정화된 컴포넌트는 추상 컴포넌트여야 하며, 이를 통해 안정성이 컴포넌트를 확장하는 일을 방해해서는 안 된다고 말한다.
다른 한편으로는 불안정한 컴포넌트는 반드시 구체 컴포넌트여야 한다고 말한다. 왜냐면 컴포넌트가 불안정 하면 코드를 변경하기 쉬워야 하기 때문이다.
따라서
안정적인 컴포넌트는 반드시 인터페이스나 추상클래스로 구성되어 쉽게 확장되어야 한다
안정된 컴포넌트가 확장이 가능해지면 유연성을 얻게 되고 아키텍처를 과도하게 제약하지 않게 된다.
SAP와 SDP와 결합하면 컴포넌트에 대한 DIP나 마찬가지가 된다.
- SDP는 의존성이 반드시 안정성의 방향으로 향해야 한다고 말한다.
- SAP에서는 안정성이 결국 추상화를 의미한다고 말한다.
- 따라서 의존성은 추상화의 방향으로 향하게 된다.
(그러나 DIP는 클래스에 한정되었으며 추상이거나 구체인 중간이 없는 원칙이다.)
- A지표 : 컴포넌트의 추상화 정도를 측정한 값 이 값은 컴포넌트의 클래스 총 수 대비 인터페이스와 추상화 클래스의 개수를 단순히 계산한 값이다.
- NC : 컴포넌트의 클래스 개수
- NA : 컴포넌트의 추상 클래스와 인터페이스의 개수
- A: 추상화 정도, A = Na / Nc
안정성과 추상화 관계를 정의해야 한다. 아래 그래프 처럼 나타난다.
각 구간을 살펴보자.
이 컴포넌트는 매우 안정적이무 구체적이다. 하지만 굉장히 뻣뻣한 상태여서 바람직한 상태는 아니다. 따라서 제대로 설계 된 컴포넌트라면 (0,0) 근처는 없을것이며 이 구간은 배재해야한다.
하지만 데이터베이스의 스키마는 변동성이 심하지만 엄청나게 구체적이고 많은 부분들이 의존하고 있다. 그래서 변경하면 매우 고통스럽다.
또한, 구체적인 유틸 클래스 또한 매우 구체적이다. 그렇지만 이러한 라이브러리는 1에 가깝게 있더라도 변화가 대부분 없다.
예를들어 스트링 클래스의 경우 변화가 생기면 그 파급력이 엄청나게 크기 때문에 변화를 쉽게 하기 힘들다.
이 영역도 바람직 하지 않다. 가장 추상화 되어 있지만 어떤 컴포넌트도 이 컴포넌트에 의존적이지 않기 때문이다. 따라서 쓸모없는 영역이다.
변동성이 큰 컴포넌트 대부분은 두 배제 구역으로 부터 가능한한 멀리 떨어트려야 하면 각 배제구역으로 부터 멀리 떨어진 궤적을 주계열 구역이라고 부른다.
사실 가장 바람직한 지접은 주계열의 두 종점이다.
... 더 구체적인 지표는 생략
(각자 읽어보기)
좋은 의존성도 있고 좋지 않은 의존성도 존재한다. 이러한 것들을 판단하는데 지표는 도움이 되지만 지표가 절대적이지는 않다는 것을 알아야 한다.
하지만 이러한 지표를 적극적으로 활용하면 도움이 될 것이다.