본문 바로가기

Language & Framework/개발잡담

클린 아키텍처 - 컴포넌트 응집도와 컴포넌트 결합

 

원래 책 내용 정리해서 올리는 걸 별로 좋아하지 않는데.. (인터넷 폐기물 재생산)

내용이 상당히 좋음.

 

내가 가장 마음에 들었던 챕터라 정리해서 올려본다.

특히 컴포넌트 결합도 파트가 좋음.

마음에 들면 나머지는 사서 읽으셈.

 

 

 

 

컴포넌트 응집도

 

1. 어떤 클래스를 어느 컴포넌트에 포함시켜야 하는가?

 

3가지 원칙이 있다.

  • REP: 재사용/릴리즈 등가 원칙 (Reuse/Release Equivalence Principle)
  • CCP: 공통 폐쇄 원칙 (Common Clousre Principle)
  • CRP: 공통 재사용 원칙 (Common Reuse Principle)

 

(1) REP : 재사용/릴리즈 등가 원칙

1-1. 거시적 관점의 의미

  • 재사용 단위는 릴리즈 단위와 같다.
  • maven, gradle 같은 도구의 중요성이 점점 커지는 이유 = 재사용 가능한 컴포넌트나 컴포넌트 라이브러리가 엄청나게 많아졌기 때문.
  • 릴리즈 번호가 없다면 재사용 컴포넌트들이 서로 호환되는지 보장할 방법이 없으며, 새로운 버전이 출시되고 무엇이 변했는지를 알려면 개발자들이 릴리즈 버전을 알아야 함.

 

1-2. 아키텍처 관점의 의미

  • 단일 컴포넌트는 응집성 높은 클래스와 모듈들로 구성되어야 한다.
  • 하나의 컴포넌트로 묶인 클래스와 모듈은 반드시 함께 릴리스할 수 있어야 한다.
  • 하나의 컴포넌트로 묶인 클래스와 모듈은 버전 번호가 같아야 하며, 동일한 릴리즈로 추적 관리되고, 동일한 릴리즈 문서에 포함되어야 한다.

1-3. 종합

  • 이 원칙은 당연히 지켜져야할 너무나도 '기본적인 이치'에 대한 이야기다.
  • 직접적인 제약이 아니기 때문에 3가지 원칙 중 가장 조언이 약한 편.

 

(2) CCP: 공통 폐쇄 원칙

 

종합

  • 동일한 이유로 동일한 시점에 변경되는 클래스는 같은 컴포넌트로, 다른 시점에 다른 이유로 변경되는 클래스는 다른 컴포넌트로 분리하라. (SRP의 컴포넌트 버전이라고 생각할 수 있다.)
  • 유지보수성은 재사용보다 훨씬 중요하다. 애플리케이션에서 코드가 반드시 변경되어야 한다면, 이런 변경이 여러 컴포넌트 도처에 분산되어 발생하기보다는, 차라리 변경 모두가 단일 컴포넌트에서 발생하는 편이 낫다.
  • OCP와 같이 100% 폐쇄하는 것은 불가능하므로 전략적인 폐쇄가 필요하다. 발생할 가능성이 있거나 과거 발생했단 대다수의 공통적인 변경에 대해 클래스가 닫혀있도록 설계해야 한다.

 

(3) CCP : 공통 재사용 원칙

 

종합

  • 컴포넌트 사용자들을 필요하지 않는 것에 의존하게 강요하지 마라.
  • 개별 클래스가 단독으로 재사용되는 경우는 거의 없으며, 재사용 가능한 클래스는 재사용 모듈의 일부로써 해당 모듈의 다른 클래스와 상호작용한다. 이런 클래스들이 동일한 컴포넌트에 포함되어야 한다.

하지만 CCP에서 더 중요한 것은 "무엇을 포함하느냐" 보다 "무엇을 포함하면 안되느냐"이다.

  • 어떤 컴포넌트(A)가 다른 컴포넌트(B)를 사용하면, A 컴포넌트는 B 컴포넌트에 의존하게 된다.
  • B 컴포넌트의 클래스 중 단 하나의 클래스에만 의존할 수 있지만, A 컴포넌트 자체가 B 컴포넌트에 의존하게 된다는 사실에는 변함이 없다.
  • 사용되는(used) 컴포넌트가 변경될 때마다 사용하는(using) 컴포넌트도 변경해야할 가능성이 높으며, 사용하는 컴포넌트를 변경하지 않더라도 재컴파일, 재검증, 재배포해야할 가능성이 여전히 남아있다.
  • 강하게 결합되지 않은 클래스들을 굳이 한 컴포넌트에 위치시키면 이런 일이 발생함.

 

 

(4) 컴포넌트 응집도에 대한 균형 다이어그램

 

  • REP와 CCP는 컴포넌트를 크게 만들고 CRP는 컴포넌트를 작게 만든다.
  • 각 변은 반대쪽 꼭지점에 있는 원칙을 포기했을 때 감수해야할 비용을 나타낸다.
  • 일반적으로 프로젝트는 삼각형 오른쪽에서 시작하며, 프로젝트가 성숙할수록 왼쪽으로 이동해간다.
  •  

(4) 결론

  • 어느 클래스들을 묵어서 컴포넌트로 만들지를 결정할 때, 재사용성과 개발 가능성이라는 상충하는 힘을 반드시 고려해야 한다.
  • 현재 상황에 맞더라도 내년에는 맞지 않을 수 있음. 어플리케이션이 발전함에 따라 컴포넌트를 구성하는 방식도 진화한다.

 

 

 

컴포넌트 결합도

 

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

 

1. ADP: 의존성 비순환 원칙 (Acyclick Dependencies Principle)

 

(1) ADP?

  • 컴포넌트 의존성 그래프에 순환이 있으면 안된다.
  • 종일 일해서 무언가를 작동하게 만들어놓고 퇴근했는데, 다음 날 실행해보면 전혀 돌아가지 않는다? -> 누군가 당신보다 더 늦게까지 일하면서 당신이 의존하고 있던 무언가를 수정했기 때문
  • 많은 개발자가 동일한 소스 해파일을 수정하는 환경에서 발생
  • 소수 개발자로 구성된 작은 프로젝트에서는 큰 문제가 되지 않지만, 프로젝트와 개발팀 규모가 커지면 안정 버전을 빌드하지 못한 채 몇 주가 그냥 흘러가는 일도 드물지 않음.

(2) 해결책

2-1.  주단위 빌드 (weekly build)

  • 중간 규모 프로젝트에서 흔하게 사용된다.
  • 일주일 첫 4일은 서로 신경쓰지 않고 개인적으로 개발하고, 금요일에 변경된 코드를 모두 통합하여 시스템을 빌드.
  • 장점
    • 주 5일 중 4일을 개발자가 고립된 세계에서 살 수 있게 해줌.
  • 단점
    • 금요일에 모든 업보를 맞아야 함.
    • 프로젝트가 커지면 금요일 하루만에 끝마치는 게 불가능해지고, 결국 통합 작업을 목요일부터, 나중에는 수요일부터 준비해야함.
    • 결국 개발자의 능률을 위해 빌드를 격주로 하자고 한다. 효율적인 개발을 위해 빌드 일정은 계속 늘어나고, 빌드 주기가 늦어질수록 빌드 후 위험이 발생할 가능성은 점점 커진다.
    •  결과적으로 통합과 테스트 수행은 점점 어려워진다.

2-2. 순환 의존성 제거하기

  • 개발 환경을 릴리즈 가능한 컴포넌트 단위로 분리한다.
  • 컴포넌트는 개별 개발자 혹은 단일 개발팀이 책임질 수 있는 작업 단위가 된다.
  • 개발자가 해당 컴포넌트를 동작하도록 만든 후, 해당 컴포넌트를 릴리즈하여 다른 개발자가 사용할 수 있도록 만든다.
  • 담당 개발자는 이 컴포넌트에 릴리즈 번호를 부여하고, 다른 팀에서 사용할 수 있는 디렉터리로 이동시킨다. 그런 다음 개발자는 자신만의 공간에서 해당 컴포넌트를 지속적으로 수정한다. 나머지 개발자는 릴리즈된 버전을 사용한다.
  • 컴포넌트가 새로 릴리즈되어 사용할 수 있게 되면, 다른 팀에서는 새 릴리즈를 당장 적용할지를 결정해야 한다. 적용하지 않기로 했다면 그냥 과거 버전의 릴리즈를 계속 사용한다. 새 릴리즈를 적용할 준비가 되었다는 판단이 들면 새 릴리즈를 사용하기 시작한다.
  • 이 작업 절차는 단순하며 합리적이어서 널리 사용되지만, 컴포넌트 사이의 의존성 구조에 순환이 생긴다면 아무 의미가 없음.

2-3. 순환 의존성 제거 예시

 

  • Partners팀은 자신들이 수정했을 때 영향을 받는 컴포넌트를 쉽게 확인할 수 있고, 이 두 컴포넌트를 작업 중인 개발자는 Partners의 릴리즈를 확인하며 자신들의 작업물을 통합할 시기를 결정할 수 있다.
  • Main이 변경되어도 어떤 컴포넌트에도 영향을 주지 않는다.
  • 시스템 전체를 릴리즈해야하는 경우 entities부터 상향식으로 빌드, 릴리즈 절차를 거치면 된다.

 

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

 

(Entities의 User 클래스가 Authorizer의 Permission을 사용하게 된 상황)

  • Database 컴포넌트를 개발하는 개발자는 릴리스 시 Enttiies와 호환되어야 한다는 사실을 알고 있다.
  • 그러나 Entities는 Authorizer와 호환되어야 하고, Authorizer는 Interactors와 호환되어야 한다.
  • 결국 Authorizer, Entities, Interactors가 하나의 거대한 컴포넌트가 되어 다시 병합 문제가 발생한다.
  • 클래스 중 하나의 단위 테스트를 하는데 다양한 라이브러리와 다른 사람들의 많은 작업물을 포함해야하는 경우 대개 순환 의존성이 생긴 것이다.
  • 의존성 그래프에 순환이 생기면 컴포넌트를 빌드해야하는 올바른 순서를 파악하기 힘들고, 애초에 올바른 순서라는 것 자체가 없을 수도 있다.

 

2-5. 순환 끊기

 

 

  • 첫 번째 방법 : DIP를 활용해 User가 필요로하는 메서드를 제공하는 인터페이스를 상속하고, 해당 인터페이스를 Entities에 위치시킨 뒤 Authorizer는 해당 인터페이스를 상속 받는다.

 

 

  • 두 번째 방법 : Entities와 Authorizer가 모두 의존하는 새로운 컴포넌트를 만들고, 두 컴포넌트가 모두 의존하는 클래스를 해당 컴포넌트에 밀어 넣는다.
    • 두 번째 방법이 시사하는 바 -> 요구사항이 변경됨에 따라 컴포넌트도 변경된다. 어플리케이션의 성장에 따라 의존성 구조는 서서히 흐트러지며 성장하며, 의존성 구조에 순환이 발생하는지 항상 관찰해야 함.

 

2-6. 하향식 설계

 

  • 지금까지의 결론 : 컴포넌트 구조는 하향식으로 설계할 수 없다. 즉, 시스템에서 가장 먼저 설계할 수 없으며 시스템이 성장하고 변경될 때 함께 진화한다.
  • 컴포넌트 의존성 다이어그램은 애플리케이션의 기능을 기술하는 일과는 관련이 없다.
  • 컴포넌트 의존성 다이어그램은 애플리케이션의 buildability, maintainabillity를 보여주는 지도와 같다.

 

 

2. SDP: 안정된 의존성 원칙 (Stable Dependencies Principles)

 

(1) SDP?

 

  • 설계는 결코 정적일 수 없으며, 변경은 불가피하다.
  • 공통 폐쇄 원칙을 준수하여 컴포넌트가 다른 유형의 변경에는 영향받지 않으면서 특정 유형의 변경에만 민감하게 만들 수 있다.
  • 변경이 쉽지 않은 컴포넌트는 변동이 예상되는 컴포넌트에 의존해서는 안된다.
  • 내가 모듈을 만들 때는 변경하기 쉽도록 설계했지만, 다른 누군가가 모듈에 의존성을 매다는 순간 내 코드도 변경하기 어려워진다.

 

(2) 안정성

 

2-1. 안정성의 정의

  • 동전을 옆면으로 세워놓은 채 오랫동안 건드리지 않으면 세워져 있겠지만, 이 상태는 stable한 상태라고 보기는 어렵다. 반면 탁자는 넘어트리기 어렵기 때문에 상대적으로 stable한 상태라고 볼 수 있음.
  • 많은 컴포넌트가 의존하는 컴포넌트는 상대적으로 안전하다. 따라서 중요한 로직을 최대한 내부 컴포넌트로 밀어넣어야 한다.
  • 가령, 3개의 컴포넌트가 의존하고 있는 컴포넌트는 변경하지 말아야 할 이유가 3가지나 존재한다고 이야기할 수 있다.

 

2-2. 안정성 지표

 

  • 컴포넌트로 들어오고 나가는 의존성 개수를 체크한다.
    • fan-in : 안으로 들어오는 의존성
    • fan-out : 바깥으로 나가는 의존성
    • I (불안정성): I = fan-out / (fan-in + fan-out). I = 0이면 최고로 안정적인 상태.

 

  • 예시) Cc컴포넌트의 의존성은?
    • fan-in = 3
    • fan-out = 1
    • I = 0.25
  • I값이 1이면 어떤 컴포넌트도 해당 컴포넌트에 의존하지 않지만, 해당 컴포넌트는 다른 컴포넌트에 의존한다는 것.
    • 이 경우, 자신에게 의존하는 컴포넌트가 다른 컴포넌트에 영향을 미치지 않기 때문에 해당 컴포넌트를 변경하지 말아야할 이유가 없음.
    • 반대로, 이 컴포넌트는 다른 컴포넌트들에 의존하고 있으므로 다른 컴포넌트들의 변경에 의해 해당 컴포넌트를 변경할 수도 있다는 것.
  • I값이 0이면 다른 컴포넌트들을 책임지며, 다른 컴포넌트에 의존하지 않는 독립적인 상태라는 뜻.
    • 의존하는 컴포넌트들이 있으므로 변경하기는 어렵지만, 다른 컴포넌트들 때문에 해당 컴포넌트를 변경해야할 일은 없음.
  • SDP에서 컴포넌트의 I 지표는 컴포넌트가 의존하는 다른 컴포넌트들의 I보다 커야한다고 한다. 즉, 의존성 방향으로 갈수록 I 지표 값이 감소해야 한다.
  • 모든 컴포넌트가 안정적이어야 하는 것은 아니다.
    • 모든 컴포넌트가 최고로 안정적인 시스템이라면 변경이 아예 불가능할 것이다. 당연히 바람직한 상황이 아님.
    • 예시) 이상적인 상태
    • 변경 가능한 컴포넌트가 안정된 컴포넌트에 의존하고 있다.
  • 예시) SDP가 위배된 상태
    • 변경 가능한 컴포넌트가 안정된 컴포넌트에 의존하고 있는데, 그 컴포넌트를 '변경 가능하길 희망하는' 컴포넌트에 의존하고 있다.
    • stable의 I 지표가 Flexible의 I 지표보다 낮다.
    • 어떻게 해결할 것인가?
      • 가령, stable이 flexible의 어떤 클래스를 사용하길 희망하는 상황이라고 가정하자.
      • 새로운 모듈을 만들어 해당 클래스의 인터페이스를 만든다. (추상 컴포넌트)
      • Flexible에 해당 인터페이스의 구현체를 구현한다.
      • Flexible의 I를 1로 유지할 수 있어 유연함을 되찾은 상태
    • 추상 컴포넌트
      • 자바나 C#같은 정적 타입 언어 사용 시에는 꼭 필요한 전략
      • 루비나 파이썬 같은 동적 타입 언어에서는 의존성 역전 시 인터페이스가 필요 없다.

 

3. SDP: 안정된 의존성 원칙(stable abstractions principle)

 

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

  • 고수준 아키텍처나 정책 결정과 관련된 소프트웨어는 변동성이 없기를 기대하므로, 반드시 안정된 컴포넌트(I=0)에 위치해야 한다.
  • 반면, 불안정한 컴포넌트(I=1)는 반드시 변동성이 큰 소프트웨어만을 포함해야 한다.
    • 문제 : 그러면 고수준 아키텍처의 소스코드는 어떻게 수정하는가? 유연성이 떨어지는데.
    • 정답 : OCP 원칙에서 그 답을 찾을 수 있다.

 

(2) SDP?

  • 안정성과 추상화 정도 사이의 관계를 정의한다.
  • 안정된 컴포넌트는 추상 컴포넌트여야 하며, 안정성이 컴포넌트를 확장하는 일을 방해해서는 안된다. -> 인터페이스와 추상클래스로 구성되면 쉽게 확장 가능해짐.
  • 불안정한 컴포넌트는 구체 컴포넌트여야 하며, 컴포넌트가 불안정하므로 내부의 구체적인 코드를 쉽게 변경할 수 있다.
  • SAP와 SDP를 결합하면 컴포넌트에 대한 DIP나 마찬가지다. SDP에서는 의존성이 반드시 안정성의 방향으로 향해야 하며, SAP에서는 안정성이 결국 추상화를 의미한다고 말한다.

 

(3) 추상화 정도

 

3-1. 추상화 정도 측정하기

  • 측정 지표
    • Nc : 컴포넌트의 클래스 개수
    • Na : 컴포넌트의 추상 클래스와 인터페이스 개수
    • A : 추상화 정도. A = Na/Nc
  • A지표는 0과 1 사이의 값을 갖는다. A가 0이면 컴포넌트에는 추상 클래스가 하나도 없다는 것을 의미하며, A가 1이면 컴포넌트는 오로지 추상 클래스만을 포함한다는 뜻.

3-2. 주계열 (천문학 용어, 관측된 모든 별 중의 90%가 표시되어 있는 좁은 띠)

 

  • 최고로 안정적이며 추상화된 컴포넌트는 좌측 상단 (0, 1)에 위치한다.
  • 최고로 불안정하며 구체화된 컴포넌트는 우측 하단 (1, 0)에 위치한다.
  • 추상 클래스 (혹은 인터페이스)는 다른 추상 클래스로부터 파생해서 만들기 때문에 추상적이면서도 의존성을 가진다. 이 경우 최고로 추상적이나 최고로 안정적이지는 않은 상태.
  • 따라서 모든 컴포넌트가 (0, 1) 또는 (1, 0)에 위치해야 한다는 규칙을 강요할 수는 없으므로, A/I 그래프 상에서 컴포넌트가 위치할 수 있는 합리적인 지점이 있으리라고 가정하고, 배제해야할 구역을 찾는 방식으로 추론할 수 있다.

 

 

  • 고통의 구역
    • 매우 안정적이며 구체적인 컴포넌트
    • 추상적이지 않으므로 확장할 수 없고, 안정적이므로 변경하기도 어려우므로 바람직하지 않은 상태
    • 제대로 설계된 컴포넌트라면 (0, 0) 근처에는 위치하지 않을 거라고 보는 것이 일반적
    • 데이터베이스 스키마는 변동성이 높기로 악명이 높으며, 극단적으로 구체적이며, 많은 컴포넌트가 여기에 속함
    • 혹은 String 컴포넌트 같이 구체적인 유틸리티 라이브러리도 여기에 속함. 단, String 같이 변동성이 없는 컴포넌트는 (0, 0) 구역에 위치해 있더라도 해롭지 않다. 반면 데이터베이스 스키마 같이 변동성이 크면 클수록 수반되는 고통은 더욱 고통스럽다.
  • 쓸모없는 구역
    • 최고로 추상적이지만 누구도 의존하지 않는 상태이므로 아무 쓸모가 없으며, 이 영역에 존재하는 소프트웨어 엔티티는 폐기물과 같다.
  • 배제 구역 벗어나기
    • 변동성이 큰 컴포넌트 대부분은 두 배제영역에 닿지 않는 주계열 영역으로 이동시켜야 한다.
    • 컴포넌트가 위치할 수 있는 가장 바람직한 지점은 주계열의 두 종점이지만, 소수의 일부 컴포넌트는 완전히 추상적이거나 완전히 안정적일 수 없다. 이 경우는 주계열과 최대한 가깝게 위치할 때 이상적이다.
  • 주계열과의 거리
    • 이상적인 상태로부터 컴포넌트가 얼마나 멀리 떨어져 있는지 측정하는 지표
      • D4거리 : D = |A + 1 - 1|. 지표가 0이면 주계열 바로 위에 위치, 1이면 가장 멀리 위치하는 것.

  • 표준 편차가 Z=1인 영역을 벗어난 경우, 의존하는 컴포넌트가 없음에도 추상적이거나 (쓸모 없음), 의존하는 컴포넌트가 많은데도 구체적인 경우(고통스러움)
  • 각 컴포넌트의 D 값을 시간에 따라 그려보는 것으로 추적해볼 수도 있다.