hexdrinker

컴포넌트 결합

-10 min read

소프트웨어 공학에서 결합도는 모듈 간의 상호 정도, 두 모듈이 얼마나 밀접하게 연결되어있는지를 나타내는 척도이다. 결합도는 보통 응집도와 대비된다.

컴포넌트 결합도와 응집도 간의 관계
컴포넌트 결합도와 응집도 간의 관계

컴포넌트 결합에서 다룰 세 가지 원칙은 컴포넌트 사이의 관계를 말한다. 마찬가지로 개발 가능성과 논리적 설계 사이의 균형을 다룬다.

  • ADP: 의존성 비순환 원칙 (Acyclic Dependencies Principle)
  • SDP: 안정된 의존성 원칙 (Stable Dependencies Principle)
  • SAP: 안정된 추상화 원칙 (Stable Abstractions Principle)

ADP: 의존적 비순환 원칙

컴포넌트 의존성 그래프에 순환(cycle)이 있어서는 안 된다.

많은 개발자가 있는 팀에서는 동일한 소스 파일을 수정하는 경우에, 오늘 잘 되던 동작이 의존성 문제로 내일은 안 되는 것을 종종 겪을 수 있다.

소수의 개발자로 구성된 상대적으로 작은 프로젝트에서는 이것이 큰 문제가 되지 않는다.

하지만 프로젝트와 개발팀의 규모가 커지면 이 문제는 지독한 문제가 될 수 있다. 해결책은 주 단위 빌드와 의존성 비순환 원칙이다.

주 단위 빌드(Weekly Build)

주 단위 빌드는 중간 규모의 프로젝트에서 흔하다. 마지막 날에 각자의 코드를 통합하여 시스템을 빌드한다.

프로젝트가 커지면 통합은 금요일 하루만에 끝내는 게 불가능해진다. 그러면서 통합일이 앞당겨지게 되고 개발보다 통합에 더 많이 시간을 쏟으며 팀의 효율성이 서서히 나빠진다.

순환 의존성 제거하기

해결책은 개발 환경을 릴리스 가능한 컴포넌트 단위로 분리하는 것이다. 컴포넌트는 단일 개발자 또는 단일 팀이 책임질 수 있는 단위가 된다.

개발자가 특정 컴포넌트를 작업 후 릴리스하여 다른 개발자가 사용할 수 있도록 한다. 나머지 개발자들은 상황에 맞춰서 릴리스된 컴포넌트를 쓸 수도 있고 과거의 릴리스를 계속 사용할 수도 있다.

따라서 어떤 팀도 다른 팀에 의해 좌우되지 않는다. 특정 컴포넌트가 변경되어도 다른 컴포넌트에 즉각 영향을 주지 않는다.

하지만 이 절차가 성공적으로 동작하려면 컴포넌트 사이의 의존성 구조를 반드시 관리해야 한다. 의존성 구조에 순환이 있어서는 안 된다.

전형적인 컴포넌트 다이어그램
전형적인 컴포넌트 다이어그램

컴포넌트 간의 의존성 구조를 보자. 이 구조가 방향 그래프(directed graph)임에 주의하라.

잘 보면 눈치 챘겠지만, 어느 컴포넌트에서 시작하더라도 의존성 관계를 따라가면 최초 컴포넌트로 돌아갈 수 없다는 사실이다. 이 구조는 순환이 없는 비순환 방향 그래프(Directed Acyclic Graph, DAG)이다.

Presenters를 담당하는 팀에서 새로운 릴리스를 만들면 ViewMain에서 영향을 받는다. 하지만 각 팀의 개발자들은 새 릴리스와 본인의 작업을 언제 통합할지 결정하기만 하면 된다.

또한 Main은 새로 릴리스 되더라도 나머지 컴포넌트들이 Main에 대해서 알지 못하므로 크게 문제되는 것이 없다.

Presenters 컴포넌트를 만드는 개발자가 테스트를 원한다면, 현재 사용 중인 버전의 InteractorsEntities를 이용해서 Presenters 자체 버전을 빌드하면 그만이다. 이 과정은 다른 컴포넌트와는 전혀 상관 없는 일이다.

시스템 전체를 릴리스해야 할 때가 오면 릴리스 절차는 상향식으로 진행된다. Entities를 컴파일하고, 테스트하고, 릴리스한다. 그리고 Database, Interactors도 동일하게 진행한다. Main은 마지막에 처리한다.

순환이 컴포넌트 의존성 그래프에 미치는 영향

새로운 요구사항이 발생하여 Entities에 포함된 클래스 하나가 Authorizer에 포함된 클래스 하나를 사용하도록 변경할 수 밖에 없다면 아래와 같이 순환 의존성(dependency cycle)이 발생한다.

순환 의존성
순환 의존성

이 순환은 즉각적으로 영향을 미친다. Database 컴포넌트가 새로 릴리스되려면 Entities와 반드시 호환되어야 한다. 하지만 Entities엔 순환 의존성이 존재하여 Authorizer, Interactors 또한 함께 호환되어야 한다.

이로 인해 Entities, Authorizer, Interactors는 하나의 거대한 컴포넌트가 되고 Database`는 릴리스하기 훨씬 더 어려워진다.

하지만 이것은 겪게 될 문제 중 일부이다. Entities 컴포넌트를 테스트 하려면 AuthorizerInteractors까지 반드시 빌드하고 통합해야 한다.

이처럼 순환이 생기면 컴포넌트를 분리하기가 상당히 어려워진다. 단위 테스트를 하고 릴리스 하는 일도 굉장히 어려워지며 에러도 쉽게 생긴다. 게다가 모듈의 수가 많아짐에 따라 빌드 관련 이슈는 기하급수적으로 증가한다.

순환 끊기

컴포넌트 사이의 순환을 끊고 의존성을 비순환 방향 그래프로 원상복구하는 일은 언제라도 가능하다.

  1. 의존성 역전 원칙(DIP)
Entities와 Authorizer 사이의 의존성을 역전시킨다.
Entities와 Authorizer 사이의 의존성을 역전시킨다.

User가 필요로 하는 메소드를 인터페이스를 생성한다. 그리고 이 인터페이스를 Entities에 두고 Authorizer는 이 인터페이스를 상속받는다. 이렇게 하면 EntitiesAuthorizer의 의존성을 역전시켜 순환을 끊을 수 있다.

  1. 새 컴포넌트
Entities와 Authorizer 모두가 의존하는 새로운 컴포넌트
Entities와 Authorizer 모두가 의존하는 새로운 컴포넌트

EntitiesAuthorizer가 모두 의존하는 새 컴포넌트를 만든다. 그리고 두 컴포넌트가 모두 의존하는 클래스들을 새로운 컴포넌트로 이동시킨다.

흐트러짐

두 번째 해결책에서 시사하는 바는 요구사항이 변경되면 컴포넌트 구조도 변경될 수 있다는 사실이다.

애플리케이션이 성장함에 따라 컴포넌트 의존성 구조는 서서히 흐트러지며 또 성장한다. 따라서 의존성 구조에 순환이 발생하는지 항상 관찰해야 한다.

하향식 설계

지금까지의 논의로 우리는 하나의 결론에 도달한다.

컴포넌트 구조는 하향식으로 설계될 수 없다.

컴포넌트는 시스템에서 가장 먼저 설계할 수 있는 대상이 아니며 시스템의 성장과 변화와 함께 진화한다. 컴포넌트 의존성 다이어그램은 애플리케이션의 빌드 가능성(buildability)유지보수성(maintainability) 을 보여주는 지도(map)와 같다.

이러한 이유 때문에 컴포넌트 구조는 프로젝트 초기에 설계할 수 없다. 빌드하거나 유지보수할 소프트웨어가 없다면 그에 관한 지도도 필요 없기 떄문이다.

프로젝트 초기에 모듈이 쌓이면서 의존성 관리에 대한 요구가 늘어난다. 그러면서 단일 책임 원칙(SRP)와 공통 폐쇄 원칙(CCP)에 관심을 갖게 된다.

의존성 구조와 관련된 최우선 관심사는 변동성을 격리하는 일이다. 컴포넌트 의존성 그래프는 자주 변경되는 컴포넌트로부터 안정적이며 가치가 높은 컴포넌트를 보호하려는 아키텍트가 만들고 가다듬게 된다.

아무 클래스도 설계하지 않은 상태에서 컴포넌트 의존성 구조를 설계하려고 시도한다면 큰 실패를 겪을 수 있다. 재사용 가능한 요소도 알지 못하며, 컴포넌트 생성 시 거의 확실히 순환 의존성이 발생할 것이다. 따라서 컴포넌트 의존성 구조는 시스템의 논리적 설계에 발맞춰 성장해야 한다.

SDP: 안정된 의존성 원칙

안정성의 방향으로 의존하라

설계는 결코 정적일 수 없다. 설계를 유지하다 보면 변경은 불가피하다. 공통 폐쇄 원칙(CCP)를 준수함으로써, 컴포넌트가 다른 유형의 변경에 영향받지 않으면서 특정 유형의 변경에만 민감하게 만들 수 있다.

이처럼 컴포넌트 중 일부는 변동성을 지니도록 설계된다. 우리는 그 컴포넌트가 언젠가는 변경되리라 예상한다.

변경이 쉽지 않은 컴포넌트가 변동이 예상되는 컴포넌트에 의존하게 만들어서는 절대로 안 된다. 한번 의존하게 되면 변동성이 큰 컴포넌트가 움직이기 쉽지 않아진다.

안정성

무언가가 안정적이라는 말은 웹스터 사전에서는 '쉽게 움직이지 않는'이라고 정의한다.

소프트웨어 컴포넌트를 변경하기 어렵게 만드는 데는 많은 요인이 존재하나 확실한 방법은 수많은 다른 컴포넌트가 해당 컴포넌트에 의존하게 만드는 것이다.

컴포넌트 안쪽으로 들어오는 의존성이 많아지면 상당히 안정적이라 볼 수 있는데, 사소한 변경이라도 의존하는 모든 컴포넌트를 만족시키면서 변경하려면 상당한 노력이 필요하기 때문이다.

안정적인 컴포넌트
X는 안정적인 컴포넌트다.

세 컴포넌트가 X에 의존하여 X 컴포넌트는 변경하지 말아야 할 이유가 세 가지나 된다. 이 경우 X는 세 컴포넌트를 책임진다(responsible) 라고 한다.

반면 X는 어디에도 의존하지 않으므로 X가 변경되도록 만들 수 있는 외적인 영향이 없다. 이 경우 X는 독립적이다(independent) 라고 한다.

불안정적인 컴포넌트
Y는 상당히 불안정적인 컴포넌트다.

Y는 상당히 불안정한 컴포넌트다.

어떤 컴포넌트도 Y에 의존하지 않으므로 Y는 책임성이 없다고 할 수 있다. 또한 Y는 세 가지 컴포넌트에 의존하므로 변경이 발생할 요인이 세 가지다. 이 경우 Y는 의존적이라고 한다.

안정성 지표

컴포넌트로 들어오고 나가는 의존성의 개수를 통해 컴포넌트가 위치상(positional) 어느 정도 안정성을 갖는지 계산할 수 있다.

  • Fan-in: 안으로 들어오는 의존성. 컴포넌트 내부 클래스에 의존하는 컴포넌트 외부 클래스 개수.
  • Fan-out: 바깥으로 나가는 의존성. 컴포넌트 외부 클래스에 의존하는 컴포넌트 내부 클래스 개수.
  • I(불안정성): I = Fan-out ÷ (Fan-in + Fan-out). [0, 1] 범위의 값을 가지며 0에 가까울수록 안정된 컴포넌트, 1에 가까울수록 불안정한 컴포넌트이다.
예제
예제

모든 컴포넌트가 안정적이어야 하는 것은 아니다

모든 컴포넌트가 최고로 안정적인 시스템이라면 변경이 불가능하다. 이는 바람직하지 않다.

컴포넌트 구조를 설계할 때 기대하는 것은 불안정한 컴포넌트와 안정된 컴포넌트도 존재하는 상태다.

세 컴포넌트로 구성된 시스템의 이상적인 구성
세 컴포넌트로 구성된 시스템의 이상적인 구성

위쪽에는 변경 가능한 컴포넌트가 보이고, 아래의 안정된 컴포넌트에 의존한다.

SDP 위배
SDP 위배

Flexible은 변경하기 쉽도록 설계한 컴포넌트다. 그래서 불안정하기를 기대한다. 하지만 Stable 컴포넌트가 Flexible에 의존성을 걸게 되어 결국 Flexible의 변경이 어려워졌다.

Stable 내부의 클래스 U가 Flexible 내부의 클래스 C를 사용한다.
Stable 내부의 클래스 U가 Flexible 내부의 클래스 C를 사용한다.

위와 같은 상황을 가정해보자. 이 문제를 해결하려면 반드시 StableFlexible에 대한 의존성을 끊어야 한다.

DIP를 도입하면 이 문제를 해결할 수 있다.

C는 US 인터페이스를 구현한다.
C는 US 인터페이스를 구현한다.

US 라는 인터페이스를 생성하여 UServer 컴포넌트에 넣는다. US 인터페이스는 U가 사용하든 모든 메소드가 반드시 선언되어 있어야 한다.

그리고 C가 해당 인터페이스를 구현하도록 한다. 이를 통해 StableFlexible에 대한 의존성을 끊을 수 있다.

추상 컴포넌트

오로지 인터페이스만 포함하는 UServer와 같은 컴포넌트를 생성하는 게 이상해 보일 수도 있다. 하지만 자바나 C#같이 정적 타입 언어를 사용할 때 이와 같은 방식은 상당히 흔하고 꼭 필요한 전략이다.

이러한 추상 컴포넌트는 상당히 안정적이며, 따라서 덜 안정적인 컴포넌트가 의존할 수 있는 이상적인 대상이다.

동적 타입 언어를 사용할 때는 이런 추상 컴포넌트가 전혀 존재하지 않을 뿐더러 추상 컴포넌트로 향하는 의존성 같은 것도 없다. 이들 언어에서 의존성 구조는 훨씬 단순한데, 의존성을 역전시킬 때 인터페이스를 선언하거나 상속 받는 일이 전혀 필요하지 않기 때문이다.

SAP: 안정된 추상화 원칙

컴포넌트는 안정된 정도만큼만 추상화되어야 한다.

고수준의 정책을 어디에 위치시켜야 하는가?

시스템에서 고수준 정책을 캡슐화하는 소프트웨어는 반드시 안정된 컴포넌트(I = 0)에 위치해야 한다. 불안정한 컴포넌트(I = 1)은 반드시 변동성이 큰 소프트웨어, 즉 쉽고 빠르게 변경할 수 있는 소프트웨어만을 포함해야 한다.

하지만 고수준 정책을 안정된 컴포넌트에 위치시키면 그 정책을 포함하는 코드는 수정이 어려워지고 시스템 전체 아키텍처가 유연성을 잃게 된다.

컴포넌트가 최고로 안정된 상태이면서도 변경에 충분히 대응할 수 있을 정도로 유연하게 만드는 방법은 개방 폐쇄 원칙(OCP)에서 찾을 수 있다.

안정된 추상화 원칙

안정된 추상화 원칙(SAP)는 안정성(stability)와 추상화 정도(abstractness) 사이의 관계를 정의한다.

이 원칙은 안정된 컴포넌트는 추상 컴포넌트여야 하며, 안정된 컴포넌트를 확장하는 일을 방해해서는 안 된다고 한다. 반면 불안정한 컴포넌트는 반드시 구체 컴포넌트여야 한다고 말하는데, 불안정하므로 내부의 구체적인 코드를 쉽게 변경할 수 있어야 하기 때문이다.

따라서 안정적인 컴포넌트라면 반드시 인터페이스와 추상 클래스로 구성되어 쉽게 확장할 수 있어야 한다.

SAP와 SDP를 결합하면 컴포넌트에 대한 DIP나 마찬가지다. 의존성은 반드시 안정성의 방향으로 향해야 하며, 안정성은 추상화를 의미한다고 하기 때문에 의존성은 추상화의 방향으로 향하게 된다.

추상화 정도 측정하기

  • Nc: 컴포넌트의 클래스 개수
  • Na: 컴포넌트의 추상 클래스와 인터페이스의 개수
  • A: 추상화 정도, A = Na ÷ Nc

A 지표는 컴포넌트의 추상화 정도를 계산한 값으로 0에서 1 사이의 값을 갖는다. 0에 가까울수록 구체에 가깝고 1에 가까울수록 추상에 가깝다.

주계열

안정성(I)과 추상화 정도(A) 사이의 관계를 정의해보자. 수직축에는 A를 수평축에는 I를 나타내는 그래프를 그린다. 대체로 컴포넌트는 추상화와 안정화의 정도가 다양해서 극단에 위치하는 경우는 많지 않다.

A/I 그래프
A/I 그래프

A/I 그래프 상에서 컴포넌트가 위치할 수 있는 합리적인 지점을 정의하는 궤적이 있는데 이를 **주계열(Main Sequence)**이라 한다. 그리고 컴포넌트가 절대 위치하면 안되는 **배제 구역(Zone of Exclusion)**이 있다.

고통의 구역

(0, 0) 주변의 컴포넌트는 매우 안정적이며 구체적이다. 추상적이지 않으므로 확장할 수 없고 안정적이라 변경도 어렵다.

따라서 제대로 설계된 컴포넌트라면 (0, 0) 근처에 위치하지 않을 거라고 보는 게 일반적이다. 이 부근을 고통의 구역(Zone of Pain)이라고 한다.

대표적으로 데이터베이스 스키마가 있는데, 스키마 변경이 대체로 어려운 것을 생각하면 고통스럽다는 것이 과언은 아니다. 또한 유틸리티 라이브러리가 이곳에 위치하는데 변동성이 거의 없는 컴포넌트다. 변동성이 없는 컴포넌트는 (0, 0)에 위치하더라도 문제가 없다.

여기서 문제가 되는 것들은 변동성이 있는 것들인데 변동성이 크면 클수록 수반되는 고통이 크다.

쓸모없는 구역

(1, 1) 주변의 컴포넌트는 아주 추상적이면서 그 어떤 컴포넌트에 의존하지도 않는다. 그래서 쓸모가 없다.

누구도 구현하지 않은 채 남격진 추상 클래스인 경우가 있을 수 있다.

배제 구역 벗어나기

각 배제 구역으로 최대한 멀리 떨어진 궤적은 (0, 1)과 (1, 0)을 잇는 선분이다. 이를 주계열이라 한다.

주계열에 위치한 컴포넌트는 안정성에 비해 너무 추상적이지도 않고, 추상화 정도에 비해 너무 불안정하지도 않다.

컴포넌트가 위치할 수 있는 가장 바람직한 지점은 주계열의 두 종점이다. 하지만 소수의 컴포넌트를 제외하고 완벽히 추상적이거나 완벽히 안정적일 수 없다. 그래서 대체로 주계열 위에 또는 가깝게 위치할 때 이상적이다.

주계열과의 거리

D: 거리. D = |A + I - 1|

이 지표의 유호범위는 [0, 1]이다. D가 0이면 컴포넌트가 주계열 바로 위에 위치한다는 뜻이고 1이면 주계열로부터 가장 멀리 위치한다는 뜻이다.

D가 0에 가깝지 않은 컴포넌트가 있다면 재검토 후 재구성할 수 있다.

그리고 '극히 예외적인' 컴포넌트를 식별할 수도 있다.

컴포넌트 산점도
컴포넌트 산점도

결론

의존성 관리 지표는 설계의 의존성과 추상화 정도가 내가 '훌륭한' 패턴이라고 생각하는 수준에 얼마나 잘 부합하는지를 측정한다.

지표는 결정된 표준을 기초로 한 측정값에 지나지 않므로 불완전하다. 하지만 지표를 통해 무언가 유용한 것을 찾을 수 있어야 한다.