본문 바로가기

클린아키텍처

[클린 아키텍처] 4부 컴포넌트 원칙

** 클린 아키텍처 소프트웨어 구조와 설계의 원칙 책을 읽고 작성한 글입니다.

 

컴포넌트란?

SOLID 원칙이 벽돌을 벽과 방에 배치하는 방법이라면,

컴포넌트 원칙은 빌딩에 방을 배치하는 방법이라고 할 수 있습니다.

컴포넌트 = 배포 단위

컴포넌트란, 시스템 구성 요소로 배포할 수 있는 가장 작은 단위입니다.

 

  • 컴파일형 언어: 바이너리 파일의 결합체 (ex. 자바의 jar, 루비의 gem, 닷넷의 dll)
  • 인터프리터형 언어: 소스파일의 결합체

 

여러 컴포넌트를 서로 링크하여 단일 파일을 생성하거나,

묶어서 .war과 같은 단일 아카이브로 만들 수 있습니다.

 

또한 각 컴포넌트를 동적으로 로드할 수 있는 플러그인으로 만들거나,

각각을 .exe 파일로 배포하는 것도 가능합니다.

 

컴포넌트의 간략한 역사

오늘날: 프로그램이 메모리의 어느 위치에 로드되어야 하는지 고민할 필요가 없습니다.

초창기: 프로그래머가 메모리에서 프로그램의 위치와 레이아웃을 직접 제어해야 했습니다.

  • 프로그램이 시작하는 메모리 주소를 정적으로 컴파일러에 알려줘야 했습니다
  • 위치가 한번 결정되면 재배치가 불가능했습니다

초창기의 라이브러리 접근 방법

애플리케이션 코드에 직접 라이브러리 함수 소스코드를 포함시켜 단일 프로그램으로 컴파일했습니다.

(바이너리가 아닌 소스코드 형태)

초창기 접근방법의 문제점

  • 메모리가 비싸고 자원이 한정적 → 소스코드 전체를 메모리에 상주시키는 게 불가능
  • 컴파일러는 소스코드를 여러 차례 읽어야 함

 

해결책: 재배치성

함수 라이브러리를 애플리케이션 코드로부터 분리 → 개별 컴파일 → 컴파일된 바이너리를 메모리 특정 위치에 로드

 

하지만 애플리케이션이 점점 커지면서 할당된 공간을 넘어서게 되었고,

애플리케이션을 두 개의 주소 세그먼트로 분리했지만 이는 지속 가능한 방법이 아니었습니다.

 

위 그림처럼 단편화가 지속되었기 때문입니다.

재배치가 가능한 바이너리의 등장

지능적인 로더를 사용해서 메모리에 재배치할 수 있는 형태의 바이너리를 생성하도록 컴파일러를 수정했습니다.

(상대적인 위치로 저장)

플래그와 로더의 역할

🏷 플래그: "원래 어느 주소에서 시작할 예정이었는지" 정보

🏷 로더: 재배치 코드가 자리할 위치 정보 계산

 

예시:

  • 플래그: "시작주소 0x1000"
  • 의미: "이 바이너리 안의 모든 주소는 0x1000부터 시작한다고 가정하고 만들어짐"
  • 로더 역할: "실제로는 0x5000에 로드해야 하니까, 모든 주소에 +0x4000 해주자"

 

🔄 변환 공식:

새주소 = 원래주소 + (실제로드위치 - 플래그시작주소)
      = 원래주소 + (0x5000 - 0x1000)
      = 원래주소 + 0x4000

 

외부 참조와 외부 정의

컴파일러는 재배치 가능한 바이너리 안 함수 이름을 메타데이터 형태로 생성하도록 수정되었습니다.

  • 외부 참조 (External Reference): 라이브러리 함수를 호출할 때 사용 ("나는 print라는 함수가 필요한데, 내 안에는 없네? 밖에서 찾아줘!"라고 요청)
  • 외부 정의 (External Definition): 라이브러리 함수를 정의하는 프로그램 ("내 안에 print라는 함수가 있어. 필요한 사람 여기서 가져다 써!"라고 알려줌)

 

여기까지가 링킹 로더의 탄생!

 

 

링커

작은 프로그램과 작은 라이브러리 링크 시에는 잘 동작했지만,

프로그램이 커지면 느려졌습니다.

실행할 때마다 조립해서 메모리에 올려야했기 때문입니다.

 

해결책: 로드와 링크를 두 단계로 분리

  • 링크: 프로그래머가 맡음
  • 링커: 링크가 완료된 재배치 코드를 생성 (미리 조립해서 실행 파일을 만들어 놓음)

이후 소스 모듈은 .c → .o로 컴파일 후 링커로 전달되어

빠르게 로드될 수 있는 형태의 실행파일(.exe)이 되었습니다.

 

링킹 로더와 링커 비교

항목 링킹 로더 링커
링크 시점 실행할 때마다 컴파일할 번만
실행 속도 느림 (매번 링크) 빠름 (이미 링크됨)
생성물 없음 (매번 메모리에서 조립) 실행 파일 (.exe)
장점 간단함 빠른 실행
단점 느림 컴파일 시간 증가

새로운 문제

각 모듈의 컴파일은 빠르지만, 전체 모듈 양이 매우 많다 보니 컴파일은 오래 걸렸습니다.

로드 시간은 빠르지만, 컴파일-링크 시간이 병목이 되었습니다.

머피의 법칙

컴파일하고 링크하는데 사용 가능한 시간을 모두 소모할 때까지 프로그램은 커진다

 

다시 해결: 무어의 법칙

디스크는 작아지지만 빨라졌고, 메모리도 저렴해졌습니다.

컴퓨터와 장치가 빨라짐에 따라 로드와 링크를 동시에 할 수 있게 되었습니다.

컴포넌트 플러그인 아키텍처 탄생

 

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

 


 

컴포넌트 응집도

그렇다면 우리는 어떤 클래스를 어떤 컴포넌트에 포함시켜야 할까요?

 

컴포넌트 응집도와 관련된 세 가지 원칙이 있습니다.

  • REP: 재사용/릴리스 등가 원칙
  • CCP: 공통 폐쇄 원칙
  • CRP: 공통 재사용 원칙

하나씩 차근차근 설명해보도록 하겠습니다.

 

REP: 재사용/릴리스 등가 원칙 (Reuse/Release Equivalence Principle)

"재사용 단위는 릴리즈 단위와 같다"

 

우리는 소프트웨어 재사용의 시대에 살고 있습니다.

메이븐, 라이닝언, RVM 같은 모듈 관리 도구가 우후죽순 등장했습니다.

 

당연한 원칙

  • 소프트웨어 컴포넌트가 릴리즈 절차를 통해 추적 관리되지 않거나 릴리즈 번호가 부여되지 않는다면 컴포넌트 재사용이 불가능합니다
  • 릴리즈 번호가 없다면 재사용 컴포넌트들이 서로 호환되는지 보증할 방법도 없습니다
  • 새로운 버전이 언제 출시되고 무엇이 변했는지 개발자들이 알아야 합니다

소프트웨어 설계와 아키텍처 관점

  • 단일 컴포넌트는 응집성 높은 클래스+모듈로 구성되어야 합니다
  • 컴포넌트를 구성하는 모든 모듈들은 서로 공유하는 중요한 테마나 목적이 있어야 합니다
  • 하나의 컴포넌트로 묶인 클래스와 모듈은 반드시 함께 릴리스 가능해야 합니다
  • 같은 버전 번호 하에 추적 관리되어야 합니다

이 조언만으로는 클래스와 모듈을 단일 컴포넌트로 묶는 방법을 설명하기 힘들지만,

원칙 자체는 중요합니다. 어기면 이치에 어긋나기 때문입니다.

 

다른 CCP, CRP 원칙이 REP를 엄격하게, 하지만 제약을 가하는 측면에서 정의합니다.

 

 

단일 컴포넌트는 응집성 높은 클래스와 모듈들로 구성되어야 하고,

하나의 컴포넌트로 묶인 클래스와 모듈은 버전 번호가 같아야 하며,

동일한 릴리스로 추적 관리되어야 합니다.

 

 

 

CCP: 공통 폐쇄 원칙 (Common Closure Principle)

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

 

이는 컴포넌트 관점에서의 SRP입니다.

CCP에서도 마찬가지로 단일 컴포넌트는 변경의 이유가 여러 개 있어서는 안 된다고 말합니다.

 

대다수 애플리케이션에서 유지보수성 > 재사용성

 

코드 변경이 필요할 때 단일 컴포넌트에서만 변경이 발생하는 편이 낫습니다.

그래야 다른 컴포넌트를 검증 및 배포할 필요가 없습니다.

 

이러한 관점에서 CCP는 변경될 가능성이 있는 클래스는 모두 한 곳으로 묶을 것을 권합니다.

이렇게 하면 릴리즈, 재검증, 배포하는 일과 관련된 작업량을 최소화할 수 있습니다.

 

OCP와의 관련

CCP의 폐쇄 = OCP의 폐쇄입니다.

 

OCP에서는 클래스가 변경에 닫혀있고 확장에 열려있어야 한다고 말합니다.

완전한 폐쇄는 불가능하므로 대다수의 공통적인 변경에 대해서 클래스가 닫혀있도록 설계해야 합니다.

 

동일한 유형의 변경에 대해 닫혀있는 클래스들을 하나의 컴포넌트로 묶으면 OCP를 확대 적용하는 것입니다.

 

CRP: 공통 재사용 원칙 (Common Reuse Principle)

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

 

클래스와 모듈을 어느 컴포넌트에 위치시킬지 결정할 때 도움되는 원칙입니다.

재사용되는 경향이 있는 클래스와 모듈을 같은 컴포넌트에 포함시켜야 합니다.

 

재사용 가능한 클래스는 재사용 모듈의 일부로 다른 클래스와 상호작용합니다.

CRP에서는 이러한 클래스들은 동일한 컴포넌트 내에 위치해야 한다고 말합니다.

 

CRP는 동일한 컴포넌트로 묶으면 안 되는 클래스가 무엇인지도 말해줍니다.

  • 어떤 컴포넌트가 다른 컴포넌트를 사용하면 의존성이 생성됩니다
  • 하나의 클래스에서만 의존하더라도 의존성은 약해지지 않습니다
  • 의존성으로 인해 사용하지 않는 컴포넌트가 변경될 가능성이 높아집니다
    • 의존하는 컴포넌트가 있다면 해당 컴포넌트의 모든 클래스에 의존함을 확실히 인지해야 합니다
    • 일부 클래스에만 의존하고 다른 클래스와는 독립적일 수 없습니다 → 배포가 필요해지기 때문
  • 강하게 결합되지 않는 클래스들을 동일한 컴포넌트에 위치시키면 안 됩니다.

ISP와의 관계

  • ISP는 사용하지 않은 메서드가 있는 클래스에 의존하지 말라고 조언
  • CRP는 사용하지 않는 클래스를 가진 컴포넌트에 의존하지 말라고 조언

요약: 필요하지 않은 것에 의존하지 말라

 

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

 

앞서 말했던 세 원칙은 서로 상충됩니다.

  • REP, CCP: 포함에 대한 원칙 → 컴포넌트가 커짐
  • CRP: 배제에 대한 원칙 → 컴포넌트가 작아짐

초기에는 CCP가 REP보다 중요합니다.

개발 가능성이 재사용성보다 중요하기 때문입니다.

 

때문에 일반적으로 초기에는 삼각형의 오른쪽에서 시작해서 점차 왼쪽으로 이동해갑니다.

즉, 컴포넌트 구조는 프로젝트가 실제 수행하는 일보다 프로젝트가 발전되고 사용되는 방법과 더 관련이 깊습니다.

 

결과적으로,

REP, CCP, CRP는 응집도가 가질 수 있는 다양한 복잡성을 설명합니다.

 

클래스를 묶어 컴포넌트를 만들 때,

재사용성과 개발 가능성이라는 상충하는 힘을 반드시 고려해야 합니다.

 

컴포넌트를 구성하는 방식은 프로젝트가 발전함에 따라 변경되고 진화합니다.

 


컴포넌트 결합

지금부터는 컴포넌트 사이의 관계를 설명하는 세기지 원칙에 대해 소개하면서,

개발 가능성과 논리적 설계 사이의 균형에 대해 다뤄보겠습니다.

 

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

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

 

숙취 증후군

무언가를 만들고 퇴근했더니 다음 날 전혀 돌아가지 않는 경우가 있습니다.

이는 누군가 더 늦게까지 남아서 내가 의존하고 있던 무언가를 수정했기 때문입니다.

이러한 상황은 많은 개발자가 동일한 소스 파일을 수정하는 환경에서 발생합니다.

 

해결책 1: 주단위 빌드

4일 동안 서로 신경 쓰지 않고 일하고, 1일 동안 싱크를 맞춥니다

  • 장점: 4일 동안 고립되어 작업 가능
  • 단점: 하루는 업보를 치러야 하고, 프로젝트가 커지면 통합에 시간이 많이 걸림

개발보다 통합에 소요되는 시간이 많아지면서 효율성이 떨어지고, 격주 빌드로 바뀌거나 빌드 일정을 미루게 됩니다.

해결책 2: 의존성 비순환 원칙

개발 환경을 릴리즈 가능한 컴포넌트 단위로 분리합니다.

여기서 컴포넌트는 단일 개발팀이 책임질 수 있는 작업 단위를 말합니다.

 

순서는 아래와 같습니다.

 

개발자가 컴포넌트를 동작하도록 만들면 릴리즈하고, 릴리즈 번호를 부여합니다.

 

릴리즈가 되면 해당 컴포넌트는 다른 팀에서 사용 가능하고,

컴포넌트가 새로 릴리즈되면 다른 팀에서는 새 릴리즈를 적용할지 결정합니다.

 

이때 그 어떤 팀도 다른 팀에 의해 좌우되지 않습니다.

특정 컴포넌트가 변경되더라도 다른 팀에 즉각적인 영향을 주지 않는다는 것입니다.

 

다만 성공적 수행을 위해서는 의존성 관리를 잘 해야 하며,

의존성 구조에 순환이 있어서는 안 됩니다.

 

아래 그림은 방향 그래프입니다. (컴포넌트는 정점, 의존성 관계는 간선)

 

 

어느 컴포넌트에서 시작하더라도 최초의 컴포넌트로 되돌아갈 수 없습니다. (비순환 방향 그래프)

 

예시

Presenters 담당 팀에서 새로운 릴리즈를 생성한다고 가정해봅시다.

 

  • 영향받는 팀: View, Main 컴포넌트 (의존성 화살표를 거꾸로 따라가면 됨)
  • 이 팀들은 새로운 릴리즈를 언제 통합할지 결정하면 됩니다

 

Presenters 컴포넌트를 만드는 개발자가 테스트하려고 한다면,

Interactors와 Entities를 이용해서 Presenters 자체 버전만 빌드하면 끝입니다.

(나머지 컴포넌트는 전혀 관련이 없으므로)

 

결과적으로, Presenters를 만드는 개발자는 테스트를 구성할 때 대체로 적은 노력이 듭니다.

 

 

시스템 전체를 릴리즈해야 할 때: 상향식으로 진행 (bottom-up)

Entities → Database → Interactors →
Presenters → View → Controllers → Authorizer → Main

 

 

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

만약 Entities 클래스가 Authorizer에 포함된 클래스 하나를 사용한다면 순환이 발생합니다.

 

 

문제 1

Database 컴포넌트 개발자는 릴리즈 시 Entities 컴포넌트와 호환되어야 합니다.

 

하지만 Entities 컴포넌트에는 순환이 존재 → Authorizer와도 호환되어야 합니다

이때, Authorizer는 또 Interactors에 의존합니다

 

이로 인해 Database 릴리즈는 매우 어려워집니다.

 

문제 2

Entities 컴포넌트를 테스트할 때 Authorizer와 Interactors까지도 빌드 및 통합해야 합니다.

 

순환이 생기면 컴포넌트 빌드 순서를 파악하기 힘들고, 단위 테스트/릴리스도 어려워집니다.

빌드 관련 이슈도 증가합니다.

 

해결책: 순환 끊기 (DAG로 원상복구)

첫 번째 방법: 의존성 역전 원칙 (DIP)

 

Permission 인터페이스를 Entities에 위치시키고, Authorizer가 이 인터페이스를 상속하도록 만듭니다.

Entities → Authorizer 의존성이 제거됩니다.

 

두 번째 방법: 새로운 컴포넌트 생성

 

 

새로운 컴포넌트 Permissions를 생성해 두 컴포넌트가 모두 의존하는 클래스들을 이동시킵니다.

Entities → Permission ← Authorizer

 

흐트러짐 (jitters)

두 번째 해결책에서 시사하는 바는 요구사항이 변경되면 컴포넌트 구조도 변경될 수 있다는 것입니다.

실제로도 의존성 구조는 서서히 흐트러지고 성장합니다.

따라서 의존성 구조에 순환이 발생하는지 항상 관찰해야 합니다.

 

하향식(top-down) 설계

지금까지의 내용을 보면, 컴포넌트 구조는 하향식 설계가 불가능하다는 결론에 다다를 수 있습니다.

시스템에서 가장 먼저 설계할 수 없고, 시스템이 성장하고 변경될 때 진화하기 때문입니다.

 

이는 직관과 어긋나기도 합니다.

컴포넌트와 같이 큰 단위는 고수준의 기능적인 구조로 분해할 수 있다고 기대하기 때문입니다.

("큰 기능을 쪼개면 컴포넌트가 되겠지?")

 

큰 단위로 분해된 집단을 관찰할 때 시스템의 기능적 측면을 컴포넌트가 표현하리라고 믿습니다.

→ 하지만 이는 컴포넌트 의존성 다이어그램이 가진 속성이 아닙니다.

 

컴포넌트는 코드의 변경과 유지보수를 용이하게 하기 위해 변화합니다.

 

의존성 다이어그램은 기능을 보여주는 것이 아니라 빌드 가능성과 유지보수성을 보여주는 지도입니다.

때문에 컴포넌트 구조를 초기에 설계할 수 없습니다.

 

모듈이 쌓여가면 숙취 증후군을 겪지 않고 프로젝트를 개발하기 위해 의존성 관리에 대한 요구가 늘어납니다.

→ SRP, CCP 등에 관심을 가지게 됩니다.

 

의존성 구조와 관련된 최우선 관심사는 변동성을 격리하는 것입니다.

자주 변경되는 컴포넌트로 인해 다른 컴포넌트가 영향받길 원하지 않습니다.

 

예: GUI 표현 형식이 업무 규칙까지 영향을 주길 바라지 않습니다.

 

애플리케이션이 성장함에 따라 재사용 가능한 요소를 만드는 일에 관심을 가지게 되고,

이때 CRP가 영향을 미칩니다.

 

또, 순환이 발생하면 ADP를 적용합니다.

이렇듯 의존성 그래프는 조금씩 흐트러지면서 성장합니다.

 

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

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

 

설계는 정적이지 않습니다. 변경이 불가피합니다.

 

공통 폐쇄 원칙을 준수하면 컴포넌트가 다른 유형의 변경에는 영향받지 않으면서

특정 유형의 변경에만 민감하게 만들 수 있습니다.

 

SDP는 변경이 쉽지 않은 컴포넌트가 변동이 예상되는 컴포넌트에 의존하게 만들어서는 안 된다는 원칙입니다.

 

한번 의존하게 되면 변동성이 큰 컴포넌트도 변경이 어려워지기 때문입니다.

 

변경이 쉽도록 설계했지만,

변경이 쉽지 않은 모듈에 의존성을 갖게 되면 해당 컴포넌트를 변경하는 일이 매우 도전적으로 변합니다.

 

안정성

동전을 옆면으로 세우고 건드리지 않을 경우 오랫동안 서 있을 수 있긴 하지만,

우리는 이게 안정적이라고 말할 순 없습니다.

 

안정적: 쉽게 움직이지 않음

 

안정성은 변경을 만들기 위해 필요한 작업량과 관련됩니다.

(변경하기 어려울수록 안정적)

 

소프트웨어를 변경하기 어렵게 만드는 확실한 방법은 

수많은 다른 컴포넌트가 해당 컴포넌트에 의존하게 만드는 것입니다.

 

아래 그림에서 X는 안정된 컴포넌트입니다.

 

 

  • 세 컴포넌트가 X에 의존 → 변경하지 말아야 할 이유가 세 가지
  • X는 어디에도 의존하지 않음 → 변경되도록 만들 수 있는 외적인 영향이 없음
  • X는 independent

 

아래의 Y는 불안정한 컴포넌트입니다.

 

 

  • 어떤 컴포넌트도 Y에 의존하지 않음 (책임성이 없음)
  • Y는 세 컴포넌트에 의존함
  • Y는 dependent

 

안정성 지표

I (불안정성) = Fan-out / (Fan-in + Fan-out)

 

  • Fan-out: 외부 클래스에 의존하는 컴포넌트 내부의 클래스 개수
  • Fan-in: 컴포넌트 내부의 클래스에 의존하는 컴포넌트 외부의 클래스 개수

 

I = 0이면 최고로 안정된 컴포넌트, I = 1이면 최고로 불안정한 컴포넌트입니다.

 

 

Cc 컴포넌트의 안정성: I = 1/(1+3)

 

  • I = 1이면 컴포넌트가 가질 수 있는 최고로 불안정한 상태입니다. 책임성이 없으며 의존적입니다.
  • I = 0이면 해당 컴포넌트가 다른 컴포넌트에 의존하지 않습니다. 다른 컴포넌트를 책임지며 독립적입니다.

SDP에서 컴포넌트의 I 지표는 그 컴포넌트가 의존하는 다른 컴포넌트들의 I보다 커야 한다고 말합니다.

 

즉, 의존성 방향으로 갈수록 I 지표 값이 감소해야 합니다.

 

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

모든 컴포넌트가 최고로 안정적인 시스템이라면 변경이 불가능합니다.

 

아래 그림은 이상적인 구조입니다.

 

 

  • 위쪽에는 변경 가능한 컴포넌트
  • 아래에는 안정된 컴포넌트
  • 위에서 아래로 의존

 

아래 그림은 SDP가 어떻게 위배되는지 보여줍니다.

 

Flexible은 변경하기 쉽도록 설계한 컴포넌트이지만,

Stable이 Flexible에 의존하면서 Stable의 I 지표보다 Flexible의 I 지표가 더 작아져 변경이 어려워집니다.

 

이를 해결하기 위해서는 Stable의 Flexible에 대한 의존성을 어떤 식으로든 끊어야 합니다.

 

의존성 존재의 이유:

 

 

Stable 내부의 클래스 U가 Flexible 내부의 클래스 C를 사용하기 때문입니다.

 

DIP를 활용하여 해결:

 

 

  1. US라는 인터페이스를 생성 후 UServer 컴포넌트에 넣습니다
  2. US 인터페이스에는 U가 사용하는 모든 메서드가 선언되어 있습니다
  3. C가 해당 인터페이스를 구현하도록 만듭니다

 

결과:

 

  • Stable → Flexible 의존성 제거
  • 두 컴포넌트 모두 UServer에 의존
  • UServer의 I = 0, Flexible의 I = 1

 

추상 컴포넌트

인터페이스만을 포함하는 컴포넌트 (위 예제에서 UServer)

  • 상당히 안정적
  • 덜 안정적인 컴포넌트가 의존할 수 있는 이상적 대상
  • 정적 타입 언어를 사용할 때 매우 흔하게 사용

 

동적 타입 언어를 사용할 때 (Python, Ruby)

  • 추상 컴포넌트가 존재하지 않고, 추상 컴포넌트로 향하는 의존성도 없음
  • 의존성 구조가 훨씬 단순 (의존성 역전 시 인터페이스 선언/상속이 필요하지 않기 때문)

 

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

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

 

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

고수준 아키텍처나 정책 결정과 관련된 소프트웨어는 자주 변경해서는 안 됩니다.

(업무 로직이나 아키텍처와 관련된 결정에는 변동성이 없기를 기대)

 

고수준 정책을 캡슐화하는 소프트웨어는 반드시 I = 0 컴포넌트에 위치해야 합니다.

 

하지만 최고로 안정된 상태이면서 동시에 변경에 충분히 대응할 수 있을 정도로 유연하게 만들려면 어떻게 해야 할까요?

 

OCP (개방 폐쇄 원칙)에서 그 답을 찾을 수 있습니다.

⇒ 추상 클래스가 이 원칙을 준수합니다.

안정된 추상화 원칙

안정성(stability)과 추상화 정도(abstractness) 사이의 관계를 정의합니다.

 

  • 안정된 컴포넌트는 추상 컴포넌트여야 하며, 안정성이 컴포넌트를 확장하는 일을 방해해서는 안 됩니다
    • 안정적인 컴포넌트라면 반드시 인터페이스와 추상 클래스로 구성되어 쉽게 확장할 수 있어야 합니다
    • 안정된 컴포넌트가 확장 가능해지면 유연성을 얻게 되고 아키텍처를 과도하게 제약하지 않게 됩니다
  • 불안정한 컴포넌트는 구체 컴포넌트여야 하며, 컴포넌트 내부의 구체적인 코드를 쉽게 변경할 수 있어야 합니다

 

SAP와 SDP를 결합하면 컴포넌트에 대한 DIP와 마찬가지가 됩니다.

 

  • SDP: 의존성이 반드시 안정성의 방향으로 향합니다
  • SAP: 안정성이 결국 추상화를 의미합니다
  • DIP: 따라서 의존성은 추상화의 방향으로 향하게 됩니다

 

하지만 DIP는 클래스에 대한 원칙이고,

클래스의 경우 중간은 존재하지 않습니다. (추상적이거나 아니거나 둘 중 하나)

 

SDP, SAP는 컴포넌트에 대한 원칙이므로 중간이 있을 수 있습니다.

 

추상화 정도 측정하기

A 지표: 컴포넌트의 추상화 정도를 측정한 값.

컴포넌트의 클래스 총 수 대비 인터페이스와 추상 클래스의 개수를 단순히 계산한 값입니다.

 

  • Nc: 컴포넌트의 클래스 개수
  • Na: 컴포넌트의 추상 클래스와 인터페이스의 개수
  • A (추상화 정도) = Na / Nc

 

A 지표는 0~1 사이의 값입니다.

 

주계열

안정성(I)과 추상화(A) 사이의 관계를 나타냅니다.

 

 

모든 컴포넌트가 (0, 1), (1, 0)에 존재하지는 않습니다.

 

예를 들어, 추상 클래스는 흔히 다른 추상 클래스로부터 파생해서 만들곤 합니다.

 

이러한 파생 클래스는 추상적이면서도 의존성(안정성 낮아짐)을 가집니다.

⇒ 최고로 추상적이지만, 최고로 안정적인 것은 아닙니다.

 

 

배제할 구역 (Zone of Exclusion)

 

모든 컴포넌트가 (0, 1), (1, 0)에 위치해야 할 순 없으므로 컴포넌트가 위치할 수 있는 합리적인 지점을 정의하는 점의 궤적입니다.

고통의 구역 (Zone of Pain) - (0, 0)

  • 매우 안정적이며 구체적입니다. 뻣뻣한 상태입니다
  • 추상적이지 않으므로 확장이 불가능합니다
  • 안정적이므로 변경하기도 상당히 어렵습니다

 

사실 일부 소프트웨어 엔티티는 고통의 구역에 위치합니다.

 

  • 예: 데이터베이스 스키마
  • 예: 유틸리티 라이브러리(String)
    • 광범위하게 사용되지만 변동성이 거의 없어야 합니다
    • 이런 경우는 고통의 구역에 있어도 해롭지 않습니다

 

쓸모없는 구역 (Zone of Uselessness) - (1, 1)

  • 최고로 추상적이지만, 누구도 그 컴포넌트에 의존하지 않습니다
  • 사용되지 않으면서 시스템의 기반 코드에 남아 있는 경우입니다

 

배제 구역 벗어나기

변동성이 큰 컴포넌트는 두 배제 구역으로부터 가능한 한 멀리 떨어뜨려야 합니다.

 

이 선분을 주계열(Main Sequence)이라고 부릅니다.

 

주계열에 위치한 컴포넌트

 

  • 자신의 안정성에 비해 너무 추상적이지도 않고
  • 추상화 정도에 비해 너무 불안정하지도 않습니다

 

주계열과의 거리

세 번째 지표입니다.

주계열로부터 컴포넌트가 얼마나 멀리 떨어져 있는지를 나타냅니다.

 

D (거리) = | A + I - 1 |

 

  • 유효범위: [0, 1]
  • D = 0: 컴포넌트가 주계열 위에 위치
  • D = 1: 컴포넌트가 주계열로부터 가장 멀리 떨어져 있음

 

설계를 통계적으로 분석하기

 

D 지표의 평균과 분산을 구합니다.

 

  • 관리 한계를 결정하기 위한 목적으로 사용합니다
  • 극히 예외적인 컴포넌트를 식별하여, 어느 정도까지 주계열에 가깝도록 재구성할 것인지를 결정합니다

 

각 컴포넌트의 D 값을 시간에 따라 그려보는 것도 가능합니다.

 

 

예를 들어 R2.1은 관리 한계를 초과한 것이므로 시간을 들여서 조사해볼 가치가 있습니다.

 

 

의존성 관리 지표는 설계의 의존성과 추상화 정도가

내가 '훌륭한' 패턴이라고 생각하는 수준에 얼마나 잘 부합하는지를 측정합니다.

 

좋은 의존성도 있지만 좋지 않은 의존성도 있습니다.

 

지표는 신이 아닙니다. 그렇지만 이러한 지표로부터 유용한 것을 찾으려 노력해야 합니다.


마치며

컴포넌트 원칙은 SOLID 원칙보다 더 높은 수준에서 소프트웨어 구조를 조직화하는 방법을 제시합니다.

 

  • 컴포넌트 응집도: REP, CCP, CRP를 통해 어떤 클래스를 어떤 컴포넌트에 포함시킬지 결정합니다
  • 컴포넌트 결합: ADP, SDP, SAP를 통해 컴포넌트 간의 의존성을 어떻게 관리할지 결정합니다

 

이러한 원칙들은 프로젝트가 성장함에 따라 진화하며, 초기 설계로 정의될 수 없습니다.

 

시스템이 발전하면서 변동성 격리, 재사용성, 개발 가능성 사이의 균형을 찾아가는 과정에서 자연스럽게 형성됩니다.