소개
클린아키텍처: 소프트웨어 구조와 설계의 원칙 책을 읽고 정리하며 소감을 적는 포스트입니다.
컴포넌트 결합
지금 부터 다룰 세 가지 원칙은 컴포넌트 사이의 관계를 설명한다. 개발 가능성과 논리적 설계 사이의 균형을 다룬다.
컴포넌트 구조와 관련된 아키텍처를 침번하는 힘은 기술적이며, 정치적이고, 가변적이다.
ADP: 의존성 비순환 원칙
컴포넌트 의존성 그래프에 순환(cycle)이 있어서는 안 된다.
하루 종일 일해서 무언가를 작동하게 만들고 퇴근 했는데, 이튿날 전혀 돌아가지 않는 경험을 해본적 있는가? 누군가 당신보다 더 늦게까지 일하며 의존하고 있던 무언가를 수정했기 때문이다.
나는 이러한 현상을 숙취 증후군(the morning after syndrome)
이라고 부른다.
프로젝트의 규모가 커지면 커질 수록 이러한 숙취는 지독한 악몽이 될 수 있다. 누군가가 마지막에 수정한 코드 때문에 망가진 부분이 동작하도록 만들기 위해 코드를 수정하고 또 수정하는 작업만이 계속 된다.
해결책으로는 첫 번째 주 단위 빌드(weekly build)
이며 두 번째는 의존성 비순황 원칙(Acyclic Dependencies Principle, ADP)
이다.
주 단위 빌드(Weekly Build)
주 단위 빌드는 중간 규모의 프로젝트에서는 흔하게 사용된다. 모든 개발자가 일주일의 첫 4일 동안은 서로를 신경쓰지 않고 개발하며 금요일이 되면 변경된 코드를 모두 통합하여 시스템을 빌드한다.
이 접근법은 5일 중 4일 동안 개발자를 고립된 세계에서 살 수 있게 해주지만 금요일에 통합과 관련된 막대한 업보를 치러야 한다.
하지만 안타깝게도 프로젝트가 커지면 통합이 금요일 하루 만에 끝나지 않는다.
개발보다 통합에 드는 시간이 늘어나면서 팀의 효율서도 서서히 나빠진다.
순환 의존성 제거하기
이 해결책은 개발 환경을 릴리스 가능한 컴포넌트 단위로 분리하는 것이다. 이를 통해 컴포넌트는 개별 개발자 또는 단일 개발팀이 책임질 수 있는 작업 단위가 된다.
개발자가 해당 컴포넌트를 동작하도록 만든 후, 해당 컴포넌트를 릴리스하여 다른 개발자가 사용할 수 있도록 만든다.
컴포넌트가 새로 릴리스되어 사용할 수 있게 되면, 다른 팀에서는 새 릴리스를 당장 적용할지를 결졍해야 한다. 새 릴리스를 적용할 준비가 되었다는 판단이 들면 새 릴리스를 사용하기 시작하면 된다.
이 같은 작업 절차는 단순하며 합리적이지만 이 절차가 성공적으로 동작하려면 컴포넌트 사잉의 의존성 구조를 반드시 관리해야 한다. 의존성 구조에 순화이 있어서는 안된다.
전형적인 컴포넌트 다이어그램
위 그림의 중요한 점은 컴포넌트 간의 의존성 구조다. 이 구조가 방향 그래프(directed graph)엠에 주의하자. 컴포넌트는 정점(vertex)에 해당하고, 의존성 관계는 방향이 있는 간선(directed edge)에 해당한다.
또한 위 그림의 의존성 구조는 비순환 방향 그래프(Directed Acyclic Graph, DAG)이다.
Presenters 컴포넌트를 만드는 개바랒가 이 컴포넌트를 테스트하고자 한다면, 단순히 현재 사용 중인 버전의 Interactors와 Entities를 이용해서 Presenters 자체 버전을 빌드하면 그만이다. 이 빌드 과정에서 시스템의 나머지 컴포넌트는 전혀 관련이 없다.
시스템 전체를 릴리스 해야 할때가 온다면 릴릴스 절차는 상향식으로 진행하면 된다. Entities 컴포넌트를 컴파일하고, 테스트하고, 리리리스 한다. 그러고 나서 database와 Interactors에 대해서도 동일한 과정을 거친다.
이 처럼 구성요소 간 의존성을 파일하고 있으면 시스템을 빌드하는 방법을 알 수 있다.
순환이 컴포넌트 의존성 그래프에 미치는 영향
새로운 요구사항이 발생해서 Entities에 포함된 클래스 하나가 Authorizer에 포함된 클래스 하나를 사용하도록 변경할 수밖에 없다고 가정해 보다.
순환 의존성
위와 같이 순환 의존성(dependency cycle)이 발생하며 이 순환은 즉각적인 문제를 일으킨다.
예를 들어 Database 컴포넌트를 만드는 개발자가 릴리스하려면 Entities 컴포넌트와 반드시 호환되너야 한다느 사실을 알고 있다. 하지만 Entitites 컴포넌트에는 순환이 있으므로, Database 컴포넌트는 또한 Authorizer와도 호환되어야 한다.
이 말은 결국 개발자들은 모두, 이들 컴포넌트 중 어느 것을 개발하더라도 숙취 증후군
에 떠는 경험을 하게 될 것이다.
의존성 그래프에 순환이 생기면 컴포넌트를 어떤 순서로 빌드해야하 하는지 파악하기 힘들어진다.
순환 끊기
컴포넌트 사이의 순환을 끊고 의존성을 다시 DAG로 원상복구 하는 일은 언제라도 가능하다. 아래의 두 가지 메커니즘을 살펴 보자.
- 의존성 역전 원칙(DIP)를 적용한다. User가 필요한 메서드를 제공하는 인터페이스를 생성한다. 그리고 이 인터페이스는 Entities에 위치시키고, Authorizer에서는 이 인터페이스를 상속받는다. 이렇게 하면 Entities와 Authorizer 사이의 의존성을 역전시킬 수 있고, 이를 통해 순환을 끊을 수 있다.
Entities와 Authorizer 사이의 의존성을 역전 시킨다.
- Entities와 Authorizer가 모두 의존하는 새로운 컴포넌트를 만든다. 그리고 두 컴포넌트가 모두 의존하는 클래스드릉르 새로운 컴포넌트로 이동 시킨다.
Entities와 Authorizer 모두가 의존하는 새로운 컴포넌트
흐트러짐(Jitters)
두 번째 해결책에서 시사하는 바는 요구사항이 변경되면 컴포넌트 구조도 변경 될 수 있다는 사실이다. 실제로 애플리케이션이 성장함에 따라 컴포넌트 의존성 구조는 서서히 흐트러지며 또 성장한다.
따라서 의존성 구조에 순환이 발생하는지를 항상 관찰해야 한다. 순환이 발생하면 어떤 식으로든 끊어야 한다.
이 말은 때로 새로운 컴포넌트를 생성하거나 의존성 구조가 더 커질 수 있음을 의미한다.
하향식(top-down) 설계
지금까지 논의로 우리는 피할 수 없는 결론에 다다른다. 즉, 컴포넌트 구조는 하향식으로 설계 될 수 없다. 컴포넌트는 시스템에서 가장 먼저 설계할 수 있는 대상이 아니며, 오히려 시스템이 성장하고 변경 될 때 까지 함께 진화한다.
사실 컴포넌트 의존성 다이어그램은 애플리케이션의 기능을 기술하는 일과는 거의 관련이 없다. 그렇게 때문에 컴포넌트 구조는 프로젝트 초기에 설계할 수 없다. 빌드하거나 유지보수할 소프트웨어가 없다면 빌드와 유지보수에 관한 지도 또한 필요없기 때문이다.
하지만 프로젝트를 개발하기 위해서 의존성 관리에 대한 요구가 점처 늘어나게 되며 변경되는 범위가 시스템의 가능한 한 작은 일부로 한정되기를 원한다.
결국 단일 책임 원칙(SRP)과 공통 폐쇄 원칙(CCP)에 관심을 갖기 시작하고, 이를 적용해 함께 변경되는 클래스는 같은 위치에 배치되도록 만든다.
의존성 구조와 관련된 최우선 관심사는 변동성을 격리하는 일이다. 컴포넌트 의존성 그래프는 자주 변겨오디는 컴포넌트로부터 안정적으며 가치가 높은 컴포넌트를 보호하려는 아키텍트가 만들고 가다듬게 된다.
아직 아무런 클래스도 설계하지 않은 상태에서 컴포넌트 의존성 구조를 설계하려고 시도한다면 상단히 큰 실패를 맛볼 수 있다.
SDP : 안정된 의존성 원칙
안정성의 방향으로 (더 안정된 쪽에) 의존하라
설계는 결코 정적일 수 없다. 설계를 유지하다 보면 변경이 불가피하다. 공통 폐쇄 원칙을 준수함으로써, 컴포넌트가 다른 유형의 변경에는 영향을 받지 않으면서도 특정 유형의 변경에만 민감하게 만들 수 있다.
우리는 변동성을 지니도록 설계한 컴포넌트는 언젠가 변경되리라고 예상한다.
즉, 당신이 모듈을 만들때는 변경하기 쉽도록 설계했지만, 이 모듈에 누군가가 의존성을 매달아 버리면 당신의 모듈도 변경하기 어려워진다.
안정된 의존성 원칙(Stable Dependencies Principle, SDP)을 준수하면 변경하기 어려운 모듈이 변경하기 쉽게 만들어진 모듈에 의존하지 않도록 만들 수 있다.
안정성
안정성(stability)
는 무슨 뜻인가? 안정성은 변화가 발생하는 빈도와는 직접적인 관련이 없다.
소프트웨어 컴포넌트를 변경하기 어렵게 만드는 데는 많은 요인이 존재하며, 그 예로는 컴포넌트의 크기, 복잡도, 간결함 등을 들 수 있다.
소프트웨어 컴포넌트를 변경하기 어렵게 만드는 확실한 방법 하나는 수많은 다른 컴포넌트가 해당 컴포넌트에 의존하게 만드는 것이다.
아래의 그램에서 X 컴포넌트는 안정된 컴포넌트다. 세 컴포넌트가 X에 의존하며, 따라서 X 컴포넌트는 변경하지 말아야 할 이유가 세 가지나 된다.
X는 안정된 컴포넌트다.
아래의 그림의 Y는 상당히 불안정한 컴포넌트다. 어떤 컴포넌트도 Y에 의존하지 않으므로 Y는 책임성이 없다고 말할 수 있다. 또한 Y는 세 개의 컴포넌트에 의존하므로 변경이 발생할 수 있는 외부 요인이 세 가지다
Y는 상당히 불안정한 컴포넌트다.
안정성 지표
컴포넌트의 안정성을 측정하는 방법은 의존성의 개수를 세어 보는 방법이 있다. 이 숫자를 통해 컴포넌트가 위치상(positional) 어느 정도의 안정성을 가지는지 계산할 수 있다.
- Fan-in : 안으로 들어오는 의존성
- 의존하는 외부 컴포넌트의 클래스 갯수
- Fan-out : 바깥으로 나가는 의존성
- 외부 클래스에 의존하는 컴포넌틑 내부의 클래스 개수
- I(불안정성) : I = Fan-out % (Fan-in + Fan-out).
- I = 0이면 최고로 안정된 컴포넌트
- I = 1이면 최고로 불안정한 컴포넌트
SDP에서 컴포넌트의 I 지표는 그 컴포넌트가 의존하는 다른 컴포넌트들의 I보다 커야 한다고 말한다.
즉, 의존성 방향이 갈수록 I 지표 값이 감소해야 한다.
모든 컴포넌트가 안정적이여야 하는 것은 아니다.
모든 컴포넌트가 최고로 안정적인 시스템은 변경이 불가능하다. 사실 우리가 컴포넌트 구조를 설계할 때 기대하는 것은 불안정한 컴포넌트도 있고 안정된 컴포넌트도 존재하는 상태다.
세 컴포넌트로 구성된 시스템이 이상적인 구조
다이어그램에서 불안정한 컴포넌트를 관례적으로 위쪽에 두는데, 이 관례를 따르면 상당히 유용하다. 위로 향하는 화살표가 있으면 SDP를 위배하는 상태가 되기 때문이다.
SDP 위배
Flexible은 변경하기 쉽도록 설계한 컴포넌트다. 하지만 Stable 컴포넌트에서 작업하던 개발자가 Flexible에 의존성을 걸게 되면 이로 인해 SDP를 위배하는데, 결국 Flexible은 변경하기 어렵게 된다.
수정 된 그림
위 그림과 같이 불안정한 컴포넌트를 위에 배치하면 한눈에 알아보기 쉽다.
위와 같은 문제를 해결하려면 Stable의 Flexible에 대한 의존성은 어떤 식으로든 끊어야한다. 이 의존성은 무슨 이유로 존재하는가? Stable 내부의 클래스 U가 Flexible 내부의 클래스 C를 사용한다고 가정해보자.
Stable 내부의 클래스 U가 Flexible 내부의 클래스 C를 사용한다.
DIP를 도입하면 이 문제를 해결할 수 있다.
C는 US 인터페이스를 구현한다.
추상 컴포넌트
오로지 인터페이스만을 포함하는 컴포넌트(위 예제의 UServer)를 생성하는 방식이 이상하게 보일 수도 있다.
하지만 C#이나 Java 같은 정적 타입 언어를 사용할 때 이 방식은 상당히 흔할 뿐만 아니라, 꼭 필요한 전략이다.
이러한 추상 컴포넌트는 상당히 안정적이며, 따라서 덜 안정적인 컴포넌트가 의존할 수 있는 이상적인 대상이다.
SAP: 안정된 추상화 원칙
컴포넌트는 안정된 정도만큼만 추상화되어야 한다.
고수준 정책을 어디에 위치시켜야 하는가?
시스템에서 자주 변경시켜서는 안되는 소프트웨어도 있다. 고수준 아키텍처나 정책결정과 관련된 소프트웨어가 그 예다.
이처럼 업무 로직이나 아키텍처와 관련된 결정에는 변동성이 없기를 기대한다.
하지만 고수준 정책을 안정된 컴포넌트에 위치시키면, 그 정책을 포함하는 소스 코드는 수정하기 어려워진다. 이로 인해 시스템 전체 아키텍처가 유연성을 잃는다.
해답은 개방 폐쇄 원칙(OCP)에서 착을 수 있다. OCP는 클래스를 수정하지 않고도 확장이 충분히 가능할 정도로 클래스를 유연하게 만들 수 있다.
어떤 클래스가 이 원칙을 준수하는가? 바로 추상(abstract) 클래스다.
안정된 추상화 원칙
안정된 추상화 원칙(Stable Abstractions Principle, SAP)는 안정성과 추상화 정도 사이의 관계를 정의한다.
이 원칙은 한편으로는 안정된 컴포넌트는 추상 컴포넌트여야 하며, 이를 통해 안정성이 컴포넌트를 확장하는 일을 방해해서는 안 된다고 말한다. 다른 한편으로는 불안정한 컴포넌트는 반드시 구체 컴포넌트여야 한다고 말하는데, 컴포넌트가 불안정하므로 컴포넌트 내부의 구체적인 코드를 쉽게 변경할 수 있어야 하기 때문이다.
따라서 안정적인 컴포넌트라면 반드시 인터페이스와 추상 클래스로 구성되어 쉽게 확장할 수 있어야 한다.
SAP와 SDP를 결합하면 컴포넌트에 대한 DIP나 마찬가지가 된다. 실제로 SDP에서는 의존성이 반드시 안정성의 방향으로 향해야 한다고 말하며, SAP에서는 안정성이 결국 추상화를 의미한다고 말하기 때문이다. 따라서 의존성은 추상화의 방향으로 향하게 된다.
하지만 DIP는 클래스에 대한 원칙이며, 클래스의 경우 중간은 존재하지 않는다.
즉, 클래스는 추상적이거나 아니거나 둘 중 하나다. SDP와 SAP의 조합은 컴포넌트에 대한 원칙이며, 컴포넌트는 어떤 부분은 추상적이면서 다른 부분은 안정적일 수 있다.
추상화 정도 측정하기
A 지표는 컴포넌트의 추상화 정도를 측정한 값이다. 이 값은 컴포넌트의 클래스 총 수 대비 인터페이스와 추상 클래스의 개수를 단순히 계산한 값이다.
- Nc: 컴포넌트의 클래스 개수
- Na: 컴포넌트의 추상 클래스와 인터페이스의 개수
- A: 추상화 정도. A = Na % Nc
A 지표는 0과 1 사이의 값을 갖는다. A가 0이면 컴포넌트에는 추상 클래스가 하나도 없다는 뜻이다. A가 1이면 컴포넌트는 오로지 추상 클래스만을 포함한다는 뜻이다.
주계열
아래의 그래프는 안정성(I)과 추상화 정도(A)사이의 관계를 정의한다.
A/I 그래프
최고로 안정적이며 추상화된 컴포넌트는 좌측 상단이 (0,1)에 위치하고 최고로 불안정하고 구체화된 컴포넌트는 우측하단(1,0)에 위치한다.
모든 컴포넌트가 좌측 상단, 우측 하단에 위치 할 수 없음으로 합리적인 지점을 정의하는 점의 궤적이 있으리라고 가정해 볼 수 있다.
이 궤적은 컴포넌트가 절대로 위치해서는 안 되는 영역, 다시 말해 배제할 구역(Zone of Exclusion)을 찾는 방식으로 추론할 수 있다.
배제 구역(Zone of Exclusion)
고통의 구역
(0,0) 주변의 컴포넌트는 매우 안정적이며 구체적이다. 이는 바람직한 상태가 아닌데, 뻣뻣한 상태이기 때문이다.
추상적이지 않아 확장할 수 없고, 안정적이므로 변경하기 상당히 어렵다.
사실 일부 소프트웨어 엔티티는 고통의 구역에 위치하곤 한다. 데이터베이스 스키마가 한 예이다. 스키마는 변동성이 높기로 악명이 높다.
또 다른 예는 구체적인 유틸리티 라이브러리를 들 수 있다. 하지만 이러한 라이브러리는 I 지표가 1일지라도, 실제로 변동성이 거의 없다. 예를 들어 String 컴포넌트이다.
변동성이 거의 없는 컴포넌트는 (0,0) 구역에 위치했더라도 해롭지 않다. 변동 될 가능성이 거의 없기 때문이다.
쓸모없는 구역
(1,1) 주변의 컴포넌트는 추상적이지만, 누구도 그 컴포넌트에 의존하지 않는다. 이는 쓸모가 없는 컴포넌트다.
이 영역에 존재하는 엔티티는 폐기물과도 같다. 엔티티는 누구도 구현하지 않은 채 남겨친 추상 클래스인 경우가 많다.
배제 구역 벗어나기
변동성이 큰 컴포넌트 대부분은 두 배제 구역으로부터 가능한 한 멀리 떨어뜨려야 한다.
이 선분이 주계열(Main Sequence)라고 부르며, 주계열에 위치한 컴포넌트는 자신의 안정성에 비해 너무 추상적
이지도 않고, 추상화 정도에 비해 너무 불안정
하지도 않다.
컴포넌트가 위칳라 수 있는 가장 바람직한 지점은 주계열의 두 종점이다.
주계열과의 거리
컴포넌트가 주계열 바로 위에, 또는 가까이 있는 것이 바람직하다면, 이 같은 이상적인 상태로부터 컴포넌트가 얼마나 멀리 떨어져 있는지 측정하는 지표를 말들어 볼 수 있다.
D: 거리. D = A + I - 1 - 유효범위는 [0,1]
D가 0이면 컴포넌트가 주계열 바로 위에 위치하고, 1이면 주계열로부터 가장 멀리 위치한다.
지표를 사용하면 컴포넌트를 재검토 후 재구성 할 수 있고 통계적으로 분석하는 일 또한 가능해진다.
모든 컴포넌트에 대해 D 지표의 평균과 분산을 구한다. 분산은 관리 한계(Control limit)
를 결정하기 위한 목적으로 사용할 수 있고, 분산을 통해 다른 컴포넌트에 비해 극히 예외적인
컴포넌트를 식별 할 수 있다.
아래의 그림에서 보듯이 일부 컴포넌트의 표준편차가 1(Z=1)인 영역을 벗어나 있다.
이처럼 이상한 컴포넌트는 좀 더 면밀히 검토해 볼 가치가 있다. 이들 컴포넌트는 자신에게 의존하는 컴포넌트가 거의 없는데도 너무 추상적이거나, 자신에게 의존하는 컴포넌트가 많은데도 너무 구체적일 것이다.
컴포넌트 산점도
결론
의존성 관리 지표는 설계의 의존성과 추상화 정도가 내가 훌륭한
패턴이라고 생각하는 수준에 얼마나 잘 부합하는지를 측정한다.
하지만 지표는 그저 임의로 결정된 표준을 기초로 한 측정값에 지나지 않는다. 이러한 지표는 아무리 해도 불완전하다. 하지만 이들 지표로부터 무언가 유용한 것을 찾을 수 있기를 바란다.