컴포넌트
- 컴포넌트는 시스템의 구성 요소로 배포할 수 있는 가장 작은 단위이며, 자바의 경우 jar 파일이 컴포넌트임
- 컴포넌트는 다양한 형태로 만들어질 수 있음
- 여러 컴포넌트를 서로 링크하여 실행 가능한 단일 파일로 생성할 수 있음
- 여러 컴포넌트를 묶어서 war 파일과 같은 단일 아카이브로 만들 수 있음
- 컴포넌트 각각을 jar과 같이 동적으로 로드 할 수 있는 플러그인이나 실행 가능한 파일로 만들어 독립적으로 배포할 수 있음
- 컴포넌트가 최종적으로 어떤 형태로 배포되든, 잘 설계된 컴포넌트라면 반드시 독립적으로 개발 가능해야 함
- 참고. 앞으로 내용을 따라갈 때 거시적인 관점에서 하나의 큰 시스템을 생각하며 흐름을 따라가야 함.
컴포넌트 응집도
- 어떤 클래스를 어느 컴포넌트에 포함시켜야 할지는 중요한 결정이므로, 제대로 된 소프트웨어 엔지니어링 원칙이 필요함
REP(reuse/release equivalance principle): 재사용/릴리스 등가 원칙
- 정의: 재사용 단위는 릴리스 단위와 같다.
- 이는 당연한데, 컴포넌트가 릴리스 절차를 통해 관리되지 않거나, 릴리스 번호가 없다면 재사용하고 싶지도, 할 수도 없음
- 릴리스 번호가 없다면 컴포넌트들이 호환되는지 보증할 수 없음
- 개발자는 새로운 버전이 언제 출시되고 무엇이 변했는지 알아야 함(새로운 버전으로의 통합 여부 및 시기 결정)
- REP를 소프트웨어 설계와 아키텍처 관점에서 보면 다음과 같음
- 단일 컴포넌트는 응집성이 높은 클래스와 모듈들로 구성되어야 함
- 이를 다르게 보면 하나의 컴포넌트로 묶인 클래스와 모듈은 반드시 함께 릴리스 할 수 있어야 한다는 것
- 하나의 컴포넌트로 묶인 클래스와 모듈은 버전이 같고, 동일한 릴리스로 관리되고, 동일한 릴리스 문서에 포함되어야 함
- 이 원칙 만으로는 클래스와 모듈을 단일 컴포넌트로 묶는 방법을 제대로 설명하지 못하지만(약점), 이 원칙의 약점은 다음 두 원칙(CCP와 CRP)으로 보완할 수 있음
CCP(common closure principle): 공통 폐쇄 원칙
- 정의: 동일한 이유로 동일한 시점에 변경되는 클래스는 같은 컴포넌트로 묶고, 다른 시점에 다른 이유로 변경되는 클래스는 분리하라
- 대다수의 애플리케이션에서 유지보수성은 재사용성보다 훨씬 중요하며, 변경은 단일 컴포넌트에서 발생해야 함(독립적인 배포)
- 수정이 필요한 경우 모든 컴포넌트를 조금씩 수정하기 보다는 하나의 컴포넌트만 수정하도록 하는게 낫다.
- OCP(개방 폐쇄 원칙)는 class level이고, CCP는 컴포넌트 level.
- OCP: 공통적인 변경에 대해 클래스가 닫혀 있도록 설계.
- SRP(단일 책임 원칙) class level이고, CCP는 컴포넌트 level : 단일 컴포넌트는 변경의 이유가 여러 개 있어서는 안됨
- SRP: 서로 다른 이유로 변경되는 메소드를 서로 다른 클래스로 분리하라
- CCP: 서로 다른 이유로 변경되는 클래스를 서로 다른 컴포넌트로 분리하라.
CRP(common reuse principle): 공통 재사용 원칙
- 정의: 컴포넌트 사용자들을 필요로 하지 않는 것에 의존하게 강요하지 말라.
- CRP도 클래스와 모듈을 어느 컴포넌트에 위치시킬지 결정할 때 도움이 됨
- 같이 재사용되는 경향이 있는 클래스와 모듈들은 같은 컴포넌트에 포함
- CRP에서는 재사용한 클래스들의 연결고리가 동일한 컴포넌트에 포함되어 있어야
- 컴포넌트를 의존하겠다고 결정한다는 것은 생각보다 많은 유지보수가 필요할 것을 암시하며 CRP에 의거하면 동일 컴포넌트로 묶어서는 안되는 것을 의의함
- 컴포넌트의 단 하나의 클래스만을 사용한다고 해서 의존성이 약해지는게 아님
- 의존성은 이분법적인 개념(Y/N) 이지 %가 아님
- 이로 인해 사용되는 컴포넌트가 변경될 때마다 같이 변경(재배포 등)해야 할 가능성이 높음
- 그러므로 의존하는 컴포넌트가 있다면 해당 컴포넌트의 모든 클래스에 대해 의존함을 확실히 인지해야 함
- 컴포넌트의 단 하나의 클래스만을 사용한다고 해서 의존성이 약해지는게 아님
- CRP는 강하게 결합되지 않은 클래스들을 동일한 컴포넌트에 위치시켜서는 안 된다고 말함
- ISP(인터페이스 분리 원칙)는 class level, CRP는 컴포넌트 level : 필요하지 않은 것에 의존하지 말라
- ISP: 사용하지 않는 메소드가 있는 클래스에 의존하지 말라
- CRP: 사용하지 않는 클래스를 가진 컴포넌트에 의존하지 말라
컴포넌트 응집도에 대한 균형 다이어그램
- 세 원칙은 서로 상충되는데, REP와 CCP는 포함 원칙(컴포넌트를 크게 만듦)이며, CRP는 배제 원칙(컴포넌트를 작게 만)임
- 뛰어난 아키텍트는 3가지 원칙들이 균형을 이루는 방법을 찾아야 함.
- 각 변은 반대쪽 꼭지점에 있는 원칙을 포기했을 때 감수해야 할 비용을 나타냄
- CCP를 포기하면, 사소한 변경이 생겼을 때 너무 많은 컴포넌트에 영향이 미침
- CRP를 포기하면 불필요한 릴리스가 너무 빈번해짐
- REP를 포기하면 재사용이 어려워짐
- 일반적으로 삼각형의 오른쪽에서 시작해서 왼쪽으로 이동해 감
- 프로젝트 처음부터 재사용성이 필요하지는 않다가 성숙해지다보면 점점 재사용성(REP)이 중요해짐
- 프로젝트의 컴포넌트 구조는 시간과 성숙도에 따라 변한다.
컴포넌트 결합
ADP(Acyclic dependency principle): 의존성 비순환 원칙
- 정의: 컴포넌트 의존성 그래프에 순환이 있어서는 안된다.
- 많은 개발자가 동일한 소스 파일을 수정하는 환경에서 코드가 동작하지 않게 될 수 있으며, 2가지 해결방법이 발전되어 옴
- 주단위 빌드
- 순환 의존성 제거하기
주 단위 빌드
- 4일은 서로를 신경쓰지 않고, 금요일이 되면 코드를 통합하여 시스템을 빌드함
- 프로젝트가 커지면서 통합에 드는 시간이 계속해서 늘어나게 됨
- 결국 빌드 일정을 늘려야 하고, 통합과 테스트는 수행하기 점점 어려워지며, 빠른 피드백이 주는 장점을 잃게됨
순환 의존성 제거하기
- 이 문제의 해결책은 개발 환경을 릴리스 가능한 컴포넌트 단위로 분리하는 것
- 이를 통해 컴포넌트는 개별 관리자 또는 단일 개발팀이 책임질 수 있는 작업 단위가 됨
- 개발자가 해당 컴포넌트가 동작하도록 만든 후, 릴리스하여 다른 개발자가 사용할 수 있도록 만듬
- 이는 단순하며 합리적이라 널리 사용되지만 컴포넌트 사이의 의존성 구조를 반드시 관리해야 함
- 의존성 구조에 순환이 있어서는 안되며, 컴포넌트 간의 의존성은 비순환 방향 그래프(DAG, Directed Acyclic Graph)여야 함
- 어느 컴포넌트에서 시작하더라도 의존성 관계를 따라 최초의 컴포넌트로 돌아갈 수 없음
- Presenters를 담당하는 팀에서 새로운 릴리스를 만들면 이 릴리스에 영향받는 팀을 쉽게 찾을 수 있음
- Main은 새로 릴리스되더라도 시스템에서 영향받는 컴포넌트가 없음
- 시스템 전체를 릴리스한다면 릴리스 절차는 상향식으로 진행됨(Entities부터 시작해 Main은 마지막에 처리)
이처럼 구성요소 간 의존성을 파악하고 있으면 시스템을 빌드하는 방법을 알 수 있음
순환이 컴포넌트 의존성 그래프에 미치는 영향
- 요구사항으로 Entities에 포함된 클래스 하나가 Authorizer의 클래스 하나를 사용할 수 밖에 없다면 순환 의존성이 발생함
- Database 컴포넌트 개발자는 컴포넌트를 릴리스 하려면 Entities, Authorizer, Interactors 모두와 호환되어야 함
- 세 컴포넌트는 하나의 거대 컴포넌트가 되며, 개발자 서로가 얽매여 모두 항상 정확하게 동일한 릴리스를 사용해야 함
- Entities를 테스트하려면 Authorizer와 Interactors도 빌드하고 통합해야 하면서 어려워짐
- 모듈의 개수가 많아짐에 따라 빌드 관련 이슈는 기하급수적으로 증가함
- 컴포넌트를 어떤 순서로 빌드해야 올바른지 파악하기 힘들어지며, 올바른 순서라는 것 자체가 없을 수 있음
순환 끊기
- 컴포넌트 사이의 순환을 끊고, DAG로 복구하는 것은 언제든 가능하며, 의존성 역전 원칙 또는 새로운 컴포넌트 생성으로 가능함
- 의존성 역전 원칙(DIP)
- User가 필요로 하는 메소드를 제공하는 인터페이스(permissions in Entity)를 제공함
- 그리고 이 인터페이스는 Entities에, 구현체는 Authorizer에 위치시킴
- 새로운 컴포넌트 생성
- Entities와 Authorizer가 의존하는 새로운 컴포넌트를 만듬
- 그리고 두 컴포넌트가 모두 의존하는 클래스들을 새로운 컴포넌트로 이동시킴
흐뜨러짐(Jitters)
- 두 번째 해결책(새로운 컴포넌트 생성)이 시사하는 바는 요구사항이 변경되면 컴포넌트 구조도 변경될 수 있다는 사실
- 실제로 애플리케이션이 성장하면서 컴포넌트 의존성 구조는 서서히 흐트러지며 또 성장함
- 따라서 의존성 구조에 순환이 발생하는지를 항상 관찰해야 하며, 어떤 식으로든 끊어내야 함
하향식(top-down) 설계
- 프로젝트 초기에는 컴포넌트 구조를 설계할 수 없음. 즉, 컴포넌트 구조는 하향식(top-down)으로 설계될 수 없음
- 컴포넌트 의존성 다이어그램은 애플리케이션 기능과는 거의 관련이 없고, 빌드 가능성과 유지보수성의 지도와 같음
- 컴포넌트는 시스템에서 가장 먼저 설계할 수 있는 대상이 아니며, 시스템이 성장하고 변경될 때 함께 진화함
- 하지만 모듈들이 점차 쌓이기 시작하면 의존성 관리에 대한 요구가 점차 늘어남
- 변경되는 범위가 시스템의 가능한 한 작은 일부로 한정되기를 원함
- 함께 변경되는 클래스는 같은 위치에 배치시킴: 단일 책임 원칙(SRP), 공통 폐쇄 원칙(CRP)
- 의존성 구조와 관련된 최우선 관심사는 변동성의 격리(자주 변경되는 컴포넌트로 부터 다른 컴포넌트를 보호함)
- 애플리케이션이 계속 성장하면서 재사용 가능한 요소를 만드는 일에 관심을 기울이기 시작함: 공통 재사용 원칙(CRP)
- 결국 순환이 발생하면 컴포넌트 의존성 그래프는 조금씩 흐트러지고 또 성장함: 의존성 비순한 원칙(ADP)
"아무런 클래스도 설계하지 않은 상태에서 컴포넌트 의존성 구조를 설계하려고 시도하면 큰 실패를 맛볼 수 있다. 공통 폐쇄 원칙에 대해 그다지 파악하지 못하고 있고, 재사용 가능한 요소도 알지 못하며, 컴포넌트를 생성할 때 거의 확실히 순환 의존성이 발생할 것이다. 따라서 컴포넌트 의존성 구조는 시스템의 논리적 설계에 발맞춰 성장하며 또 진화해야 한다."
SDP: 안정된 의존성 원칙
- 정의: 안정성의 방향으로(더 안정된 쪽에) 의존하라.
- 변경이 어려운 컴포넌트는 최대한 독립적으로
- 변경이 어려운 컴포넌트에 한번 의존하게 되면 변동성이 큰 컴포넌트도 결국 변경이 어려워짐
- 즉, 변경하기 쉽도록 모듈을 설계해도 이 모듈에 누군가가 의존성을 메달아 버리면 이 모듈도 변경하기 어려워짐
안정성(stability)
- 안정성은 변경의 발생 빈도와는 직접적인 관련이 없고, 변경을 위해 필요한 작업과 관련됨
- 안정적이라는 것은 변경을 위해 상상한 수고를 감수해야 한다는 것
- 컴포넌트를 변경하기 어렵게 만드는 많은 요인(컴포넌트의 크기, 복잡도, 간결함 등)이 존재하는데, 이중 다른 컴포넌트가 해당 컴포넌트에 의존하게되면 변경이 특히 어려워짐
- 왜냐하면 사소한 변경이라도 의존하는 모든 컴포넌트를 만족시키면서 변경하려면 상당한 노력이 들기 때문임
- X는 안정된 컴포넌트인데, 세 컴포넌트가 X에 의존하며 X는 변경하지 말아야 할 이유가 3가지나 됨
- 이때 X는 세 컴포넌트를 책임진다고 말하며, 반대로 X는 어디에도 의존하지 않음
- X가 변경되도록 만들 수 있는 외적인 영향이 전혀 없으므로, X는 독립적이라고 말함
- 아래의 Y는 상당히 불안정한 컴포넌트임
- 어떤 컴포넌트도 Y에 의존하지 않으므로 Y는 책임성이 없음
- Y는 세 컴포넌트에 의존하므로 변경이 발생할 수 있는 외부 요인이 3가지이므로, Y는 의존적이라고 함
안정성 지표
- 컴포넌트로 들어오고 나가는 의존성의 개수를 통해 컴포넌트의 불안정성(I)을 계산할 수 있음
- fan-in: 안으로 들어오는 의존성으로 컴포넌트 내부의 클래스에 의존하는 컴포넌트 외부의 클래스 개수
- fan-out: 바깥으로 나가는 의존성으로 컴포넌트 외부의 클래스에 의존하는 컴포넌트 내부의 클래스 개수
- 불안정성(I)은 fan-out / (fan-in + fan-out)으로 계산 가능하며, [0, 1] 사이의 값을 가짐
- 불안정성(I)가 0인 경우(X)
- 해당 컴포넌트에 의존하는 다른 컴포넌트는 있지만, 해당 컴포넌트 자체는 다른 컴포넌트에 의존하지 않음
- 이는 컴포넌트가 가질 수 있는 최고로 안정된 상태이며, 이러한 컴포넌트는 다른 컴포넌트를 책임지며 독립적임
- X에게 의존하는 컴포넌트가 있으므로 변경이 어렵지만, X를 강제하는 의존성은 갖지 않음
- 불안정성(I)가 1인 경우(Y)
- 어떤 컴포넌트도 해당 컴포넌트에 의존하지 않지만, 해당 컴포넌트는 다른 컴포넌트에 의존함
- 최고로 불안정한 상태이며 책임성이 없으므로 의존적임
- 의존하는 컴포넌트가 없으므로 변경하지 말아야 할 이유가 없음
- Y가 다른 컴포넌트에 의존한다는 뜻은 Y를 변경할 이유가 있다는 것임
모든 컴포넌트가 안정적이어야 하는 것은 아니다
- 우리가 기대하는 것은 불안정한 컴포넌트와 안정된 컴포넌트가 모두 존재하는 상태
- 위 다이어그램은 세 컴포넌트로 구성된 시스템이 갖는 이상적인 구조임
- 상단에는 변경 가능한 컴포넌트들이 있고, 하단의 안정된 컴포넌트에 의존함
- 위로 향하는 화살표가 있으면 안정된 의존성 원칙(SDP)에 위배되는 것인데, 존재하지 않음
- Flexible은 변경하기 쉽도록 설계한 컴포넌트임
- 우리는 Flexible이 불안정한 상태이기를 바라지만, Stable에서 Flexible에 의존성을 걸게 되면 SDP를 위배함
- Flexible을 변경하려면 Stable과 Stable에 의존하는 나머지 컴포넌트에도 조치를 취해야 함
- 이를 해결하려면 Flexible에 대한 Stable의 의존성을 끊어야 함
- 예를 들어 Stable의 내부 클래스 U가 Flexible의 내부 클래스 C를 사용할 때, DIP를 도입하면 이 문제를 해결할 수 있음
- US라는 인터페이스를 생성하고 이를 UServer 컴포넌트에 넣은 후 C가 해당 인터페이스를 구현하도록 만듦
- 이를 통해 Flexible에 대한 Stable의 의존성을 끊고, 두 컴포넌트는 모두 UServer에 의존하도록 강제함
- UServer는 매우 안정되며(I=0) Flexible은 불안정성(I=1)을 유지할 수 있고, 모든 의존성은 I가 감소하는 방향으로 향함
- 오로지 인터페이스만을 포함하는 컴포넌트(UServer)를 생성하는 방식이 이상하게 보일 수도 있음
- 하지만 자바와 같은 정적 타입 언어에서는 이 방식이 흔히 사용되며 꼭 필요한 전략으로 알려져 있음
- 이러한 추상 컴포넌트는 상당히 안정적이며, 따라서 덜 안정적인 컴포넌트가 의존할 수 있는 이상적인 대상임
SAP: 안정된 추상화 원칙
- 정의: 컴포넌트는 안정된 정도만큼만 추상화되어야 한다.
고수준 정책(자주 변경해서는 안되는 소프트웨어)을 어디에 위치시켜야 하는가?
- 고수준 정책을 캡슐화하는 소프트웨어는 안정된 컴포넌트에, 변동성이 큰 소프트웨어는 불안정한 컴포넌트에 포함시켜야 함
- 하지만 고수준 정책을 안정된 곳에 위치시키면, 그 정책을 포함하는 소스 코드 수정이 어려워져 시스템 전체 아키텍처가 유연성을 잃음
- 해결: 개방 폐쇄 원칙(OCP)
- 이 원칙을 준수하는 클래스가 추상 클래스임
안정된 추상화 원칙(SAP)
- 안정된 추상화 원칙은 안정성과 추상화 정도 사이의 관계를 정의
- 안정된 컴포넌트는 추상 컴포넌트여야 하며, 이를 통해 안정성이 컴포넌트를 확장하는 일을 방해해서는 안됨
- 불안정한 컴포넌트는 반드시 구체 컴포넌트로써, 컴포넌트가 불안정하므로 내부의 구체적인 코드를 쉽게 변경할 수 있어야 함
- 안정된 추상화 원칙(SAP)와 안정된 의존성 원칙(SDP)를 결합하면 컴포넌트에 대한 의존성 역전 원칙(DIP)
추상화 정도 측정하기
- A = Na / Nc
- Nc는 컴포넌트의 클래스 개수다.
- Na는 컴포넌트의 추상 클래스와 인터페이스 개수다.
- A = 0 : 컴포넌트에 추상 클래스가 하나도 없다.
- A = 1 : 오로지 추상 컴포넌트만 있다.
주계열
- 안정성(I)과 추상화 정도(A) 사이의 관계를 표현하면 다음과 같음
- 최고로 안정적이며 추상화된 컴포넌트는 좌측 상단(0,1)
- 최고로 불안정하며 구체화된 컴포넌트는 (1,0)에 위치함
- 모든 컴포넌트가 이 두 지점에 위치하지는 않으며, 컴포넌트는 추상화와 안정화의 정도가 다양함
- 컴포넌트가 위치할 수 있는 합리적인 궤적을 표현하면 다음과 같음
- 고통의 영역(Zone of Pain)
- (0, 0) 주변 구역에 위치한 컴포넌트들
- 매우 안정적이며 구체적인데, 컴포넌트가 뻣뻣한 상태이므로 바람직하지 않음
- 추상적이지 않으므로 확장이 어렵고, 안정적이므로 변경이 어려움
- 제대로 설계된 컴포넌트라면 여기에 위치하지 않으며, 배제해야 하는 구역임
- ex) 데이터베이스 스키마 or String 클래스(String은 변동성이 없으므로 해롭지는 않음) 등
- 쓸모없는 구역(Zone of Uselessness)
- (1, 1) 주변 구역에 위치한 컴포넌트들
- 최고로 추상적이지만, 누구도 그 컴포넌트에 의존하지 않음(쓸모 없음)
- 이는 누구도 구현하지 않은 채 남겨진 추상클래스인 경우가 많음
- 주계열(The Main Sequence)
- 변동성이 큰 컴포넌트들을 두 배제 구역으로부터 가능한 멀리 떨어뜨리는 선분
- 쓸모없지 않으면서도 심각한 고통을 안겨주지도 않음
- 가장 바람직한 지점은 주 계열의 종점이긴 하지만 일부 컴포넌트는 불가능할 수 있음
- 주계열 바로 위에 또는 가깝게 위치할 때 가장 이상적
주계열과의 거리
- 주계열로부터 얼마나 떨어져 있는지 측정하는 지표.
- D=| A + I -1 |
- D가 0에 가까울수록 이상적(주계열 선 위)
- D가 0에서 가깝지 않다면 해당 컴포넌트는 재검토한 후 재구성하도록 계획 가능
- D지표의 평균과 분산을 통해 다른 컴포넌트에 비해 예외적인 컴포넌트 추출 가능 -> 리팩
- 반대로 한 컴포넌트의 D를 시간 별(릴리즈 별)로 측정하여 주계열에서 멀리 떨어진 시점에 들어간 feature에 대해 리뷰 가능
- 의존성 관리 지표는 설계의 의존성과 추상화 정도가 내가 “흘륭한” 패턴이라고 생각하는 수준에 얼마나 잘 부합하는지를 측정함
- 지표는 임의로 결정된 표준을 기초로 한 측정값에 지나지 않기에, 다음 단계에 대한 힌트로 사용하면 충분
'개발 > 도서 스터디' 카테고리의 다른 글
자바와 Junit을 활용한 실용주의 단위 테스트 5장 (0) | 2023.07.25 |
---|---|
자바와 Junit을 활용한 실용주의 단위 테스트 4장 (0) | 2023.07.24 |
clean architecture #1, 2 (0) | 2023.04.07 |
cleancode #15, Junit들여다보기 (0) | 2022.12.28 |
cleancode #10, 클래스 (0) | 2022.09.29 |