728x90
반응형
- SOLID원칙이 벽과 방에 벽돌을 배치하는 방법이라면, 컴포넌트 원칙은 빌딩에 방을 배치하는 방법을 설명해준다.
- 큰 빌딩과 마찬가지로 대규모 소프트웨어 시스템은 작은 컴포넌트들로 만들어진다.
- 컴포넌트는 시스템의 구성 요소로 배포할 수 있는 가장 작은 단위다. 자바의 경우 jar 파일이 컴포넌트
컴포넌트 응집도
- 어떤 클래스를 어느 컴포넌트에 포함시켜야 할까? 소프트웨어 엔지니어링 원칙의 도움이 필요하다.
- 컴포넌트 응집도와 관련된 세 가지 원칙
- REP : 재사용/릴리스 등가 원칙 (Reuse/Release Equivalence Principle)
- CCP : 공통 폐쇄 원칙 (Common Closure Principle)
- CRP : 공통 재사용 원칙 (Common Reuse Principle)
REP: 재사용/릴리스 등가 원칙
재사용 단위는 릴리스 단위와 같다.
- 우리는 소프트웨어 재사용의 시대에 살고 있다.
- 재사용/릴리스 등가 원칙은 사실 당연한데, 소프트웨어 컴포넌트가 릴리스 절차를 통해 관리되지 않거나 번호가 부여되지 않는다면 재사용하고 싶어도 할수 없다.
- 릴리스 번호가 없다면 재사용 컴포넌트들이 서로 호환되는지 보증할 방법이 없다.
- 새로운 버전이 언제 출시되고 무엇이 변했는지 개발자가 알아야 새 릴리스로 통합이 가능하다.
- 소프트웨어 설계와 아키텍처 관점에서 보면 단일 컴포넌트는
응집성 높은 클래스와 모듈
로 구성되어야 한다.- 하나의 컴포넌트로 묶인 클래스와 모듈은 반드시 함께 릴리스 할 수 있어야 함
- 이 조언만으로는 클래스와 모듈을 단일 컴포넌트로 묶는 방법을 제대로 설명하기 힘들지만 원칙 자체는 중요하다.
- CCP, CRP는 REP을 보완한다.
CCP: 공통 폐쇄 원칙
동일한 이유로 동일한 시점에 변경되는 클래스는 같은 컴포넌트로 묶어라. 서로 다른 시점에 다른 이유로 변경되는 클래스는 다른 컴포넌트로 분리하라.
- 이 원칙은 단일 책임 원칙(SRP)을 컴포넌트 관점에서 다시 쓴 것이다.
- 단일 클래스는 변경의 이유가 여러 개 있어서는 안된다고 말하듯이, CCP에서도 단일 컴포넌트는 변경의 이유가 여러 개 있어서는 안 된다고 말한다.
- 대다수의 애플리케이션에서
유지보수성(maintainability)
은 재사용성보다 훨씬 중요하다. 따라서 코드 변경시 도처에 분산된 경우 보다는 변경 모두가 단일 컴포넌트에서 발생해야 한다.- 해당 컴포넌트만 재배포하면 되며, 다른 컴포넌트를 다시 검증/배포할 필요가 없다.
- 변경될 가능성이 있는 클래스를 한 곳으로 묶으면 릴리스, 재검증, 배포하는 일과 관련된 작업량을 최소화할 수 있다.
- OCP와도 관련이 있으며, 발생할 가능성이 있거나 과거에 발생했던 대다수의 공통적인 변경에 대해서 클래스가 닫혀 있도록 설계한다.
- CCP에서는 동일한 유형의 변경에 대해 닫혀 있는 클래스들을 하나의 컴포넌트로 묶음으로써 OCP에서 얻은 교훈을 확대 적용한다.
CRP: 공통 재사용 원칙
컴포넌트 사용자들을 필요하지 않는 것에 의존하게 강요하지 말라.
- CRP도 클래스와 모듈을 어느 컴포넌트에 위치시킬지 결정할때 도움되는 원칙
- 재사용되는 경향이 있는 클래스와 모듈들은 같은 컴포넌트에 포함해야 한다.
- 대체로 재사용 가능한 클래스들은 다른 모듈의 다른 클래스와 서로 상호작용 하는 경우가 많다. 이러한 클래스들은 동일한 컴포넌트에 포함되어야 한다.
- container 클래스와 iterator 클래스는 서로 강하게 결합되어 있기에 반드시 동일 컴포넌트에 위치해야 한다.
- CRP는 어떤 클래스를 한데 묶어도 되는지보다는, 어떤 클래스를 한데 묶어서는 안되는지에 대해 훨씬 더 많은 것을 이야기한다.
- 이 원칙은 인터페이스 분리 원칙(ISP)의 포괄적인 버전이다.
- ISP : 사용하지 않는 메서드가 있는 클래스에 의존하지 말라 (interface 분리)
- CRP : 사용하지 않는 클래스를 가진 컴포넌트에 의존하지 말라
- 즉, 필요하지 않은 것에 의존하지 말라
컴포넌트 응집도에 대한 균형 다이어그램
- 세 원칙은 상충되기 때문에 균형(tension)을 이루는 방법을 찾아야 한다.
- REP와 CCP는 포함 원칙이며 두 원칙은 컴포넌트를 더욱
크게
만든다. - CRP는 배제 원칙이며 컴포넌트를 더욱
작게
만든다. (필요 없는 부분에 의존하지 않게 하기 위해 컴포넌트를 작게 분리)
- REP와 CCP는 포함 원칙이며 두 원칙은 컴포넌트를 더욱
- 각 변은 반대쪽 꼭지점에 있는 원칙을 포기했을 때 감수 비용
- REP(재사용/릴리스 등가 원칙) - 포기시 재사용이 어려워짐
- CCP(공통 폐쇄 원칙) - 포기시 사소한 변경에도 많은 컴포넌트에 영향을 미침
- CRP(공통 재사용 원칙) - 포기시 불필요한 릴리스가 늘어남
- 프로젝트의 컴포넌트 구조는 시간과 성숙도에 따라 변한다. (수행하는 일 자체보다는 프로젝트가 발전되고 사용되는 방법과 더 관련이 있음)
- 일반적으로 프로젝트는 삼각형의 오른쪽에서 시작하는 편, 이때 재사용성에 대한 중요도가 떨어진다.
- 프로젝트가 성숙하고, 파생된 프로젝트가 시작되면 삼각형에서 점차 왼쪽으로 이동하며 재사용성의 중요도가 증가한다.
컴포넌트 결합
- 컴포넌트 사이의 관계 설명
- ADP : 의존성 비순환 원칙
- SDP : 안정된 의존성 원칙
- SAP : 안정된 추상화 원칙
ADP: 의존성 비순환 원칙
컴포넌트 의존성 그래프에 순환(cycle)이 있어서는 안 된다.
- 동일한 소스 파일을 수정하는 환경에서 코드가 정상 동작 안하는 현상 존재
- 저자는
숙취 증후군
이라고 부르며, 누군가 마지막으로 수정한 코드 때문에 이전 기능들이 동작하지 못하는 현상을 뜻한다. - 나는 무언가를 열심히 만들고 확인 후 퇴근, 이튿날 출근하였는데 동작하지 않음. 나 보다 늦게 퇴근한 사람이 내가 의존하고 있는 무언가를 수정했기 때문
- 2가지 해결책 존재
- 주 단위 빌드
- 의존성 비순환 원칙
- 저자는
주 단위 빌드
- 일주일의 첫 4일동안 서로 신경쓰지 않고 작업, 금요일이 되면 변경된 코드를 모두 통합하여 시스템 빌드
- 프로젝트가 커지면 통합에 드는 시간이 늘어남
- 금요일이었던 통합이 하루만에 불가하여 목요일로 땡겨짐 이런 현상 반복
- 효율성을 위해 빌드 일정을 계속 늘려야하고, 빌드 주기가 늦어질수록 프로젝트가 감수할 위험은 커진다.
- 통합과 테스트를 수행하기가 점점 더 어려워지고, 팀은 빠른 피드백이 주는 장점을 잃는다.
순환 의존성 제거하기
- 해결책은 개발 환경을 릴리스 가능한 컴포넌트 단위로 분리하는 것
- 이를 통해 컴포넌트는 개별 관리자 또는 단일 개발팀에 책임질 수 있는 작업 단위가 됨
- 개발자가 해당 컴포넌트 동작하도록 만든 후, 해당 컴포넌트를 릴리스하여 다른 개발자가 사용할 수 있도록 만듬
- 이 같은 작업 절차는 단순하며 합리적이어서 널리 사용된다.
- 하지만 이 절차가 성공하려면
의존성 구조에 순환이 있어서는 안된다.
- 어느 컴포넌트에서 시작하더라도, 의존성 관계를 따라가면 최초의 컴포넌트로 되돌아갈 수 없다.
- 순환이 없다. 비순환 방향 그래프(DAG - Direct Acyclic Graph)
- Presenters 컴포넌트가 릴리스하면 영향을 받는 팀을 쉽게 찾을 수 있음 (바로 View, Main 컴포넌트)
- Main이 새로 릴리스되더라도 시스템에서 영향받는 컴포넌트는 전혀 없음
- 시스템 전체를 릴리스해야 한다면 상향식으로 진행 (Entities -> Database, Interactors ... -> Main)
- 구성요소 간 의존성을 파악하고 있으면 시스템을 빌드하는 방법을 알 수 있음
순환이 컴포넌트 의존성 그래프에 미치는 영향
- 요구사항으로 Entities에 포함된 클래스가 Authorizer를 사용하게 된다면 순환 의존성이 발생함
- 이 순환은 즉각적으로 문제를 일으킴
- Database 컴포넌트를 릴리즈 하기 위해선 Entities 호환되어야 하고 Entites -> Authorizer -> Interactors 모두 호환이 필요하여 릴리스가 더 어려워진다. 사실상 하나의 거대한 컴포넌트가 됨
- Entities 컴포넌트를 테스트하기 위해선 Authorizer와 Interactors 까지 빌드하고 통합필요
- 단위 테스트 실행하는데 왜 많고 다양한 라이브러리와 다른 사람의 작업물까지 포함해야하는가?!!
- 순환이 생기면 컴포넌트 분리가 상당히 어려워지고, 릴리스 과정에서 에러도 쉽게 발생
- 어떤 순서로 빌드해야 올바른지 파악하기 힘들어지며 올바른 순서라는 것 자체가 없음
순환 끊기
- 컴포넌트 사이의 순환을 끊어 의존성을 DAG로 복구 가능
1. 의존성 역전 원칙(DIP) 사용
- User가 필요로하는 메소드를 인터페이스로 제공
- 인터페이스는 Entities에, 구현체는 Authorizer에 위치시킴
2. 새로운 컴포넌트 생성
- Entities, Authorizer 모두 의존하는 새로운 컴포넌트를 만듬
- 두 컴포넌트가 모두 의존하는 클래스들을 컴포넌트에 위치시킴
흐트러짐(Jiteers)
- 두 번째 해결책에서 시사하는바는 요구사항이 변경되면 컴포넌트 구조도 변경될 수 있따는 사실
- 의존성 구조는 서서히 흐트러지며 또 성장
- 순환이 발생하면 어떤 식으로든 끊어야함
하향식(top-down) 설계
- 컴포넌트 구조는 top-down 으로 설계될 수 없음
- 컴포넌트는 시스템에서 가장 먼저 설계할 수 있는 대상이 아니며, 오히려 시스템이 성장하고 변경될 때 함께 진화함
- 컴포넌트 의존성 다이어그램은 애플리케이션의 빌드 가능성(buildability)과 유지보수성(maintainability)을 보여주는 지도와 같음
- 이러한 이유 때문에 초기 설계가 불가함, 빌드/유지보수할 소프트웨어가 없다면 지도 또한 필요 없기 때문
- 구현과 설계가 이뤄지는 프로젝트 초기에 모듈들이 점차 쌓이면서 의존성 관리에 대한 요구가 늘어남
- 변경되는 범위가 시스템의 가능한 한 작은 일부로 한정되기를 원하며 SRP, CCP에 관심을 갖기 시작함
- 의존성 구조와 관련된 최우선 관심사는
변동성을 격리하는 일
- 자주 변경되는 컴포넌트로 인해 다른 컴포넌트가 영향받는 일을 원치 않는다.
- 컴포넌트 의존성 그래프는 자주 변경되는 컴포넌트로부터 안정적이며 가치 높은 컴포넌트를 보호해야함
- 의존성 구조와 관련된 최우선 관심사는
- 애플리케이션이 성장함에 따라 재사용 가능한 요소를 만드는 일에 관심을 갖게 됨
- 컴포넌트를 조합하는 과정에 공통 재사용 원칙(CRP)이 여향을 미치기 시작
- 컴포넌트가 증가하여 순환이 발생하면 ADP가 적용되고, 의존성 그래프를 흐트러트려 조금씩 성장
- 아무런 클래스도 설계하지 않은 상태에서 컴포넌트 의존성 구조를 설계하려고 하면 큰 실패를 맛볼 수 있음
SDP: 안정된 의존성 원칙
안정성의 방향으로 (더 안정된 쪽에) 의존하라
- 컴포넌트의 일부는 변동성을 지니도록 설계되며 변경이 쉽지 않은 컴포넌트가 변동이 예상되는 컴포넌트에 의존하게 만들어서는 안됨
- 한번 의존하게 되면 변동성이 큰 컴포넌트도 결국 변경이 어려워짐
- 당신의 모듈에서는 단 한 줄의 코드도 변경되지 않았지만, 어느 순간 갑자기 당신의 모듈이 변경하는 일이 상당히 도전적인 일이 될 수 있음
- 안정된 의존성 원칙(SDP : Stable Dependency Principle)을 준수하면 변경하기 쉽게 만들어진 모듈에 의존하지 않도록 할 수 있음
안정성
- 안정성은 변화가 발생하는 빈도와는 관련이 없고 변경을 만들기 위해 필요한 작업량과 관련이 있음
- 옆면으로 선 동전은 그다지 힘을 쓰지 않고도 넘어뜨릴 수 있기 때문에 안정적이지 않음
- 반면 탁자는 안정적임. 탁자를 뒤집으려면 상당한 수고를 감수해야 하기 때문
- 소프트웨어와는 어떤 관련이 있을까?
- 컴포넌트 변경을 어렵게만드는 요인은 컴포넌트 크기, 복잡도, 간결함 등이 있음
- 변경하기 어렵게 만드는 확실한 방법 하나는 수많은 다른 컴포넌트가 해당 컴포넌트에 의존하게 만드는 것
- 사소한 변경이라도 의존하는 모든 컴포넌트를 만족시키면서 변경하려면 상당히 노력이 들기 때문 (common-lib)
- X는 안정적인 컴포넌트. X 컴포넌트가 변경하지 말아야 할 이유가 세 가지나 되기 때문
- X는 세 컴포넌트를 책임진다(reponsible)라고 말함
- X는 독립적(independent)임
- X는 어디에도 의존하지 않으므로 X가 변경되도록 만들 수 있는 외적인 영향이 전혀 없다.
- Y는 상당히 불안정한 컴포넌트
- Y는 어떤 컴포넌트도 의존하지 않으므로 Y는 책임성이 없음
- Y는 의존적임
- Y는 세 개의 컴포넌에 의존하므로 변경이 발생할 수 있는 외부 요인이 세 가지임
안정성 지표
- 컴포넌트로 들어오고 나가는 의존성의 개수를 세어 안정성 계산이 가능
- Fan-in : 안으로 들어오는 의존성. 컴포넌트 내부의 클래스에 의존하는 컴포넌트 외부 클래스 개수
- Fan-out : 밖으로 나가는 의존성. 컴포넌트 외부 클래스에 의존하는 컴포넌트 내부 클래스 개수
- I(불안정성) : Fant-out / (Fan-in + Fan-out). 이 지표는 [0, 1] 범위의 값을 가짐
- I=0 이면 최고로 안정된 컴포넌트, I=1 이면 최고로 불안정한 컴포넌트
- SDP에서 컴포넌트의 I지표는 그 컴포넌트가 의존하는 다른 컴포넌트들의 I보다 커야 한다고 말함. 즉, 의존성 방향으로 갈수록 I 지표 값이 감소해야 함
모든 컴포넌트가 안정적이어야 하는 것은 아니다.
- 모든 컴포넌트가 최고로 안정적인 시스템이라면 변경이 불가능함
- 우리가 기대하는 것은 불안정한 컴포넌트도 있고 안정된 컴포넌트도 존재하는 상태
- 위는 세 컴포넌트로 구성된 시스템이 가질 수 있는 이상적인 구조
- 위쪽에는 변경 가능한 컴포넌트가 있고, 아래의 안정된 컴포넌트에 의존함
- 다이어그램에서 불안정한 컴포넌트를 관례적으로 위쪽에 둠
- 위로 향하는 화살표가 있으면 SDP를 위배하는 상태
SDP가 어떻게 위배될 수 있는지 살펴보자
- Flexible은 변경하기 쉽도록 설계한 컴포넌트
- Flexible이 불안정한 상태이길 바라지만, Stable 컴포넌트에서 Flexbile 의존성을 걸게되면 SDP에 위배됨
- Stable의 I 지표는 Flexible의 I 지표보다 더 작아지고, 결국 Flexbile은 변경하게 어렵게 됨
- 이 부분은 잘 이해가 되지 않는다. (Stable의 I 지표가 1/3으로 더 큰 것이 아닌가?)
- 변경하게 되면 Stable과 Stable에 의존하는 나머지 컴포넌트에도 조치를 취해야 함
- 어떤 식으로든 Stable의 Flexbile에 대한 의존성을 끊어야 함
- DIP를 도입하면 문제 해결이 가능
- US라는 인터페이스를 생성 후 UServer 컴포넌트에 넣은 후 C가 해당 인터페이스를 구현하게 만듬
- Stable의 Flexible 의존성을 끊을 수 있고, 두 컴포넌트 모두 Userver에 의존하도록 강제함
- Userver는 매우 안정된 상태(I=0)이며, Flexible은 자신에게 맞는 불안정성(I=1)을 그대로 유지할 수 있음
- 이제 모든 의존성은 I가 감소하는 방향으로 향함
추상 컴포넌트
- 인터페이스만 포함하는 컴포넌트(Userver)가 이상하게 보일수도 있으나..
- 정적 타입 언어에서 이 방식은 상당히 흔할 뿐 아니라 꼭 필요한 전략으로 알려져 있음
- 추상 컴포넌트는 상당히 안정적이며, 따라서 덜 안정적인 컴포넌트가 의존할 수 있는 이상적 대상
SAP: 안정된 추상화 원칙
컴포넌트는 안정된 정도만큼만 추상화되어야 한다.
고수준 정책을 어디에 위치시켜야 하는가?
- 고수준 정책을 캡슐화하는 소프트웨어는 반드시 안정된 컴포넌트(I=0)에 위치해야 함
- 업무 로직이나 아키텍처와 관련된 결정에는 변동성이 없기를 기대
- 쉽고 빠르게 변경할 수 있는 소프트웨어만 불안정한 컴포넌트(I=1)에 위치해야 함
- 하지만 고수준 정책을 안정된 컴포넌트에 위치시키면, 그 정책을 포함한 코드는 수정이 어려워지고 전체 아키텍처가 유연성을 잃음
- 안정된 상태이면서 동시에 변경에 충분히 대응할 수 있도록 유연해야한다면? 개방 폐쇄 원칙(OCP)에서 해답을 찾을 수 있음
OCP
에서는 클래스를 수정하지 않고도 확장이 충분히 가능할 정도로 클래스를 유연하기 만들 수 있음. 바로 추상(abstract) 클래스
안정된 추상화 원칙
- 안정된 추상화 원칙(SAP, Stable Abstractions Principle)은
안정성
과추상화 정도
사이의 관계를 정의 - 안정된 컴포넌트는 추상화 컴포넌트여야 함
- 이를 통해 안정성이 컨포넌트를 확장하는 일을 방해해서는 안됨
- 불안정한 컴포넌트는 반드시 구체 컴포넌트여야 함
- 컴포넌트가 불안정하므로 내부의 구체적인 코드를 쉽게 변경할 수 있어야 하기 때문
- 안정된 컴포넌트가 확장이 가능해지면 유연성을 얻고 아키텍처를 과도하게 제약하지 않음
- 안정적인 컴포넌트 -> 인터 페이스와 추상 클래스로 구성
- SAP + SDP = DIP
- SDP : 의존성이 반드시 안정성의 방향으로 향해야 함
- SAP : 안정성이 결국 추상화를 의미
- 하지만 DIP는 클래스에 대한 원칙이며, 클래스의 경우 중간은 존재하지 않는다.
- 즉, 클래스는 추상적이거나 아니거나 둘 중 하나
- SDP와 SAP의 조합은 컴포넌트에 대한 원칙이다.
- 컴포넌트는 어떤 부분은 추상적 다른 부분은 안정적일 수 있음
추상화 정도 측정
- A 지표는 컴포넌트의 추상화 정도를 측정한 값
- Nc: 컴포넌트의 클래스 개수
- Na: 컴포넌트의 추상 클래스와 인터페이스의 개수
- A : 추상화 정도, A = Na/Nc
주계열
- 안정성(I)과 추상화 정도(A)사이의 관계
- (0,1)은 최고로 안정적이며 추상화된 컴포넌트, (1,0)은 최고로 불안정하며 구체화된 컴포넌트
- 모든 컴포넌트가 이 두 지점에 위치하지 않음, 대체로 컴포넌트는 추상화와 안정화의 정도가 다양하기 때문
- 추상 클래스는 흔히 또 다른 추상 클래스로부터 파생해서 만들기 때문에 의존성을 가짐
- 따라서 클래스가 최고로 추상적이지만, 최고로 안정적인 것은 아님. 의존성으로 인해 안정성이 감소하기 때문
고통의 구역(Zone of Pain)
- (0,0) 주변 구역에 위치한 컴포넌트들
- 매우 안정적이며 구체적
- 바람직한 사애가 아닌데, 뻣뻣한 상태이기 때문
- 추상적이지 않으므로 확장할 수 없고, 안정적이므로 변경하기도 상당히 어려움
- 제대소 설계된 컴포넌트라면 (0,0) 근처에 위치하지 않을 거라고 보는게 일반적
- 소프트웨어 엔티티, 유틸리티 라이브러는 고통의 구역에 위치하곤함
- 변동성이 있는 소프트웨어 컴포넌트가 해당 구역에 위치하면
고통
스럽게 된다.
쓸모없는 구역 (Zone of Uselessness)
- (1,1) 주변 구역에 위치한 컴포넌트들
- 최고로 추상적이지만, 누구도 그 컴포넌트에 의존하지 않음 -> 쓸모가 없음
- 폐기물과 같으며 이러한 엔티티는 누구도 구현하지 않은 채 남겨진 추상 클래스인 경우가 많음
배제 구역 벗어나기
- 변동성이 큰 컴포넌트 대부분은 두 배제 구역으로부터 가능한 한 멀리 떨어뜨려야 함
- 최대한 멀리 떨어진 선 : 주계열(Main Sequence) - (1,0)과 (0,1)을 잇는 선분
- 안정성에 비해
너무 추상적
이지도 않고, 추상화 정도에 비해너무 불안정
하지도 않음 - 컴포넌트는 주계열과 가깝게 위치할 때 가장 이상적
728x90
반응형
'프로그래밍 노트 > 아키텍처' 카테고리의 다른 글
응집도(Cohesion)과 결합도(Coupling) (0) | 2024.10.07 |
---|---|
[클린 아키텍처] 5장. 아키텍처 (0) | 2023.11.11 |
[클린 아키텍처] 3장. 설계 원칙 - 좋은 아키텍처를 정의하는 원칙(SOLID) (1) | 2023.10.17 |
[클린 아키텍처] 객체 지향 프로그래밍 (0) | 2023.09.04 |
[클린 아키텍처] 행위, 아키텍처 두가지 가치에 대하여 (0) | 2023.08.28 |