Clean-Architecutre #4부 - CLASS를 넘어 컴포넌트 구조 잘짜기

Clean-Architecutre #4부 - CLASS를 넘어 컴포넌트 구조 잘짜기

컴포넌트 응집도와 결합도의 법칙

12. 컴포넌트

🌟 컴포넌트란?

  • 컴포넌트는 독립적으로 배포할 수 있는 단위다
  • 여러 컴포넌트를 서로 링크하여 애플리케이션을 구성할 수 있다
  • 잘 설계된 컴포넌트는 반드시 독립적으로 배포 가능한, 독립적으로 개발 가능한 능력을 갖춰야 한다.

모듈을 컴포넌트 단위로 대입하여 보면 되지 않을까 싶다.
멀티모듈을 구성할 때, 의존성을 최대한 줄이고, 독립적으로 배포 가능한 단위로 구성하기 때문이다.
각 모듈들을 linking(implements) 하여 application을 구성한다.

🌟 초기 컴포넌트

  • 장치가 느리고 메모리가 비싸 한정적이었던 상황에서 실행 메모리 주소 위치까지 프로그램 소스에 포함되어 있었다.(로드 위치 설정 강결합)
  • 특정 라이브러리를 추가하고 싶으면 해당 라이브러리소스가 합쳐져 단일 프로그램으로 병합시켜야 했다. (라이브러리 버전 강력 의존, APP에 불필요한 코드 뭉치 포함)
    • 라이브러리는 바이너리가 아니라 소스코드 형태 그대로 유지되었다.
  • 장치가 느리기 때문에 컴파일러가 여러차례 로드를 시도, But 메모리가 작아 상주 불가 -> 느린 컴파일러가 여러차례 일까지 해야하는데 계속 불필요한 반복 적재를 해야함
  • 함수 라이브러리가 포함되어 컴파일 되기 때문에 사용한 라이브러리가 크면 컴파일 속도도 당연히 느리다.

🌟 라이브러리와 컴포넌트 분리

  • 라이브러리와 어플리케이션의 분리를 위해 함수 라이브러리를 개별적으로 미리 컴파일 해두기 시작했다.
  • 그러나 아직 컴파일된 바이너리 라이브러리를 메모리의 특정 고정 위치에 심고, app 컴파일파일과 사이좋게 고정해서 사용했다.
  • 소스코드 내부에 실행될 메모리 위치가 고정되어 박혀있기 때문에, 확장성이 박살이 났다.
  • 라이브러리에 추가하려면 메모리 할당 주소가 더 필요하고, 그러나 그 바로 뒤에는 app이 가득 차지하고, app을 늘리려해도 비슷한 상황이었다.

김현우1

어플리케이션과 라이브러리의 분리는 이뤄졌지만, 확장성이 떨어지는 문제가 발생했다.

🌟 재배치가 가능한 바이너리

해답으로 메모리를 재배치할 수 있는 메모리 로더를 사용하자는 결론이 나왔다.

  • 이제 프로그래머는 라이브러리, app 의 로드 메모리 위치를 로더에게 지시해서 실행한다.
  • 로더는 단순하게 여러 바이너리를 입력받고, 하나씩 메모리에 로드하고 재배치하는 작업을 처리한다.
  • 여기서 드디어 프로그램이 라이브러리를 외부참조 할 수 있게 변했다.
  • 컴파일러는 라이브러리가 쓰이면 함수를 외부참조로 생성하고, 라이브러리에서는 외부정의로 생성했다.
  • 이렇게 외부 정의외부참조 를 연결시켜 강결합에서 벗어날 수 있게된 링킹 로더 가 탄생했다.

🌟 링커와 로더로 분리

  • 프로그램이 더 비대해지면서 링킹 로더의 역할이 너무 과해졌다. (바이너리 읽고, 외부 참조 해석하여 정의와 연결해 기능 수행 등)
  • 링커와 로더가 분리되어 링커는 바이너리를 읽고, 외부 참조를 해석하여 정의와 연결하는 역할만 수행하게 되었다.
  • 로더는 링커가 생성한 바이너리를 읽고, 메모리에 로드하고 재배치하는 역할만 수행하게 되었다.

최종적으로 기계부품 성능의 발전으로 다수의 jar, 공유 라이브러리를 순식간에 링크한 후, 메모리에 로드해 app을 실행할 수 있게 됐다.
최근에 쓰고 있는 부품 갈아끼기식 컴포넌트 플러그인 아키텍쳐 가 가능해진 상황이다.

자바의 경우 JVM이 링커, 로더의 역할을 수행한다. 개발자는 이런 저수준의 개발까지 하지 않아도 된다.
대신 Gradle과 같은 빌더로 의존성 관리만 하면 된다.


13. 컴포넌트 응집도

어떤 클래스를 컴포넌트에 포함시킬지 결정하는 기준은 응집도다.
응집도가 높은 컴포넌트는 변경이 발생할 때 다른 컴포넌트에 영향 없이 해당 컴포넌트만 수정하면 된다.

🌟 REP : 재사용/릴리즈 등가 원칙

재사용 단위는 릴리스 단위와 같다

  • 재사용될 수 있는 라이브러리 컴포넌트들이 많아지면서 릴리즈 버전이 중요해지기 시작했다.
  • 이에 따라 내 컴포넌트에 포함된 라이브러리 컴포넌트들의 릴리즈 버전을 관리하는 것이 중요해졌다.
  • 라이브러리 함수 + 내 컴포넌트로 구성된 프로그램또한 함께 빌드해도 문제가 없어야 한다.

따라서 내 컴포넌트에 꼭 필요한 함수인가, 하나의 클래스를 사용하기 위해 너무 비대한 라이브러리가 추가되는 것은 아닌지,
내 컴포넌트의 실행 목적을 위해 유의미하게 포함되는 것인지 파악해야 한다. 의존성이 추가되면 함께 묶여서 배포된다고 생각해야 한다.
이런 관점은 초기 컴포넌트에서 라이브러리와 특정 앱이 합쳐져 컴파일 될 때 최대한 시간을 줄이기 위해 깎아내고 모으면서 발생된 것이 아닐까? (한번 올라가면 수정이 어려우므로)

🌟 CCP : 공통 폐쇄 원칙

  • 클래스의 SRP(단일 책임 원칙) 과 마찬가지로 단일 컴포넌트 또한 수정의 이유가 여럿이면 안된다.
  • 내 컴포넌트의 기능을 수정한 영향이 다른 컴포넌트들에게 영향을 주면 안된다. 내 컴포넌트 내부에서 처치를 완료해야 한다.
  • 재사용성보다 유지보수성이 더 높은 가치를 가지기 때문이기도 하다.

동일한 시점에 동일한 이유로 수정되는 클래스들을 하나의 컴포넌트로 묶어야 한다.
다른 이유로 변경되는 클래스들은 다른 컴포넌트로 분리해야 한다.

🌟 CRP : 공통 재사용 원칙

  • 라이브러리에서 단 하나의 클래스만 사용하더라도, 그 라이브러리 전체를 의존해야한다.
  • 그렇게 되면 배포시에 불필요한 파일과 검증이 늘어난다.
  • 강하게 의존해야할만한 클래스와 모듈들이 한 컴포넌트에 집어넣고 나머지는 다른 컴포넌트로 분리해야 한다고 한다.

ISP와 같은 원칙이다. 인터페이스를 기능별로 구분해 놓고, 필요한 기능만 의존하도록 해야하는 것 처럼,
클래스와 컴포넌트, 라이브러리 단위에서도 동일하게 가급적 꼭 필요한 것만 의존해야한다.

🌟 컴포넌트 응집도 다이어그램

  • REP 법칙은 재사용성을 높히기 위해 각 배포에 릴리즈 버전을 붙여 관리하는 방식이다.
  • CCP 법칙은 유지보수성을 높히기 위해 동일한 이유로 수정되는 클래스들을 하나의 컴포넌트로 묶는 방식이다.
  • CRP 법칙은 배포시 불필요한 파일과 검증을 줄이기 위해 강하게 의존해야할만한 클래스와 모듈들이 한 컴포넌트에 집어넣고 나머지는 다른 컴포넌트로 분리하는 방식이다.

김현우2

각 변은 맞은편 꼭짓점을 포기할 경우 감수해야할 비용을 의미한다.

  • 재사용성과 유지보수만을 따져서 설계되면 불필요하게 릴리즈가 많아진다.(재사용된 많은 컴포넌트 + 너무 많이 분리된 컴포넌트)
  • 재사용성을 배제하면 재사용을 할 수 없게 된다. (컴포넌트 버전관리가 되지 않는다면 내 컴포넌트에 도입할 수 없다.)
  • 유지보수성을 배제하면 컴포넌트의 변경이 많아진다. (한 컴포넌트를 수정하면 다른 컴포넌트들에 영향이 크므로)

이런 각각의 법칙은 치우치지 않고 적절하게 균형을 이루며 설계되어야 한다.
초기에는 수정이 많고 새로 만들어지는 클래스가 많으므로 유지보수성을 우선시 하고, 나중에는 재사용성을 우선시 하면서 오른쪽으로 간다.
컴포넌트의 응집도는 단순히 컴포넌트가 단 하나의 기능만 수행하게 분리한다. 가 아니다.


14. 컴포넌트 결합도

컴포넌트의 결합도는 컴포넌트 간의 의존성 즉, 관계를 의미한다.
컴포넌트 간의 결합도가 높으면 하나의 컴포넌트를 수정할 때 다른 컴포넌트도 수정해야 한다.

🌟 ADP : 의존성 비순환 원칙

결합도가 높으면 내가 의존하고 있던 프로그램이 수정되면 내 컴포넌트가 작동하지 않는 경우가 있다.
이를 숙취 증후군 이라고 한다.

이 숙취 증후군을 타도하기 위해 두 가지 방법이 발전해왔다.

  • 주 단위 빌드 (BAD)
    • 일주일의 첫 4일 동안은 개발자들끼리 서로 상관 없이 코드만 짠다.
    • 금요일이 되면 모든 코드를 통합해 시스템을 빌드한다.
    • 프로그램이 커지고 자연스럽게 통합이 금요일을 넘어가면서 효율이 나빠진다.
  • 순환 의존성 제거하기 (GOOD)
    • 개발환경을 컴포넌트 단위로 분리하여 개별 팀,개인이 책임질 수 있게 한다.
    • 해당 컴포넌트는 릴리즈되어 다른 개발자가 사용할 수 있도록 만든다.
    • 다른 개발자는 이 릴리즈된 컴포넌트를 사용할지 말지 결정하여 개발한다.
    • 컴포넌트별 독립적인 개발이 가능하다. 통합은 작고 점진적이게 이뤄진다.

효율적인 개발을 하기 위해서는 컴포넌트간의 의존성 구조 관리가 반드시 필요하다. 의존성이 순환되면 숙취증후군이 필연적이다.

✒︎ 비순환 그래프 (GOOD)

김현우_비순환그래프

  • 위 그래프는 순환 의존성이 없는 그래프다.
  • 내가 Presenters의 개발팀이라면, 기능 수정을 하고 Interactors, Entities만 사용해서 자체 버전을 빌드하면 끝이다.
  • 화살표의 역방향으로는 컴포넌트가 존재하고 있는지도 모른다. (Main이 아무리 바뀌어도 나머지 컴포넌트는 존재를 모른다.)

전체 시스템을 빌드할 때는 아래에서 (Entities) 위 (Main) 쪽 순으로 빌드를 하면 된다.
순환 의존성이 없어서 간단 명료하게 빌드 순서 절차가 생긴다.

✒︎ 순환 그래프 (BAD)

김현우_순환그래프

Entities가 Authorizer의 객체 1개를 사용하여 의존성을 추가했다고 가정해보자.

  • 발생하는 문제
    • Entities를 의존하고 있는 Database 컴포넌트 개발팀은 더이상 Interactors, Entities만 고려해서 빌드할 수 없다.
    • Entities가 Autorizer와도 호환되어야 한다.
    • Entities, Authorizer, Interactors의 의존성이 순환되면서 하나의 거대 컴포넌트가 되었기 때문이다.
    • 기존에 Database 개발자들이 Entities, Interactors 의 호환성만 고려해서 개발하던 상황에서 Interactors와 맞는 Autorizer인지, 그 세가지 모두가 호환되는지를 체크하기 시작해야 한다.
    • 순환의존이 1개만 추가되어도 빌드시 고려해야할 경우의 수가 기하급수적으로 늘어난다.

각 컴포넌트끼리 결합도가 너무 높아졌기 때문이다.


그렇다면 순환을 끊고 컴포넌트의 결합도를 낮추기 위해서 어떻게 할 수 있을까?

✒︎ 순환 끊기

김현우_DIP예시

  • DIP
    • DIP를 이용해서 순환 의존성을 끊을 수 있다.
    • Entities가 Authorizer를 의존하지 않고, Authorizer가 Entities를 의존하도록 변경한다.
    • Authorizer의 Permission 객체를 Entities에서 Interface로 고수준의 모듈로 유지하고, Authorizer가 이를 참조해서 만들게 한다.
    • Entities는 Authorizer에 의존하고 있지 않기 때문에 Entities를 사용하는 컴포넌트에서 더이상 Authorizer를 고려하지 않아도 된다.

김현우_비순환그래프

  • 모두 의존하는 새 컴포넌트 생성
    • Entities가 Authorizer에 Permission을 사용한다면, 그냥 그 Permission만 분리하여 새로운 컴포넌트를 만들면 된다.
    • 결합도는 낮아지고 응집도는 올라가는 방법이다.
      • 하지만 새로운 컴포넌트를 만들어야 하므로, 빌드 순서가 늘어난다.

컴포넌트의 구조가 자연스럽게 변경되는 방법이다.
의존성 구조가 서서히 변경되고, 그 변경을 통해 또다시 순환이 생기는지 아닌지 점검하며 구조는 성장해 나간다.

  • 컴포넌트 구조나 설계는 초기에 설계하기 힘들다. 시스템이 성장하고 변경될 때 함께 진화한다.
  • 구현과 설계가 이뤄지는 초기에는 숙취 증후군을 피하고 변경에 대한 사이드이펙트를 줄이기 위해 의존성 관리에 집중한다.(SRP, CCP)
  • 이를 통해 응집도가 올라가고 변동성을 격리하기 위해 결합도를 낮춘다.
  • App이 성장하면서 재사용 가능한 요소를 만들기 집중하고 (CRP) 의존성을 끊어나가며 (ADP) 컴포넌트를 분리한다.
  • 결과적으로 컴포넌트 의존성 다이어그램은 기능적인 기술이 아니라 App의 발전을 통해 변화하며, 빌드 가능성유지보수성을 보여주는 지도다.

🌟 SDP : 안정된 의존성 원칙

안정된 컴포넌트는 불안정한 컴포넌트에 의존해서는 안된다.

  • 쉽게 변동될 수 있게 설계된 컴포넌트는 안정성이 낮다.
  • 정성이 높은 컴포넌트가 안정성이 낮은 컴포넌트에 의존하면 안된다.
  • 안정성이 낮은 컴포넌트의 변경이 어려워지기 때문이다.
  • 즉, 변동되기 쉬운 컴포넌트가 변동되지 않는 컴포넌트에서 사용되므로 변경하며 안정성이 높은 컴포넌트의 눈치를 보게 되는 상황이 발생한다.
  • 반대로 변경되기 쉬운 쪽으로 설계된 컴포넌트에서 안정성이 높은 컴포넌트를 사용하면(의존하면) 상대적으로 변경성은 유지된다.

✒︎ 컴포넌트에서 안정성이란?

안정성이란 변동되지 않을 가능성을 의미한다.

김현우_안정성높은컴포넌트

  • X는 안정성이 높은 컴포넌트이다. WHY?
    • 3개의 다른 컴포넌트가 X에 의존하고 있다. (X의 변경이 다른 컴포넌트에 영향을 미친다.)
    • X는 다른 것을 의존하고 있지 않으므로 X를 변경시킬 다른 컴포넌트는 없다.
    • X는 독립적으로 존재하고, 다른 것들이 X에 의존해 가져다 쓴다.

김현우_안정성낮은컴포넌트

  • 반면 Y는 안정성이 낮은 컴포넌트다. WHY?
    • Y를 의존하는 컴포넌트가 없어 Y의 변경은 책임을 지지 않는다.
    • Y는 가만히 있어도 3개의 의존하는 컴포넌트의 변화에 영향을 그대로 받는다. (3가지 경우의 변동성)

✒︎ 안정성 지표

컴포넌트의 안정성을 측정하는 지표는 외부의 클래스를 쓰느냐, 외부에서 내 클래스를 가져다 쓰느냐 즉 in/out되는 의존성의 개수이다.

I (불안정성) = Fan-out / (Fan-in + Fan-out)
[0 ~ 1] 범위의 값으로 I = 0이면 안정, I = 1이면 최고로 불안정이다.

  • X의 경우 I = 0 / (3 + 0) = 0 이므로 안정성이 높다.
  • Y의 경우 I = 3 / (0 + 3) = 1 이므로 최고로 불안정하다.

결과적으로 안정성이 높은 것은 어떤 컴포넌트들은 해당 컴포넌트에 의존하지만, 해당 컴포넌트는 다른 컴포넌트를 의존하지 않는다. (독립적으로 존재하며 쓰이기만 한다.)
안정성이 낮은 것은 다른 컴포넌트들이 해당 컴포넌트를 의존하지 않지만, 해당 컴포넌트는 다른 컴포넌트를 의존한다. (독립적으로 존재하지 않고 다른 것들을 가져다 쓴다.)

중요한건 안정성이 낮게 설계될수록 변경에 자유롭고, 안정성이 높게 설계될수록 변경에 제약이 생긴다는 것이다.

✒︎ 다시 SDP 설계로 돌아와서

이제 안정성이 높은 컴포넌트가 안정성이 낮은 컴포넌트를 의존할 때 발생하는 문제에 대해 생각해보자.

김현우_이상적인SDP설계

  • 위 사진은 이상적인 SDP 설계다.
  • 변동성이 높은 두 컴포넌트가 안정성이 높은 컴포넌트를 의존하고 있다.
  • 변동성이 높은 컴포넌트는 변동할 수 있어서 확장과 변화에 열려있고, 안정성이 높은 컴포넌트는 변동성이 낮아서 안정적이다.

여기에서 stable한 컴포넌트가 unstable한 컴포넌트를 의존하게 된다면, 변경되기 쉽게 설계되어 추가된 컴포넌트가 변경이 어려워진다.

해결책은 위에 ADP(의존성 비순환 원칙)의 순환끊기 방법과 비슷하다.
DIP 를 위해 인터페이스를 분리하고, 그 인터페이스를 담은 추상체용 컴포넌트를 하나 추가한다.

🌟 SAP : 안정된 추상화 원칙

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

SAP는 컴포넌트의 안정성과 추상화 정도 (얼마나 추상체 클래스가 많은가) 의 관계에 대해서 설명한다.
소프트웨어는 안정성이 높길 원하는 고수준의 정책들이 있고 안정된 컴포넌트에 위치시키길 원한다.
그러나 변동없는 고수준 정책들이 포함된 컴포넌트는 수정하기가 어려워진다.
이를 위해 변동없는 고수준 기능들은 abstract class or interface인 추상체로 변경하면 안정적이며 변동 가능하다.

김현우_SAP배제구역그래프

A인 세로축은 추상화 정도, I인 가로축은 안정성이다.

  • (0,0) 고통의 구역
    • 그러나 모든 클래스가 추상체로 구성되면 문제가 발생할 것이고, 모두가 구현체면 변동에 어려움이 있을 것이다.
    • 그래프는 컴포넌트의 추상과 구체 중간의 좋은 지점을 설명해준다.
    • (0,0)에 가까울 수록 고통의 구역, 매우 구체적이고 안정적이다. 확장이 거의 불가능하고 배제해야 할 구역이다.
    • 그러나 보통 고통은 변화가 발생할 때 발생하므로 변동의 가능성이 없는 String 같은 객체는 (0,0)에 존재해도 된다.
  • (1,1) 쓸모 없는 구역
    • 추상체이면서 어느 누구도 의존하지 않는 객체는 쓸모가 아예 없다.(삭제대상)

The Main Sequence쪽으로 컴포넌트가 이동할 수 있도록 적절한 개수의 추상체와 구현체를 배합해야 한다.