Posts [클린아키텍처] 12 ~ 14장
Post
Cancel

[클린아키텍처] 12 ~ 14장

clean-architecture-book

‘클린 아키텍처’ 기술 서적에 대해 학습했던 내용을 정리하기 위한 목적의 TIL 포스팅입니다.🙆‍♂️

4부 컴포넌트 원칙

  • SOLID 원칙이 벽과 방에 벽돌을 배치하는 방법을 알려준다면, 컴포넌트 원칙은 빌딩에 방을 배치하는 방법을 설명해준다.
  • 큰 빌딩과 마찬가지로 대규모 소프트웨어 시스템은 작은 컴포넌트들로 만들어진다.

12장 컴포넌트

컴포넌트가 무엇인지 컴포넌트의 역사에 대해 설명하는 내용이다. 크게 중요한 내용은 많지 않아서 컴포넌트가 무엇인지 그리고 어떻게 소프트웨어가 발전하게 되었는지를 가볍게 보면 좋다.

  • 컴포넌트는 시스템의 구성요소로 배포할 수 있는 가장 작은 단위다.
    • 자바 - jar
    • 루비 - gem 파일
    • 닷넷 - DLL
  • 컴파일형 언어에서 컴포넌트는 바이너리 파일의 결합체다. 인터프리형 언어의 경우 소스 파일의 결합체이다.
  • 모든 언어에서 컴포넌트는 배포할 수 있는 단위 입자다.
  • 여러 컴포넌트를 서로 링크하여 실행 가능한 단일 파일로 생성 가능하다.
  • 또는 여러 컴포넌트를 서로 묶어서 war 파일과 같은 단일 아카이브로 만들 수도 있다.
  • 또는 컴포넌트 각각을 jar 나 dll 같이 동적으로 로드할 수있는 플러그링닝나 exe 파일로 만들어서 독립적으로 배포할 수도 있따.
  • 잘 설계된 컴포넌트라면 반드시 독립적으로 배포 간으한, 따라서 독립적으로 개발 가느한 능력을 갖춰야 한다.

컴포넌트의 간략한 역사

  • 프로그래밍 초창기에는 프로그램을 로드할 메모리의 위치를 정하는 일이 프로그래머가 가장 먼저 결정해야 하는 사항 중 하나였다.
    • 이 시절엔 프로그램 위치가 한 번 결정되면, 재배치가 불가능했다.
  • 이러한 구시대에는 프로그래머가 라이브러리 함수의 소스 코드를 애플리케이션 코드에 직접 포함시켜 단일 프로그램으로 컴파일하여 해당 라이브러리에 접근했다.
    • 라이브러리는 바이너리가 아닌 소스 코드 형태로 유지되었다.
  • 이 시대엔 장치는 느리고 메모리는 너무 비싸서 자원이 한정적이었기에, 이러한 접근법은 문제가 있었다.
    • 이 시대엔 메모리가 너무 작아서 소스 코드 전체를 메모리에 상주시킬 수 없었고, 컴파일러는 소스 코드 전체를 여러번에 걸쳐서 읽어야했다.
    • 이 과정은 엄청오래 걸렸다.
  • 따라서 함수 라이브러리의 소스 코드를 애플리케이션 코드로부터 분리하고, 함수 라이브러리를 개별적으로 컴파일해, 컴파일된 바이너리를 메모리의 특정 위치에 로드했다. (102p-그림12.1 초기의 메모리 배치 이미지 참고)
  • 하지만, 이 방식도 초기에 할당된 메모리보다 애플리케이션이 더 커지게 되자, 애플리케이션을 두 개의 세그먼트로 분리하여 함수 라이브러리 공간을 사이에 두고 오가며 동작하게 배치해야 했다.
  • 하지만 이것은 분명 지속 가능한 방법이 아니었다.

image

재배치성

  • 해결책은 재배치가 가능한 바이너리였다.
  • 바로 로더를 이용해서 메모리에 재배치할 수 있는 형태의 바이너리를 생성하도록 컴파일러를 수정하자는 것이었다.
  • 프로그래머는 함수 라이브러리를 로드할 위치와 애플리케이션을 로드할 위치를 로더에게 지시할 수 있게 되었다.
  • 로더는 바이너리르 입력받은 후, 단순히 하나씩 차례로 메모리로 로드하면서 재배치하는 작업을 처리하였고, 프로그래머는 필요한 함수만을 로드할 수 있게 되었다

  • 컴파일러는 재배치 가능한 바이너리 안의 함수 이름을 메타데이터 형태로 생성하도록 수정되었다.
    • 프로그램이 라이브러리 함수를 호출한다면 외부 참조(external reference)로 생성했다.
    • 프로그램이 라이브러리 함수를 정의한다면 외부 정의(external definition)로 생성했다.
  • 이렇게 함으로써 외부 정의를 로드할 위치가 정해지기만 하면 로더가 외부 참조를 외부 정의에 링크시킬 수 있게 된다. 이를 링킹 로더라고 한다.

링커

  • 1970년대 초가 되자 프로그램이 커지게 되고, 링킹 로더는 프로그램을 로드하는데 매우 긴 시간이 걸렸다.
  • 이를 해결하고자 로드링크가 분리되었다.
  • 프로그래머가 느린 부분, 즉 링크 과정을 맡았는데, 링커라는 별도의 애플리케이션으로 이 작업을 처리하도록 만들었다.
    • 링커는 링크가 완료된 재배치 코드를 만들어 주었고, 그 덕분에 로더의 로딩 과정이 아주 빨라졌다. 그리고 한 번 만들어준 실행 파일은 언제라도 빠르게 로드할 수 있게 되었다.
  • 하지만 또 1980년대가 되어 프로그래머는 C나 또 다른 고수준 언어를 사용하기 시작했고 프로그램도 더 커지게 되었다.
    • 그러다보니 컴파일-링크 시간이 병목 구간이되어 또 다시 전체 모듈을 컴파일하는데 엄청난 시간이 들게 되었다… (끊이지 않는 반복..)
  • 이후 기술이 발전하여 디스크 속도가 증가하고 액티브X와 공유 라이브러리, jar 파일이 등장했으며, 다수의 .jar 또는 공유 라이브러리를 순식간에 서로 링크한 후, 링크가 끝난 프로그램을 실행할 수 있게 되었다.
  • 이렇게 컴포넌트 플러그인 아키텍처가 탄생했다.

결론

  • 런타임에 플러그인 형태로 결합할 수 있는 동적 링크 파일이 이 책에서 말하는 소프트웨어 컴포넌트에 해당한다.

13장 컴포넌트 응집도

  • 어떤 클래스를 어느 컴포넌트에 포함시켜야 할 지는 중요한 결정이다.
  • 이 장에선 컴포넌트 응집도와 관련된 세 가지 원칙을 논의한다.
    • REP(Reuse/Release Equivalence Principle) - 재사용/릴리스 등가 원칙
    • CCP(Common Closure Principle) - 공통 폐쇄 원칙
    • CRP(Common Reuse Principle) - 공통 재사용 원칙

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

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

  • 소프트웨어 컴포넌트가 릴리스 절차를 통해 추적 관리되지 않거나 릴리스 번호가 부여되지 않는다면 해당 컴포넌트를 재사용하고 싶어도 할 수도 없고, 하지도 않을 것이다.
  • 릴리스 번호가 없다면 재사용 컴포넌트들이 서로 호환되는지 알 수가 없다.
  • 개발자들은 릴리스 변경사항을 통해 기존 버전을 쓸지 아니면 새로운 릴리스 변경사항을 적용할지를 결정 가능하다.
  • 이 원칙을 소프트웨어 설계와 아키텍처 관점에서 보면 단일 컴포넌트는 응집성 높은 클래스와 모듈로 구성되어야 함을 뜻한다.
    • 단순히 뒤죽박죽 임의로 선택된 클래스와 모듈로 구성되선 안된다.
  • 컴포넌트를 구성하는 모든 모듈은 서로 공유하는 중요한 테마나 목적이 있어야 한다.
  • 하나의 컴포넌트로 묶인 클래스와 모듈은 반드시 함께 릴리스 할 수 있어야 한다.
  • 이 원칙의 약점은 다음에 다룰 두 원칙이 지닌 강점을 통해 충분히 보완할 수 있다.
    • 실제로 CCP와 CRP 는 REP를 엄격하게, 하지만 제약을 가하는 측면에서 정의한다.

CCP: 공통 폐쇄 원칙

동일한 이유로 동일한 시점에 변경되는 클래스를 같은 컴포넌트로 묶어라. 서로 다른 시점에 다른 이유로 변경되는 클래스는 다른 컴포넌트로 분리하라.

  • 이 원칙은 단일 책임 원칙(SRP)을 컴포넌트 관점에서 다시 쓴 것이다.
    • 공통 폐쇄 원칙 CCP 에서도 마찬가지로 단일 컴포넌트는 변경의 이유가 여러 개 있어선 안된다고 말한다.
  • 유지보수성은 재상용성보다 훨씬 중요하다.
    • 애플리케이션 코드가 반드시 변경되어야 한다면, 변경 지점들이 여러 컴포넌트에 분산되어 있는 것보단 한 컴포넌트에 존재하는 것이 낫다.
    • 만약 변경 지점을 단일 컴포넌트로 제한할 수 있다면, 해당 컴포넌트만 재배포하면 되고, 변경된 컴포넌트에 의존하지 않는 다른 컴포넌트는 다시 검증하거나 배포할 필요가 없다.
  • CCP는 위와 같은 이유로 변경될 가능성이 존재하는 클래스는 모두 한 곳으로 묶을 것을 권한다.
    • 물리적 또는 개념적으로 강하게 결합되어 항상 함께 변경되는 클래스들은 하나의 컴포넌트에 속해야 한다.
    • 이를 통해 소프트웨어를 릴리스, 재검증, 배포하는 일과 관련된 작업량을 최소화할 수 있다.
  • 이 원칙은 개방 폐쇄 원칙(OCP)과도 밀접하게 관련되어 있다.
    • 실제로 CCP의 폐쇄는 OCP의 폐쇄와 그 뜻이 같다.
    • 100%의 폐쇄는 불가능한데 변경 가능성이 있거나 과거에 발생했던 대다수의 공통적인 변경사항에 대해서 클래스가 닫혀있도록 설계한다.
  • CCP에선 동일한 유형의 변경에 대해 닫혀있는 클래스들을 하나의 컴포넌트로 묶음으로써 OCP에서 얻은 교훈을 확대 적용한다.
    • 따라서 변경이 필요한 요구사항이 발생할 경우, 그 변경이 영향을 주는 컴포넌트들이 최소한으로 한정될 가능성이 확실히 높아진다.

SRP 와의 유사성

  • CCP는 컴포넌트 수준의 SRP다.
  • SRP에선 서로 다른 이유로 변경되는 메서드를 서로 다른 클래스로 분리하라고 말한다.
  • CCP에선 서로 다른 이유로 변경되는 클래스를 서로 다른 컴포넌트로 분리하라고 말한다.
    • 두 원칙은 모두 아래와 같은 교훈으로 요약 가능하다.

동일한 시점에 동일한 이유로 변경되는 것들을 한데 묶어라. 서로 다른 시점에 다른 이유로 변경되는 것들은 서로 분리하라.

CRP: 공통 재사용 원칙

컴포넌트 사용자들을 필요하지 않는 것에 의존하게 강요하지 말라.

  • 공통 재사용 원칙 CRP도 클래스와 모듈을 어느 컴포넌트에 위치시킬지 결정할 때 도움되는 원칙이다.
  • CRP에선 같이 재사용되는 경향이 있는 클래스와 모듈들은 같은 컴포넌트에 포함해야 한다고 말한다.
    • 개별 클래스가 단독으로 재사용되는 경우는 거의 없으며 대체로 재사용 가능한 클래스는 재사용 모듈의 일부로써 해당 모듈의 다른 클래스와 상호작용하는 경우가 많다.
    • CRP 에선 이런 클래스들이 동일한 컴포넌트에 포함되어야 한다고 말한다.
  • 간단한 예시로 container와 iterator 클래스는 서로 강하게 결합되어 있기 때문에 함께 재사용된다. 따라서 동일한 컴포넌트에 위치해야한다.
  • 또한, CPR는 동일한 컴포넌트로 묶어서는 안되는 클래스가 무엇인지도 말해준다.
    • 어떤 컴포넌트가 다른 컴포넌트를 사용하면, 두 컴포넌트 사이에 의존성이 생긴다.
    • 사용하는 클래스에서 사용되는 클래스에서 단 하나의 클래스만 사용한다고 해도 의존성은 약해지지 않는다.
    • 이런 의존성으로 인해 사용되는 컴포넌트가 변경될 때 마다 사용하는 컴포넌트도 변경해야할 가능성이 높다.
    • 따라서 의존하는 컴포넌트가 있다면 해당 컴포넌트의 모든 클래스에 대해 의존함을 확실히 인지해야 한다.
  • 따라서 CRP는 어떤 클래스를 한데 묶어도 되는지보단, 어떤 클래스를 한데 묶어선 안되는지에 대해 훨씬 더 많은 것을 이야기한다.
    • CRP는 강하게 결합되지 않은 클래스들을 동일한 컴포넌트에 위치시켜서는 안 된다고 말한다.

ISP와의 관계

  • CRP는 인터페이스 분리 원칙(ISP)의 포괄적인 버전이다.
  • ISP는 사용하지 않은 메서드가 있는 클래스에 의존하지 말라고 조언한다.
  • CRP는 사용하지 않은 클래스를 가진 컴포넌트에 의존하지 말라고 조언한다.
  • 위 두 조언은 다음 한 문장으로 요약할 수 있다.

필요하지 않는 것에 의존하지 말라.

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

  • 응집도에 관한 세 원칙은 서로 상충된다.
  • REP와 CCP는 포함(inclusive) 원칙이며, 컴포넌트를 더욱 크게 만든다.
  • CRP는 배제(exclusive)원칙이며, 컴포넌트를 더욱 작게 만든다.
  • 따라서 이 원칙들이 균형을 이루는 방법을 찾아야 한다.

image

  • 위 균형 다이어그램에서 다이어그램의 각 변은 반대쪽 꼭지점에 있는 원칙을 포기했을 때 감수해야 할 비용을 나타낸다.
    • REP와 CRP에만 중점을 두면, 사소한 변경에 너무 많은 컴포넌트에 영향을 미친다.
    • CCP와 REP에만 과도하게 집중하면 불필요한 릴리스가 너무 빈번해진다.
    • CRP와 CCP에만 집중하게 되면 재사용성이 떨어지게 된다.
  • 뛰어난 아키텍트라면 이 균형 삼각형에서 현재 개발팀이 현재 관심을 기울이는 부분을 충족시키는 위치를 찾아야 하며, 또한 시간이 흐르면서 개발팀이 주의를 기울이는 부분 역시 변한다는 사실도 이해하고 있어야 한다.
    • 예를 들어, 프로젝트 초기에는 CCP가 REP보다 훨씬 더 중요한데, 개발 가능성이 재사용성보다 더욱 중요하기 때문이다.
    • 일반적으로 프로젝트는 삼각형의 오른쪽에서 시작하는 편인데, 프로젝트가 성숙하고 그 프로젝트로부터 파생된 또 다른 플조ㅔㄱ트가 시작되면, 점차 왼쪽으로 이동해 간다.
    • 즉, 프로젝트 컴포넌트 구조는 시간과 성숙도에 따라 변한다는 뜻이다.

결론

  • 어느 클래스들을 묶어서 컴포넌트로 만들지를 결정할 때, 재사용성과 개발 가능성이라는 상충하는 힘을 반드시 고려해야 한다.
  • 이들 사이에서 애플리케이션의 요구에 맞게 균형을 잡는 일은 중요하다. 심지어 이 균형점은 항상 유동적이다.
    • 즉, 두 힘을 현재 상황에 맞게 잘 분배했더라도, 내년엔 맞지 않을 수도 있다.
    • 결과적으로 시간이 흐름에 따라 프로젝트의 초점이 개발가능성에서 재사용성으로 바뀌고, 그에 따라 컴포넌트를 구상하는 방식도 조금씩 흐트러지고 또 진화한다.

14장 컴포넌트 결합

  • 지금부터 다루게 될 세 가지 원칙은 컴포넌트 사이의 관계를 설명한다.
  • 이 장에서도 마찬가지로 개발 가능성과 논리적 설계 사이의 균형을 다룬다.

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

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

  • 동일한 소스코드를 여럿이 건들게 되면, 충돌이 자주 발생하게 되고 망가진 부분을 고치느라 애를 먹을 것이다.
  • 이때 해결책으론 두 가지 방법이 있다.
    • 1)주단위 빌드(Weekly Build)
    • 2)의존성 비순환 원칙(Acyclic Dependencies Principle, ADP)

1) 주단위 빌드(Weekly Build)

  • 중간 규모 프로젝트에서 흔히 사용된다.
  • 월-목엔 각자 개발 후 금요일에 통합한다.
    • 장점: 빠른 피드백 및 서로 신경 안쓰고 편하게 개발 가능
    • 단점: 프로젝트 통합시 오래걸림
  • 위의 단점으로 인해 시간이 지날수록 통합에드는 시간이 계속 늘어나고 위기를 초래한다.

2) 순환 의존성 제거하기

  • 의존성 구조에 순환이 절대 생겨선 안된다. 그렇게 되면 숙취증후군이…

image

  • 위 이미지는 전형적인 컴포넌트 다이어그램이다. 컴포넌트 의존성 구조는 방향 그래프 임에 주의하자.
  • 어느 컴포넌트에서 시작하더라도, 의존성 관계를 따라가면서 최초의 컴포넌트로 되돌아갈 수 없게 해야 한다. (비순환 방향 그래프)
    • Presenters를 담당하는 팀에서 이 컴포넌트의 새로운 릴리스를 만들면 무슨일이 벌어질지 생각해보자.
    • 이 릴리스에 영향받는 팀은 쉽게 찾을 수 있다.
    • 의존성 화살표를 거꾸로 따라가면 된다.
    • 즉, View 와 Main 컴포넌트 둘 다 영향 받는다.
    • 이 두 컴포넌트를 작업중인 개발자라면, Presenters의 새로운 릴리스와 자신의 작업물을 언제 통합할지를 반드시 결정해야 한다.
  • 또한 Main은 영향 받는 컴포넌트가 전혀 없어서 쉽게 변경 가능하다.
  • 시스템 전체를 릴리스해야 하면 상향식으로 진행된다.
    • Entities(컴파일, 테스트, 릴리스) → Database, Interators, → …
  • 컴포넌트 구성요소 간 의존성을 파악하고 있으면 시스템을 빌드하는 방법과 순서를 알 수 있다

🙌나의 예시

  • 현재 고객사 인사정보시스템으로부터 코드, 조직, 구성원 데이터를 동기화 시켜주는 람다 애플리케이션을 개발 중에 있다.
  • 멀티 모듈로 구성되어 있으며, 현재 의존성 그래프는 아래와 같다. (실제 app-samsung, app-lg 는 단순히 예시이며, 고객사 모듈을 뜻한다.)

image

  • 의존성 그래프가 단방향으로 구성되어 있으며, 비순환 형태이다. (어느 모듈에서 시작하더라도 최초 컴포넌트로 돌아갈 수 없는)
  • 또한 위와 같이 설계한 이유는 고객사 모듈(ex. app-samgsung, app-lg) 간의 클래스들을 서로 격리시키기 위함이다.
  • 추가적으로 전체 프로세스는 람다 함수의 페이로드 이벤트에 기반하여 매칭되는 고객사 SyncManager 를 팩토리 메서드를 통해 생성하여 실제 synchronize 메서드를 호출하여 동기화가 이뤄지는 방식이다.

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

image

  • 컴포넌트 간의 의존성으로 인해 순환 컴포넌트와 관련된 개발자들은 모두 서로에게 얽매일 것이다.
    • Database 컴포넌트 릴리스 -> Entities 와 반드시 호환 -> Authorize 와 호환되도록 신경써야 한다.
    • 그렇기에 릴리스하기 훨씬 어려워진다. 그러다보면 숙취증후군이…
  • 테스트할때 또한 의존된 컴포넌트 체인을 따라 모두 빌드하고 통합해야 하는데 문제가 발생하게 되고 받아들이기도 힘들어진다.
  • 이처럼 순환이 생기면 컴포넌트를 분리하기 상당히 어려워지고, 단위 테스트 및 릴리스도 굉장히 어려워지고 에러도 쉽게 발생한다. 게다가 모듈의 개수가 많아짐에 따라 빌드 관련 이슈는 기하급수적으로 증가한다.
  • 추가적으로 컴포넌트에 대한 빌드 순서를 파악하기도 상당히 힘들어진다. 올바른 순서라는 것 자체가 없을 수 있다.

순환 끊기

  • 1)의존성 역전(DIP)을 적용한다. (고수준의 Permissions 인터페이스로 저수준이 고수준을 참조하도록 변경)

image

  • 2)Entities와 Authorizer가 모두 의존하는 새로운 컴포넌트를 만든다. 그리고 모두 의존하는 클래스들을 새로운 컴포넌트로 이동시킨다.
    • 이러한 과정을 통해 요구사항이 변겨오디면 컴포넌트 구조도 변경되게 된다. 그러기에 주기적으로 순환이 발생하는지 관찰 후 끊어줘야 한다.

image

하향식 설계

  • 컴포넌트는 시스템에서 가장 먼저 설계할 수 있는 대상이 아니기 때문에, 하향식으로 설계될 수 없다.
  • 컴포넌트 의존성 다이어그램은 애플리케이션의 기능을 의미하지는 않는다.
  • 오히려 애플리케이션의 빌드 가능성(buuildability)과 유지보수성(maintainability)을 보여주는 지도(map)와 같다.
  • 따라서, 아무런 클래스도 설계하지 않은 상태에서 컴포넌트 의존성 구조를 설계하려고 하면 실패할 것이다.

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

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

  • 설계는 결코 정적일 수 없으며 변경은 불가피하다.
  • 그러다보니 일부는 변동성을 지니도록 설계된다.
  • 그래서 변경이 쉽지 않은 컴포넌트가 변동이 예상되는 컴포넌트에 의존하게 만들어선 절대 안된다. 한 번 의존하게 되면 변동성이 큰 컴포넌트도 결국 변경이 어려워진다.
  • 안정된 의존성 원칙을 준수하면 변경하기 어려운 모듈(백엔드 도메인 모듈, 프론트 위젯 모듈)이 변경하기 쉽게 만들어진 모듈(백엔드 웹 모듈, 프론트 클라이언트 모듈)에 의존하지 않도록 만들수도 있다.

안정성

  • 변경을 만들기 위해 필요한 작업량과 관련된다.
    • 변경하는데 많은 작업이 필요 == 많은 곳에서 의존된다 == 안정적이다.
  • 소프트웨어 컴포넌트를 변경하기 어렵게 만드는 확실한 방법은 수많은 컴포넌트가 해당 컴포넌트에 의존하게 만드는 것이다.

image

  • X는 세 컴포넌트를 책임지고 있다. 그러나, X가 변경되도록 마들 수 있는 외적인 영향은 없기 때문에 독립적이다.

image

  • Y는 상당히 불안정한 컴포넌트이다. Y에 의존하는 컴포넌트는 없기 떄문에, 책임성이 없으며, 의존적이라고 볼 수 있다.

안정성 지표

  • 불안정성 : I = Fan-out / (Fan-in + Fan-out) , I =1이면, 최고로 불안정한 컴포넌트, I = 0이면, 최고로 안정한 컴포넌트다.
    • Fan-in: 안으로 들어오는 의존성. 이 지표는 컴포넌트 내부의 클래스에 의존하는 컴포넌트 외부의 클래스 개수를 나타낸다.
    • Fan-out: 바깥으로 나가는 의존성. 이 지표는 컴포넌트 외부의 클래스에 의존하는 컴포넌트 내부의 클래스 개수를 나타낸다.
  • 의존성 방향으로 갈수록 I지표 값이 감소해야 한다.

🙌나의 예시

image

  • 위에서 언급된 동기화 람다 함수 멀티 모듈의 예시로 보면 위와 같다.
  • 의존성 방향으로 갈수록 I지표 값이 감소하는 것을 볼 수 있다.
    • 즉, 의존성 방향으로 갈 수록 많은 곳에서 의존되는 것을 확인 가능하다.

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

  • 모든 컴포넌트가 안정적이라면, 변경은 불가능하다.
  • 포인트는 의존성 방향 안쪽으로 I지표 값이 감소하지 않도록 구조를 개선해야 한다.
    • DIP 를 도입하여 의존성을 역전시켜서 이를 유지할 수 있다. (p130. 그림 14.11 참고)

추상 컴포넌트

  • 오로지 인터페이스만을 포함하는 컴포넌트를 생성하는 방식인 추상 컴포넌트는 상당히 안정적이며, 따라서 덜 안정적인 컴포넌트가 의존할 수 있는 이상적인 대상이다.

3) SAP: 안정된 추상화 원칙 (Stable Abstractions Principle)

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

  • 시스템에는 자주 변경해서는 절대로 안되는 소프트웨어도 있다. 고수준 아키텍처나 정책 결정과 관련된 소프트웨어가 그 예다.
  • 따라서 시스템에서 고수준 정책을 캡슐화하는 소프트웨어는 반드시 안정된 컴포넌트(I = 0)에 위치해야 한다.
  • 하지만 고수준 정책을 안정된 컴포넌트에 위치시키면, 그 정책을 포함하는 소스 코드는 수정하기가 어려워진다. 이로 인해 시스템 전체 아키텍처가 유연성을 잃는다.
  • 그렇다면 어떻게 컴포넌트가 최고로 안정된 상태이면서도(I = 0) 동시에 변경에 충분히 대응할 수 있을 정도로 유연하게 만들 수 있을까?
  • 해답은 개방 폐쇄 원칙(OCP)에서 찾을 수 있다. 추상 클래스를 활용해서 말이다.

안정된 추상화 원칙

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

추상화 정도 측정하기

  • 추상화 정도를 지표로 표현한 것이다.
    • NC: 컴포넌트의 클래스 개수
    • Na: 컴포넌트의 추상 클래스와 인터페이스의 개수
    • A: 추상화 정도. A = Na / Nc(A가 0이면 추상 클래스가 한개도 없고, 1이면 오로지 추상 클래스만 있음)

주계열

  • 아래 이미지는 안정성(I)과 추상화 정도(A) 사이의 관계 그래프다.
    • 최고로 안정적이며 추상화된 컴포넌트는 (0, 1)에 위치한다.
    • 최고로 불안정하며 구체화된 컴포넌트는 (1, 0)에 위치한다.

image

  • 모든 컴포넌트가 이 두 지점에 위치하는 것은 아니다.
  • 아래 그림의 궤적은 컴포넌트가 절대로 위치해서는 안 되는 영역, 배제할 구역(Zone of Exclusion)이다.

image

고통의 구역

  • (0, 0) 주변 구역에 위치한 컴포넌트는 매우 안정적이며 구체적이다. 뻣뻣한 상태이다. 추상적이지 않아서 확장할 수 없고, 안정적이므로 변경하기 상당히 어렵다.
  • 하지만 변동성이 없는 컴포넌트는 (0, 0) 구역에 위치하더라고 해롭지 않다. 변동될 가능성이 없기 때문이다.

쓸모없는 구역

  • (1, 1) 주변의 컴포넌트는 최고로 추상적이지만, 누구도 그 컴포넌트에 의존하지 않는다. 이러한 컴포넌트는 쓸모가 없다. 따라서 이 구역은 쓸모없는 구역(Zone of Uselessness)이라 부른다.

배제 구역 벗어나기

  • 변동성이 큰 컴포넌트 대부분은 두 배제 구역으로부터 가능한 한 멀리 떨어뜨려야 한다.
  • 최대한 멀리 떨어진 점의 궤적은 (1, 0)(0, 1)을 잇는 선분이다. 저자는 주계열(Main Sequence)이라 부른다.
  • 주계열 위 또는 가깝게 위치해야 하며, 이렇게 위치하면 ‘너무 추상적’이지도 않고, 추상화 정도에 비해 ‘너무 불안정’하지도 않다.

주계열과의 거리

  • 주계열과의 거리를 지표화한 것으로, 주계열에 대체로 일치하도록 설계되었는지를 분석할 수 있다.

🙌나의 예시

  • 위에서 언급한 동기화 람다 함수를 기반으로 추상화 지표로 A/I 그래프를 그려보면 아래 이미지와 같다.

image

  • app-samsung 컴포넌트는 변동성이 큰 모듈이다보니 두 배제 구역으로부터 멀리 떨어뜨려놓아야 하지만 그렇지 못하다.
    • 추상 클래스 및 인터페이스를 활용하여 추상화 정도를 더 높일 필요가 있다.
  • app-core 컴포넌트는 고통의 구역에 속해있지만, 변동성이 거의 없는 컴포넌트이므로 크게 해롭지 않다.
  • app-main 컴포넌트는 주계열선에 가깝게 위치해있다.

결론

  • 이 장에서 설명하는 의존성 관리 지표 는 설계의 의존성과 추상화 정도가 내가 ‘훌륭한’ 패턴이라고 생각하는 수준에 얼마나 부합하는지를 측정한다.
  • 하지만 이러한 지표는 신이 아니고 아무리 해도 불완전하다는 것을 참고하자.

Reference

This post is licensed under CC BY 4.0 by the author.

[클린아키텍처] 7 ~ 11장

[클린아키텍처] 15장 아키텍처란?