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

Jihyun·2021년 12월 5일
0

이 글은 한달 한권 챌린지의 두번째 책, 클린아키텍처를 읽고 정리한 내용이다.

소프트웨어 컴포넌트가 무엇인지, 컴포넌트를 구성하는 요소는 무엇인지 알아보고, 컴포넌트를 결합하여 시스템을 구성하는 방법에 대해 논의한다.


12장. 컴포넌트

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

컴포넌트가 마지막에 어떤 형태로 배포되든 잘 설계된 컴포넌트라면 반드시 독립적으로 배포 가능한, 독립적으로 개발 가능한 능력을 갖춰야 한다.


컴포넌트의 간략한 역사

소프트웨어 개발 초창기에는 메모리에서의 프로그램 위치와 레이아웃을 프로그래머가 직접 제어했다. 프로그램 시작부에 있는 '*200' 명령어는 메모리 주소 200에 로드할 코드를 생성하라고 컴파일러에 알려준다.

요즘의 프로그래머는 프로그램을 메모리의 어느 위치에 로드할지 고민할 필요가 거의 없다. 하지만 프로그래밍 초창기에는 프로그램을 로드할 메모리의 위치를 정하는 일이 프로그래머가 가장 먼저 결정해야 하는 사항 중 하나였다. 이 시절에는 프로그램의 위치가 한번 결정되면 재배치가 불가능했다.


구시대에는 라이브러리 함수의 소스 코드를 애플리케이션 코드에 직접 포함시켜 단일 프로그램으로 컴파일했기 때문에 라이브러리는 바이너리가 아니라 소스 코드 형태로 유지되었다. 메모리가 너무 작아 소스 코드 전체를 메모리에 상주시킬 수가 없었으므로 컴파일러는 느린 장치를 이용해서 소스 코드를 여러 차례 읽어야만 했다. 함수 라이브러리가 크면 클수록 컴파일은 더 오래걸렸다.


컴파일 시간을 단축시키기 위해 프로그래머는 함수 라이브러리의 소스 코드를 애플리케이션 코드로부터 분리했다. 함수 라이브러리를 개별적으로 컴파일하고, 컴파일된 바이너리를 메모리의 특정 위치에 로드했다. 함수 라이브러리에 대한 심벌 테이블을 생성한 후, 이를 이용해 애플리케이션 코드를 컴파일했다.

그러나 프로그램과 라이브러리가 사용하는 메모리가 늘어날수록 메모리 단편화가 계속될 수밖 에 없는 문제가 발생했다.

해결책은 재배치가 가능한 바이너리(relocatable binary)였다.


이제 프로그래머는 함수 라이브러리를 로드할 위치와 애플리케이션을 로 드할 위치를 로더에게 지시할 수 있게 되었다. 이를 통해 프로그래머는 오직 필요한 함수만을 로드할 수 있게 되었다.

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

프로그램이 라이브러리 함수를 호출한다면 컴파일러는 라이브러리 함수 이름을 외부 참조(external reference)로 생성했다. 라이브러리 함수를 정의하는 프로그램이라면 컴파일러는 해당 이름을 외부 정의(external definition)로 생성했다. 이렇게 함으로써 외부 정의를 로드 할 위치가 정해지기만하면 로더가 외부 참조를 외부 정의에 링크시킬 수 있게 된다.

이렇게 링킹 로더(linking loader)가 탄생했다.


링킹 로더의 등장으로 프로그래머는 프로그램을 개별적으로 컴파일하고 로드할 수 있는 단위로 분할할 수 있게 되었다.

그러나 1960 말부터 프로그램의 규모가 커지면서 링킹 로더도 사용하기에 불편할 정도가 되자, 마침내 로드와 링크가 두 단계로 분리되었다.

프로그래머는 링커(linker)라는 별도의 애플리케이션으로 링크 작업을 처리하도록 만들었다. 링커는 링크가 완료된 재배치 코드를 만들어 주었고, 그 덕분에 로더의 로딩 과정이 아주 빨라졌다. 한번 만들어둔 실행 파일은 언제라도 빠르게 로드할 수 있게 되었다. 그러나 여전히 로드 시간에 비해 컴파일-링크 시간이 병목 구간이었다.

저장장치 및 메모리가 급속도로 발달하면서 1990년대 후반이 되자, 프로그래머가 프로그램을 성장시키는 속도보다 링크 시간이 줄어드는 속도가 더 빨라지기 시작했다.

이렇게 컴포넌트 플러그인 아키텍처(component plugin architecture)가 탄생했다.



13장. 컴포넌트 응집도

이 장에서는 컴포넌트 응집도와 관련된 세 가지 원칙을 논의한다.


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

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


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

소프트웨어 컴포넌트가 릴리스 절차를 통해 추적 관리되지 않거나 릴리스 번호가 부여되지 않는다면 해당 컴포넌트를 재사용하고 싶어도 할 수도 없고, 하지도 않을 것이다.

릴리스 번호가 없다면 재사용 컴포넌트들이 서로 호환되는지 보증할 방법이 전혀 없으며, 새로운 버전이 언제 출시되고 무엇이 변했는지를 소프트웨어 개발자들이 전혀 알 수 없다. 따라서 릴리스 절차에는 적절한 공지와 함께 릴리스 문서 작성도 포함되어야 한다. 그래야 개발자가 충분한 정보를 바탕으로 새 릴리스를 통합할지, 한다면 언제 할지를 결정할 수 있다.


이 원칙을 소프트웨어 설계와 아키텍처 관점에서 보면 단일 컴포넌트는 응집성 높은 클래스와 모듈들로 구성되어야 함을 뜻한다. 하나의 컴포넌트로 묶인 클래스와 모듈은 반드시 함께 릴리스할 수 있어야 한다. 하나의 컴포넌트로 묶인 클래 스와 모듈은 버전 번호가 같아야 하며, 동일한 릴리스로 추적 관리되고, 동일 한 릴리스 문서에 포함되어야 한다.


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

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

SRP에서 단일 클래스는 변경의 이유가 여러 개 있어서는 안 된다고 말하듯이, CCP에서도 마찬가지로 단일 컴포넌트는 변경의 이유가 여러 개 있어서는 안 된다고 말한다.

다수의 애플리케이션에서 유지보수성(maintainability)은 재사용성보다 훨씬 중요하다. 애플리케이션에서 코드가 반드시 변경되어야 한다면, 변경이 여러 컴포넌트에 분산되어 발생하기보다는 차라리 변경 모두가 단일 컴포넌트에서 발생하는 편이 낫다. 만약 변경을 단일 컴포넌트로 제한 할 수 있다면, 해당 컴포넌트만 재배포하면 된다. 변경된 컴포넌트에 의존하지 않는 다른 컴포넌트는 다시 검증하거나 배포할 필요가 없다. 이를 통해 소프트웨어를 릴리스, 재검증, 배포하는 일과 관련된 작업량을 최소화할 수 있다.

OCP 에서는 클래스가 변경에는 닫혀 있고 확장에는 열려 있어야 한다고 말한다. 우리는 발생 할 가능성이 있거나 과거에 발생했던 대다수의 공통적인 변경에 대해서 클래스가 닫혀 있도록설계한다.

CCP에서는 동일한 유형의 변경에 대해 닫혀 있는 클래스들을 하나의 컴포넌트로 묶음으로써 OCP에서 얻은 교훈을 확대 적용한다. 따라서 변경이 필요한 요구사항이 발생했을 때, 그 변경이 영향을 주는 컴포넌트들이 최소한으로 한정 될 가능성이 확실히 높아진다.

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

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


클래스와 모듈을 어느 컴포넌트에 위치시킬지 결정할 때 도움되는 원칙이다. CRP에서는 같이 재사용되는 경향이 있는 클래스와 모듈들은 같은 컴포넌트에 포함해야 한다고 말한다.


개별 클래스가 단독으로 재사용되는 경우는 거의 없다. 대체로 재사용 가능한 클래스는 재사용 모듈의 일부로써 해당 모듈의 다른 클래스와 상호작용 하는 경우가 많다. CRP에서는 이런 클래스들이 동일한 컴포넌트에 포함되어야 한다고 말한다. 이러한 컴포넌트 내부에서는 클래스들 사이에 수많은 의존성이 있으리라고 예상할 수 있다.

의존하는 컴포넌트가 있다면 해당 컴포넌트의 모든 클래스에 대해 의존함을 확실히 인지해야 한다. 즉, 한 컴포넌트에 속한 일부 클래스에만 의존하고 다른 클래스와는 독립적일 수 없음을 확실히 해야 한다.


따라서 CRP는 어떤 클래스를 한데 묶어도 되는지보다는, 어떤 클래스를 한데 묶어서는 안 되는지에 대해서 훨씬 더 많은 것을 이야기한다. CRP는 강하게 결합되지 않은 클래스들을 동일한 컴포넌트에 위치시켜서는 안 된다고 말한다.


CRP는 ISP의 포괄적인 버전이다. ISP는 시용하지 않은 메서드가 있는 클래스에 의존하지 말라고 조언한다. CRP는 사용하지 않는 클래스를 가진 컴포넌트에 의존하지 말라고 조언한다.


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

REP와 CCP는 포함(inclusive) 원칙이며, 컴포넌트를 더욱 크게 만든다. CRP는 배제(exdusive) 원칙이며, 컴포넌트를 더욱 작게 만든다.

뛰어난 아키텍트라면 이 원칙들이 균형을 이루는 방법을 찾아야 한다. 또한, 이 균형에서 개발팀이 현재 관심을 기울이는 부분을 중족시키는 위치를 찾아야 하며, 또한 시간이 흐르면서 개발팀 이 주의를 기울이는 부분 역시 변한다는 사실도 이해하고 있어야 한다.

프로젝트의 컴포넌트 구조는 시간과 성숙도에 따라 변한다. 프로젝트의 컴포넌트 구조는 프로젝트가 실제로 수행하는 일 자체보다는 프로젝트가 발전되고 사용되는 방법과 더 관련이 깊다.



14장. 컴포넌트 결합

이 장에서는 컴포넌트 사이의 관계를 설명하는 세 가지 원칙을 논의한다.


ADP: 의존성 비순환 원칙

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


개발 환경을 릴리스 가능한 컴포넌트 단위로 분리하면 컴포넌트는 개별 개발자 또는 단일 개발팀이 책임질 수 있는 작업 단위가 된다.

개발자가 해당 컴포넌트가 동작하도록 만든 후, 릴리스 번호를 부여하고 릴리스하여 다른 개발자가 사용할 수 있도록 만든다. 그런 다음 자신만의 공간에서 지속적으로 수정한다. 나머지 개발자는 릴리스된 버전을 사용한다.

컴포넌트가 변경 되더라도 다른 팀에 즉각 영향을 주지는 않는다. 각 팀은 특정 컴포넌트가 새롭게 릴리스되면 자신의 컴포넌트를 해당 컴포넌트에 맞게 수정할 시기를 스스로 결정할 수 있다. 뿐만 아니라 통합은 작고 점진적으로 이뤄진다. 특정 시점에 모든 개발자가 한데 모여서 진행 중인 작업을 모두 통합하는 일은 사라진다.


이 절차가 성공적으로 동작하려면 컴포넌트 사이의 의존성 구조를 반드시 관리해야 한다. 의존성 구조에 순환이 있어서는 안 된다. 컴포넌트 간의 의존성 구조를 그린다면 어느 컴포넌트에서 시작하더라도, 의존성 관계를 따라가면서 최초의 컴포넌트로 되돌아갈 수 없어야 한다. 즉, 비순환 방향 그래프(Directed Acyclic Graph, DAG)여야 한다.


시스템 전체를 릴리스해야 할 때가 오면 릴리스 절차는 상향식으로 진행된다. 이처럼 구성요소 간 의존성을 파악하고 있으면 시스템을 빌드하는 방법을 알 수 있다.


순환이 발생할 경우
  1. 컴포넌트를 분리하기가 상당히 어려워진다.
  2. 단위 테스트를 하고 릴리스를 하는 일이 어려워진다.
  3. 컴포넌트를 어떤 순서로 빌드해야 올바를지 파악하기 어려워진다.

컴포넌트 사이의 순환을 끊고 의존성을 다시 DAG로 원상복구하기 위한 두가지 해결책을 살펴보자.

  1. DIP(의존성 역전 원칙)을 적용한다.
  2. 두 컴포넌트가 모두 의존하는 새로운 컴포넌트를 만든다. 그리고 두 컴포넌트가 모두 의존하는 클래스들을 새로운 컴포넌트로 이동시 킨다

2번 해결책에서 시사하는 바는 요구사항이 변경되면 컴포넌트 구조도 변경될 수 있다는 사실이다. 실제로 애플리케이션이 성장함에 따라 컴포넌트 의존성 구조는 서서히 흐트러지며 또 성장한다. 따라서 의존성 구조에 순환이 발생하는지를 항상 관찰해야 한다. 순환이 발생하면 어떤 식으로든 끊어야 한다. 이 말은 때론 새로운 컴포넌트를 생성하거나 의존성 구조가 더 커질 수도 있음을 의미한다.


SDP: 안정된 의존성 원칙

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


설계를 유지하다 보면 변경은 불가피하다. CCP을 준수함으로써, 컴포넌트가 다른 유형의 변경에는 영향받지 않으면서도 특정 유형의 변경에만 민감하게 만들 수 있다. 이처럼 컴포넌트 중 일부는 변동성을 지니도록 설계된다. 변경이 쉽지 않은 컴포넌트가 변동이 예상되는 컴포넌트에 의존하게 만들어서는 절대로 안 된다. 한번 의존하게 되면 변동성이 큰 컴포넌트도 결국 변경이 어려워진다.


소프트웨어 컴포넌트를 변경하기 어렵게 만드는 확실한 방법 하나는 수많은 다른 컴포넌트가 해당 컴포넌트에 의존하게 만드는 것이다. 사소한 변경이라도 의존하는 모든 컴포넌트를 만족시키면서 변경하려면 상당한 노력이 들기 때문이다.


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

모든 컴포넌트가 최고로 안정적인 시스템이라면 변경이 불가능하다. 이는 바람직한 상황이 아니다.

우리가 컴포넌트 구조를 설계할 때 기대하는 것은 불안정한 컴포넌트도 있고 안정된 컴포넌트도 존재하는 상태다.


SAP:안정된 추상화 원칙

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


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

  1. 안정된 컴포넌트는 추상 컴포넌트여야 하며, 이를 통해 안정성이 컴포넌트를 확장하는 일을 방해해서는 안 된다.

  2. 불안정한 컴포넌트는 반드시 구체 컴포넌트여야 하며, 이를 통해 컴포넌트 내부의 구체적인 코드를 쉽게 변경할 수 있어야 한다.


따라서 안정적인 컴포넌트라면 반드시 인터페이스와 추상 클래스로 구성 되어 쉽게 확장할 수 있어야 한다. 안정된 컴포넌트가 확장이 가능해지면 유연성을 얻게 되고 아키텍처를 과도하게 제약하지 않게 된다.

0개의 댓글