038. (Clean Architecture) 14. 컴포넌트 결합

14. 컴포넌트 결합

이번 포스팅에서 다룰 세가지 원칙은 컴포넌트 사이의 관계를 설명한다.

컴포넌트 구조와 관련된 아키텍처를 침범하는 힘은 기술적이며 정치적이고, 가변적이다.

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

14.1. ADP: 의존성 비순환 원칙

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

많은 개발자가 동일한 소스 파일을 수정하는 환경에서는 의존성의 변경으로 어제는 되던 것이 오늘은 안되는 경우가 왕왕 발생한다.

소수의 개발자로 구성된 상대적으로 작은 프로젝트에서는 그다지 문제가 되지않지만, 프로젝트와 개발팀 규모가 커질 수록 발생하기가 쉽다.

이 문제의 해결책으로는 두 가지 방법이 발전되어왔는데 첫 번째는 주 단위 빌드(weekly build), 두 번째는 의존성 비순환 원칙(Acyclic Dependencies Principle) 이다.

14.1.1. 주단위 빌드(Weekly Build)

주 단위 빌드는 중간 규모의 프로젝트에서 흔하게 사용된다.

주 단위 빌드 방법은 단순하다.

일주일의 첫 4일은 독자적으로 개발을 진행하고, 금요일에 변경된 코드를 모두 통합하는 것이다.

이 접근법은 4일간 개발자의 자율성과 독립성을 보장하는 장점이 있지만, 이에 대한 리바운드를 금요일 모두 감당해야하는 단점도 있다.

또한 프로젝트가 규모가 커지면 금요일 하루만에 통합하는 것이 불가능한 경우도 발생할 수 있따.

토요일까지 통합에 쓰게되면 결국 목요일부터 통합하는 것으로 바뀌게 될 것이다.

개발 시간을 통합 시간이 잡아먹으면서 팁의 효율성도 서서히 나빠진다.

결국 빌드를 격주로 진행하는 의사결정을 내릴 수도 있다.

효율성을 유지하기 위해서는 빌드 일정을 계속 늘려야하고, 빌드 주기가 늦어질수록 프로젝트가 감수애햐나는 리스크는 계속해서 커진다.

결과적으로 통합과 테스트를 수행하기가 점점 더 어려워지고 빠른 피드백을 얻을 수 없는 팀이 되어버린다.

14.1.2. 순환 의존성 제거하기

두 번째 해결책은 개발 환경을 릴리스 가능한 컴포넌트 단위로 분리하는 것이다.

이를 통해 컴포넌트는 개별 개발자 또는 단일 개발팀이 책임질 수 있는 작업 단위가 된다.

개발자가 담당하는 컴포넌트의 동작을 보장하기만 하면 릴리스된 컴포넌트를 다른 개발자가 믿고 사용할 수 있는 것이다.

이 과정에서 컴포넌트에 릴리스 번호를 부여하고, 접근 가능한 저장소에 배포한다.

이후 개발자는 자신만의 공간에서 해당 컴포넌트를 지속적으로 유지보수하기만 하면 된다.

이 방법은 하나의 컴포넌트를 맡은 팀이 다른 팀에 의애 좌지우지 되지않으며, 컴포넌트는 당연히 전체 시스템보다 작기때문에 수정할 시기를 스스로 결정할 수 있게 된다.

뿐만 아니라 통합은 작고 점진적으로 이뤄지며, 특정 시점에 모든 개발자가 한 데 모여서 모두 통합하는 과정이 소거된다.

위 방법론은 단순하고 합리적이기때문에 널리 사용되는 방식이다.

얼핏 보면 좋아보이지만 이 방법이 성공하려면 컴포넌트 사이의 의존성을 관리해야하며, 이 의존성 구조에 순환이 발생해서는 안된다.

아래 그림은 컴포넌트를 조립하여 애플리케이션을 만드는 전형적인 구조를 나타낸다.

Typical component diagram

이 애플리케이션의 기능과는 상관없이 의존성 관계만 살펴보도록 하자.

위 구조는 방향 그래프(directed graph)이기때문에 컴포넌트는 정점(vertex)에 해당하고, 의존성 관계는 방향이 있는 간선(directed edge)에 해당한다.

중요한 점은 어떤 컴포넌트에서 기작하더라도 의존성 관계를 따라가면서 최초의 컴포넌트로 되돌아갈 수 없는 비순환 방향 그래프(Directed Acyclic Graph) 라는 점이다.

Presenters 컴포넌트는 ViewMain 컴포넌트에 대해서 영향을 받으며, ViewMain 컴포넌트의 담당자는 Presenters의 새로운 리리스와 자신의 작업물의 통합 싯기를 결정해야한다.

또한 Main은 새로 릴리스 되더라도 시스템에서 이로 인해 영향 받는 컴포넌트가 전혀 없으므로 Main의 릴리스가 시스템에 미치는 영향은 적은 편이다.

Presenters 컴포넌트 담당자가 테스트를 하려면 현재 사용중인 InteractorsEntities를 이용해서 자체적으로 빌드하면 되는 것도 파악할 수 있다.

즉 테스트를 구성하기위한 노력이 적게들며, 고려해야할 변수도 상대적으로 적음을 확인할 수 있다.

시스템 전체의 릴리스는 상향식으로 진행해야한다.

의존성이 아예 없는 독립적인 컴포넌트인 Entities부터 순차적으로 진행하여 Main을 마지막에 처리하는 방식이다.

이처럼 구성 요소간 의존성을 파악하고 있으면 시스템을 빌드하는 방법을 직관적으로 알 수 있다.

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

이번엔 순환이 있는 구조를 살펴보자.

새로운 요구사항이 발생해서 아래 그림처럼 EntitiesAuthorizer 컴포넌트에 속한 클래스 하나를 의존하게 되었다고 가정한다.

A dependency cycle

위 그림에서 바로 파악할 수 있듯이 순환 의존성(dependency cycle) 이 발생하였고 즉각적인 문제가 발생한다.

Database 컴포넌트는 릴리스를 위해 Entities를 반드시 필요로 하지만, Entities는 순환이 있으므로 Authorizer와도 호환되어야 한다.

여기서 끝이 아니라 AuthorizerInteractors에 의존하고 있으므로 Database 컴포넌트는 이를 모두 호환해야하는 상황에 놓인다.

위 컴포넌트의 개발자들은 항상 정확하게 동일한 릴리스를 사용하게끔 강제되는 것이다.

또한 테스트를 하기 위해서도 의존하고 있는 모든 컴포넌트를 빌드하고 통합해야한다.

이는 시간 비용을 치뤄야하며 결국 빠른 피드백을 얻을 수 없는 상황에 놓이게 된다.

이처럼 순환이 생기면 컴포넌트를 분리하기가 상당히 어려워진다.

단위 테스트를 하고 릴리스를 하는 일도 굉장히 어려워지며, 에러도 쉽게 발생한다.

게다가 모듈의 개수가 많아짐에 따라 빌드 관련 이슈는 기하급수적으로 증가한다.

시스템 전체의 릴리스도 어떤 컴포넌트를 어떤 순서로 할지도 파악하기 어려워진다.

14.1.4. 순환 끊기

순환이 문제라는 것을 알았으니 순환을 끊어서 다시 비순환 방향 그래프를 그리게끔 해야한다.

이를 위해선 아래의 두 가지 메커니즘이 쓰인다.

1. 의존성 역전 원칙을 적용한다.

Inverting the dependency between Entities and Authorizer

위 그림처럼 User가 필요로 하는 메서드를 제공하는 인터페이스를 생성하고 이 인터페이스를 Entities에 위치시킨다.

Authorizer에서는 이 인터페이스를 상속받아 의존성을 역전시켜 순환을 끊는다.

2. 새로운 컴포넌트를 만든다.

The new component that both Entities and Authorizer depend on

위 그림처럼 EntitiesAuthorizer가 모두 의존하는 새로운 컴포넌트를 만들어서 순환을 끊는다.

14.1.5. 흐트러짐(Jitters)

새로운 컴포넌트를 만들어서 순환을 끊는 방법을 자세히 살펴보자.

여기서 얻을 수 있는 교훈은 어떠한 요구사항은 컴포넌트의 구조도 변경시킬 수 있다는 것이다.

실제로 애플리케이션이 성장함에 따라 컴포넌트의 의존성 구조는 서서히 흐트러지며 또 성장한다.

따라서 의존성 구조에 순환이 발생하는지를 항상 관찰해야 한다.

순환이 발생하면 어떤 식으로든 끊어내야 한다.

다시 말해 컴포넌트의 구조는 떄때로 새로운 컴포넌트를 생성하거나 의존성 구조가 더 커질 가능성이 있음을 의미한다.

14.2. 하향식(top-down) 설계

지금까지의 논의로 우리는 컴포넌트의 구조는 하향식으로 설계될 수 없을을 간파할 수 있따.

컴포넌트는 시스템에서 가장 먼저 설계할 수 있는 대상이 아니며, 오히려 시스템이 성장하고 변경될 때 함께 진화한다.

이상하다고 생각이 들 수도 있따.

이는 컴포넌트와 같이 큰 단위로 분해된 구조는 고수준의 기능적인 구조로 분해할 수 있다고 기대할 수 있기 때문이다.

컴포넌트 의존성 구조와 같이 큰 단위로 분해된 집단을 관찰하면 시스템의 기능적 측면을 컴포넌트가 어떤 식으로든 표현하리라고 미든ㄴ다.

하지만 기능적 측면은 의존성 다이어그램에서 표현되지 않는 것이 문제다.

컴포넌트 의존성 다이어그램은 기능적 기술보다는 애플리케이션 빌드 가능성(buildability)과 유지보수성(maintainability)를 보여주는 역할을 하기 때문이다.

이러한 이유로 컴포넌트 구조는 프로젝트 초기에 설계할 수가 없다.

프로젝트가 진행되며 모듈들이 점차 쌓이기 시작하면 의존성 관리에 대한 요구가 늘어나고 단일 책임 원칙과 공통 폐쇄 원칙에 대한 관심도 늘어나게 되는 것이다.

의존성 구조와 관련된 최우선 관심사는 변동성을 격리하는 것이다.

자주 변경되는 컴포넌트로 인해 안정적인 컴포넌트가 계속해서 변경되는 것은 좋지않기 때문이다.

결국 컴포넌트 의존성 그래프는 자주 변경되는 컴포넌트로부터 안정적이며 가치가 높은 컴포넌트를 보호하려는 방향으로 움직이게 된다.

계속되는 애플리케이션의 성장은 재사용 가능한 요소를 만드는 것에 관심을 가지게 하며, 이 시점에 공통 재사용 원칙이 대두된다.

이러다 순환이 발생하면 의존성 비순환 원칙이 적용되고 컴포넌트 의존성 그래프는 조금씩 흐트러지고 또 성장하게 된다.

아무것도 없는 상태에서 컴포넌트 의존성 구조를 설계하려고 시도하는 경우 공통 폐쇄에 대한 파악도, 재사용 가능한 요소의 파악도 어렵기때문에 실패할 가능성도 없다.

따라서 컴포넌트 의존성 구조는 시스템의 논리적 설꼐에 맞춰 성장하며 진화해야한다.

14.3. SDP: 안정된 의존성 원칙

안정성의 방향으로(더 안정된 쪽에) 의존하라

설계는 동적이다.

설계를 유지하다보면 변경은 불가피하며, 공통 폐쇄 원칙을 준수함으로써 다른 유형의 변경에는 영향 받지않고 특정 유형의 변경메나 민감하게 만들 수 있다.

이처럼 일부 컴포넌트는 필연적으로 변동성을 지니도록 설계된다.

우리는 변동성을 지니도록 설계한 컴포넌트는 언젠가 변경되리라고 예상한다.

변경이 쉽지않은 컴포넌트가 변동이 예상되는 컴포넌트에 의존하게 만들면 절대로 안된다.

최초 모듈 작성시에는 변경하기 쉽게 설계하더라도 누군가 의존성을 부여하는 순간 이 유연함이 소거될 수 있기 때문이다.

이를 해소하기 위해선 안정된 의존성 원칙(Stable Dependencies Principle) 을 준수해야 한다.

14.3.1. 안전성

안정성(stablitiy)이란 무슨 뜻일까?

안정성은 변화가 발생하는 빈도보다는 쉽게 움직이지 않는 것이라고 정의한다.

컴포넌트의 크기, 복잡도, 간결함등의 요인으로 컴포넌트를 변경하기 어렵게 만들 수 있는데,

컴포넌트의 변경을 어렵게 만드는 확실한 방법은 수많은 다른 컴포넌트가 해당 컴포넌트에 의존하게 만드는 것이다.

의존성 다이어그램을 생각해보면 컴포넌트 안쪽으로 들어오는 의존성이 많을수록 안정적이라고 볼 수 있다.

아래 그림은 안정된 컴포넌트의 예시이다.

X: a stable component

컴포넌트 X는 세 컴포넌트가 모두 의존하므로 변경하지 않아야할 이유도 세 가지나 된다.

이 경우 X는 세 컴포넌트를 책임진다(responsible)고 표현하며, 반대로 어디에도 의존하지 않으므로 독립적이다(independent)라고 말할 수 있다.

반대로 아래 그림은 불안정한 컴포넌트를 표현한다.

Y: a very unstable component

컴포넌트 Y는 어떤 컴포넌트도 의존하지 않으므로 책임지는 것이 없지만, 세 개의 컴포넌트의 의존하므로 변경해야할 이유도 세 가지이다.

이 경우 Y는 의존적이라고 말할 수 있다.

14.3.2. 안전성 지표

그렇다면 어떻게 컴포넌트의 안정성을 측정할 수 있을까?

컴포넌트로 들어오고 나가는 의존성의 개수를 세어보는 방법이 있을 수 있다.

이 숫자를 통해 컴포넌트가 위치상 어느정도의 안정성을 가지는 지 계산할 수 있다.

FaninFan\text{--}in

안으로 들어오는 의존성을 의미한다.

이 지표는 컴포넌트 내부의 클래스에 의존하는 컴포넌트 외부의 클래스 개수를 나타낸다.

FanoutFan\text{--}out

밖으로 나가는 의존성을 의미한다.

이 지표는 컴포넌트 외부의 클래스에 의존하는 컴포넌트 내부의 클래스 개수를 나타낸다.

I=Fanout÷(Fanin+Fanout)I = Fan\text{--}out \div (Fan\text{--}in + Fan\text{--}out)

불안정성을 의미한다.

이 지표는 [0, 1] 범위의 값을 가지며 0에 가까울 수록 안정적인 컴포넌트라고 볼 수 있다.

반대로 1에 가까울 수록 불안정상 컴포넌트이다.

아래 그림을 보자.

Our example

Cc 컴포넌트의 안정성을 계산해보자.

Cc 내부의 클래스의 의존하며 Cc 외부에 있는 클래스는 세 개 이므로 FaninFan\text{--}in 은 3이다.

반대로 Cc 내부의 클래스가 의존하는 Cc 외부에 위치한 클래스는 1개이므로 FanoutFan\text{--}out 은 1이다.

결과적으로 불안정성 지표 II 는 0.25라고 볼 수 있다.

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

그렇다면 모든 컴포넌트는 안정적이어야 할까?

모든 컴포넌트가 안정적인 시스템은 변경이 불가능하므로 바람직한 상황이 아니다.

따라서 안정적인 컴포넌트와 불안정한 컴포넌트가 모두 존재해야하며 이상적인 구조는 아래와 같다.

An ideal configuration for a system with three components

위쪽에는 변경 가능한 컴포넌트가 보이고, 이 컴포넌트들이 아래의 안정적인 컴포넌트에 의존하는 형태이다.

아래는 안정된 의존성 원칙를 위배한 경우를 보여준다.

SDP violation

Flexible은 변경하기 쉬운 컴포넌트인데, Stable이 의존하면서 문제가 발생한다.

Flexible의 변경은 모든 컴포넌트에 영향을 주기 때문이다.

좀 더 의존 관계를 명세해보자.

 U within Stable uses C within Flexible

StableUFlexibleC를 의존하고 있다.

이 상황에서 의존성 역전 원칙을 이용해 의존성을 끊으면 아래와 같이 될 것이다.

C implements the interface class US

14.3.3.1. 추상 컴포넌트

위 그림의 UServer처럼 오직 인터페이스만을 포함하는 컴포넌트를 생성하는 게 조금 이상해보일 수 있다.

하지만 정적 타입 언어를 사용할 때 이 방식은 상당히 흔하며, 꼭 필요한 전략이다.

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

14.4. SAP: 안정된 추상화 원칙

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

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

시스템에는 자주 변경해서는 절대 안되는 소프트웨어도 존재한다.

고수준 아키텍처나 정책 결정과 관련된 소프트웨어가 그 예시이다.

이처럼 개발자는 비즈니스 로직이나 아키텍처와 관련된 결정에는 변동성이 없기를 기대한다.

따라서 고수준 정책을 캡슐화하는 소프트웨어는 반드시 안정된 컴포넌트에 위치해야하며, 불안정한 컴포넌트는 변동성이 큰 소프트웨어만을 포함해야한다.

하지만 고수준 정책을 안정된 컴포넌트에 위치시키면 그 정책을 포함하는 소스 코드를 수정하는 것이 어려워진다.

이로 인해 시스템 전체 아키텍처가 유연성을 잃어버린다.

이 문제의 해결책은 개방 폐쇄 원칙에서 찾을 수 있다.

개방 폐쇄 원칙에서는 클래스를 수정하지 않고도 확장이 충분히 가능할 정도로 클래스를 유연하게 만들 수 있을 뿐만 아니라 바람직한 방식이라고 한다.

이를 만족하는 것이 바로 추상 클래스(abstarct class) 이다.

14.4.2. 안정된 추상화 원칙

안정된 추상화 원칙은 안정성과 추상과 정도 사이의 관계를 정의한다.

이 원칙은 한편으로는 안정된 컴포넌트는 추상 컴포넌트여야 하며, 이를 통해 안정성이 컴포넌트를 확장하는 일을 방해하지 않아야한다고 말한다.

반대로 불안정한 컴포넌트는 반드시 구현체 컴포넌트여야한다.

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

안정된 컴포넌트가 확장이 가능해지면 시스템은 유연성을 얻어 아키텍처를 과도하게 제약하지 않게 된다.

안정된 추상화 원칙과 안정된 의존성 원칙을 결합하면 컴포넌트 수준에서의 의존성 역전 원칙과 일맥상통한다.

하지만 의존성 역전 원칙은 클래스에 대한 원칙이며, 클래스는 중간이 존재하지않는다.

참고 클래스는 추상적이거나 아니거나 둘 중 하나이다.

14.4.3. 추상화 정도 측정하기

NcNc

컴포넌트의 클래스 개수

NaNa

컴포넌트의 추상 클래스와 인터페이스의 개수

A=Na÷NcA = Na \div Nc

이 지표는 컴포넌트의 추상화 정도를 측정한 값이다.

이 값은 컴포넌트의 클래스 총 수 대비 인터페이스와 추상 클래스의 개수를 단순히 계산 값이다.