‘클린 아키텍처’ 기술 서적에 대해 학습했던 내용을 정리하기 위한 목적의 TIL 포스팅입니다.🙆♂️
4부 - 컴포넌트 원칙
- SOLID 원칙이 벽과 방에 벽돌을 배치하는 방법을 알려준다면, 컴포넌트 원칙은 빌딩에 방을 배치하는 방법을 설명해준다.
- 큰 빌딩과 마찬가지로 대규모 소프트웨어 시스템은 작은 컴포넌트들로 만들어진다.
12장 - 컴포넌트
컴포넌트가 무엇인지 컴포넌트의 역사에 대해 설명하는 내용이다. 크게 중요한 내용은 많지 않아서 컴포넌트가 무엇인지 그리고 어떻게 소프트웨어가 발전하게 되었는지를 가볍게 보면 좋다.
- 컴포넌트는 시스템의 구성요소로 배포할 수 있는 가장 작은 단위다.
- 자바 - jar
- 루비 - gem 파일
- 닷넷 - DLL
- 컴파일형 언어에서 컴포넌트는 바이너리 파일의 결합체다. 인터프리형 언어의 경우 소스 파일의 결합체이다.
- 모든 언어에서 컴포넌트는 배포할 수 있는 단위 입자다.
- 여러 컴포넌트를 서로 링크하여 실행 가능한 단일 파일로 생성 가능하다.
- 또는 여러 컴포넌트를 서로 묶어서 war 파일과 같은 단일 아카이브로 만들 수도 있다.
- 또는 컴포넌트 각각을 jar 나 dll 같이 동적으로 로드할 수있는 플러그링닝나 exe 파일로 만들어서 독립적으로 배포할 수도 있따.
- 잘 설계된 컴포넌트라면 반드시 독립적으로 배포 간으한, 따라서 독립적으로 개발 가느한 능력을 갖춰야 한다.
컴포넌트의 간략한 역사
- 프로그래밍 초창기에는 프로그램을 로드할 메모리의 위치를 정하는 일이 프로그래머가 가장 먼저 결정해야 하는 사항 중 하나였다.
- 이 시절엔 프로그램 위치가 한 번 결정되면, 재배치가 불가능했다.
- 이러한 구시대에는 프로그래머가 라이브러리 함수의 소스 코드를 애플리케이션 코드에 직접 포함시켜 단일 프로그램으로 컴파일하여 해당 라이브러리에 접근했다.
- 라이브러리는 바이너리가 아닌 소스 코드 형태로 유지되었다.
- 이 시대엔 장치는 느리고 메모리는 너무 비싸서 자원이 한정적이었기에, 이러한 접근법은 문제가 있었다.
- 이 시대엔 메모리가 너무 작아서 소스 코드 전체를 메모리에 상주시킬 수 없었고, 컴파일러는 소스 코드 전체를 여러번에 걸쳐서 읽어야했다.
- 이 과정은 엄청오래 걸렸다.
- 따라서 함수 라이브러리의 소스 코드를 애플리케이션 코드로부터 분리하고, 함수 라이브러리를 개별적으로 컴파일해, 컴파일된 바이너리를 메모리의 특정 위치에 로드했다. (102p-그림12.1 초기의 메모리 배치 이미지 참고)
- 하지만, 이 방식도 초기에 할당된 메모리보다 애플리케이션이 더 커지게 되자, 애플리케이션을 두 개의 세그먼트로 분리하여 함수 라이브러리 공간을 사이에 두고 오가며 동작하게 배치해야 했다.
- 하지만 이것은 분명 지속 가능한 방법이 아니었다.
재배치성
- 해결책은 재배치가 가능한 바이너리였다.
- 바로 로더를 이용해서 메모리에 재배치할 수 있는 형태의 바이너리를 생성하도록 컴파일러를 수정하자는 것이었다.
- 프로그래머는 함수 라이브러리를 로드할 위치와 애플리케이션을 로드할 위치를 로더에게 지시할 수 있게 되었다.
로더는 바이너리르 입력받은 후, 단순히 하나씩 차례로 메모리로 로드하면서 재배치하는 작업을 처리하였고, 프로그래머는 필요한 함수만을 로드할 수 있게 되었다
- 컴파일러는 재배치 가능한 바이너리 안의 함수 이름을 메타데이터 형태로 생성하도록 수정되었다.
- 프로그램이 라이브러리 함수를 호출한다면 외부 참조(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)원칙이며, 컴포넌트를 더욱 작게 만든다.
- 따라서 이 원칙들이 균형을 이루는 방법을 찾아야 한다.
- 위 균형 다이어그램에서 다이어그램의 각 변은 반대쪽 꼭지점에 있는 원칙을 포기했을 때 감수해야 할 비용을 나타낸다.
- 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) 순환 의존성 제거하기
- 의존성 구조에 순환이 절대 생겨선 안된다. 그렇게 되면 숙취증후군이…
- 위 이미지는 전형적인 컴포넌트 다이어그램이다. 컴포넌트 의존성 구조는
방향 그래프
임에 주의하자. - 어느 컴포넌트에서 시작하더라도, 의존성 관계를 따라가면서 최초의 컴포넌트로 되돌아갈 수 없게 해야 한다. (비순환 방향 그래프)
- Presenters를 담당하는 팀에서 이 컴포넌트의 새로운 릴리스를 만들면 무슨일이 벌어질지 생각해보자.
- 이 릴리스에 영향받는 팀은 쉽게 찾을 수 있다.
- 의존성 화살표를 거꾸로 따라가면 된다.
- 즉, View 와 Main 컴포넌트 둘 다 영향 받는다.
- 이 두 컴포넌트를 작업중인 개발자라면, Presenters의 새로운 릴리스와 자신의 작업물을 언제 통합할지를 반드시 결정해야 한다.
- 또한 Main은 영향 받는 컴포넌트가 전혀 없어서 쉽게 변경 가능하다.
- 시스템 전체를 릴리스해야 하면 상향식으로 진행된다.
- Entities(컴파일, 테스트, 릴리스) → Database, Interators, → …
- 컴포넌트 구성요소 간 의존성을 파악하고 있으면 시스템을 빌드하는 방법과 순서를 알 수 있다
🙌나의 예시
- 현재 고객사 인사정보시스템으로부터 코드, 조직, 구성원 데이터를 동기화 시켜주는 람다 애플리케이션을 개발 중에 있다.
- 멀티 모듈로 구성되어 있으며, 현재 의존성 그래프는 아래와 같다. (실제 app-samsung, app-lg 는 단순히 예시이며, 고객사 모듈을 뜻한다.)
- 의존성 그래프가 단방향으로 구성되어 있으며, 비순환 형태이다. (어느 모듈에서 시작하더라도 최초 컴포넌트로 돌아갈 수 없는)
- 또한 위와 같이 설계한 이유는 고객사 모듈(ex. app-samgsung, app-lg) 간의 클래스들을 서로 격리시키기 위함이다.
- 추가적으로 전체 프로세스는 람다 함수의 페이로드 이벤트에 기반하여 매칭되는 고객사 SyncManager 를 팩토리 메서드를 통해 생성하여 실제 synchronize 메서드를 호출하여 동기화가 이뤄지는 방식이다.
순환이 컴포넌트 의존성 그래프에 미치는 영향
- 컴포넌트 간의 의존성으로 인해 순환 컴포넌트와 관련된 개발자들은 모두 서로에게 얽매일 것이다.
- Database 컴포넌트 릴리스 -> Entities 와 반드시 호환 -> Authorize 와 호환되도록 신경써야 한다.
- 그렇기에 릴리스하기 훨씬 어려워진다. 그러다보면 숙취증후군이…
- 테스트할때 또한 의존된 컴포넌트 체인을 따라 모두 빌드하고 통합해야 하는데 문제가 발생하게 되고 받아들이기도 힘들어진다.
- 이처럼 순환이 생기면 컴포넌트를 분리하기 상당히 어려워지고, 단위 테스트 및 릴리스도 굉장히 어려워지고 에러도 쉽게 발생한다. 게다가 모듈의 개수가 많아짐에 따라 빌드 관련 이슈는 기하급수적으로 증가한다.
- 추가적으로 컴포넌트에 대한 빌드 순서를 파악하기도 상당히 힘들어진다. 올바른 순서라는 것 자체가 없을 수 있다.
순환 끊기
- 1)의존성 역전(DIP)을 적용한다. (고수준의 Permissions 인터페이스로 저수준이 고수준을 참조하도록 변경)
- 2)Entities와 Authorizer가 모두 의존하는 새로운 컴포넌트를 만든다. 그리고 모두 의존하는 클래스들을 새로운 컴포넌트로 이동시킨다.
- 이러한 과정을 통해 요구사항이 변겨오디면 컴포넌트 구조도 변경되게 된다. 그러기에 주기적으로 순환이 발생하는지 관찰 후 끊어줘야 한다.
하향식 설계
- 컴포넌트는 시스템에서 가장 먼저 설계할 수 있는 대상이 아니기 때문에, 하향식으로 설계될 수 없다.
- 컴포넌트 의존성 다이어그램은 애플리케이션의 기능을 의미하지는 않는다.
- 오히려 애플리케이션의 빌드 가능성(buuildability)과 유지보수성(maintainability)을 보여주는 지도(map)와 같다.
- 따라서, 아무런 클래스도 설계하지 않은 상태에서 컴포넌트 의존성 구조를 설계하려고 하면 실패할 것이다.
2) SDP: 안정된 의존성 원칙 (Stable Dependencies Principle)
안정성의 방향으로 (더 안정된 쪽에) 의존하라
- 설계는 결코 정적일 수 없으며 변경은 불가피하다.
- 그러다보니 일부는 변동성을 지니도록 설계된다.
- 그래서 변경이 쉽지 않은 컴포넌트가 변동이 예상되는 컴포넌트에 의존하게 만들어선 절대 안된다. 한 번 의존하게 되면 변동성이 큰 컴포넌트도 결국 변경이 어려워진다.
- 안정된 의존성 원칙을 준수하면 변경하기 어려운 모듈(백엔드 도메인 모듈, 프론트 위젯 모듈)이 변경하기 쉽게 만들어진 모듈(백엔드 웹 모듈, 프론트 클라이언트 모듈)에 의존하지 않도록 만들수도 있다.
안정성
- 변경을 만들기 위해 필요한 작업량과 관련된다.
- 변경하는데 많은 작업이 필요 == 많은 곳에서 의존된다 == 안정적이다.
- 소프트웨어 컴포넌트를 변경하기 어렵게 만드는 확실한 방법은 수많은 컴포넌트가 해당 컴포넌트에 의존하게 만드는 것이다.
- X는 세 컴포넌트를 책임지고 있다. 그러나, X가 변경되도록 마들 수 있는 외적인 영향은 없기 때문에 독립적이다.
- Y는 상당히 불안정한 컴포넌트이다. Y에 의존하는 컴포넌트는 없기 떄문에, 책임성이 없으며, 의존적이라고 볼 수 있다.
안정성 지표
- 불안정성 : I = Fan-out / (Fan-in + Fan-out) , I =1이면, 최고로 불안정한 컴포넌트, I = 0이면, 최고로 안정한 컴포넌트다.
- Fan-in: 안으로 들어오는 의존성. 이 지표는 컴포넌트 내부의 클래스에 의존하는 컴포넌트 외부의 클래스 개수를 나타낸다.
- Fan-out: 바깥으로 나가는 의존성. 이 지표는 컴포넌트 외부의 클래스에 의존하는 컴포넌트 내부의 클래스 개수를 나타낸다.
- 의존성 방향으로 갈수록 I지표 값이 감소해야 한다.
🙌나의 예시
- 위에서 언급된 동기화 람다 함수 멀티 모듈의 예시로 보면 위와 같다.
- 의존성 방향으로 갈수록 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)에 위치한다.
- 모든 컴포넌트가 이 두 지점에 위치하는 것은 아니다.
- 아래 그림의 궤적은 컴포넌트가 절대로 위치해서는 안 되는 영역, 배제할 구역(Zone of Exclusion)이다.
고통의 구역
(0, 0)
주변 구역에 위치한 컴포넌트는 매우 안정적이며 구체적이다. 뻣뻣한 상태이다. 추상적이지 않아서 확장할 수 없고, 안정적이므로 변경하기 상당히 어렵다.- 하지만 변동성이 없는 컴포넌트는
(0, 0)
구역에 위치하더라고 해롭지 않다. 변동될 가능성이 없기 때문이다.
쓸모없는 구역
(1, 1)
주변의 컴포넌트는 최고로 추상적이지만, 누구도 그 컴포넌트에 의존하지 않는다. 이러한 컴포넌트는 쓸모가 없다. 따라서 이 구역은 쓸모없는 구역(Zone of Uselessness)이라 부른다.
배제 구역 벗어나기
- 변동성이 큰 컴포넌트 대부분은 두 배제 구역으로부터 가능한 한 멀리 떨어뜨려야 한다.
- 최대한 멀리 떨어진 점의 궤적은
(1, 0)
과(0, 1)
을 잇는 선분이다. 저자는 주계열(Main Sequence)이라 부른다. - 주계열 위 또는 가깝게 위치해야 하며, 이렇게 위치하면 ‘너무 추상적’이지도 않고, 추상화 정도에 비해 ‘너무 불안정’하지도 않다.
주계열과의 거리
- 주계열과의 거리를 지표화한 것으로, 주계열에 대체로 일치하도록 설계되었는지를 분석할 수 있다.
🙌나의 예시
- 위에서 언급한 동기화 람다 함수를 기반으로 추상화 지표로 A/I 그래프를 그려보면 아래 이미지와 같다.
app-samsung
컴포넌트는 변동성이 큰 모듈이다보니 두 배제 구역으로부터 멀리 떨어뜨려놓아야 하지만 그렇지 못하다.- 추상 클래스 및 인터페이스를 활용하여 추상화 정도를 더 높일 필요가 있다.
app-core
컴포넌트는 고통의 구역에 속해있지만, 변동성이 거의 없는 컴포넌트이므로 크게 해롭지 않다.app-main
컴포넌트는 주계열선에 가깝게 위치해있다.
결론
- 이 장에서 설명하는
의존성 관리 지표
는 설계의 의존성과 추상화 정도가 내가 ‘훌륭한’ 패턴이라고 생각하는 수준에 얼마나 부합하는지를 측정한다. - 하지만 이러한 지표는 신이 아니고 아무리 해도 불완전하다는 것을 참고하자.