본문 바로가기
IT 공부/객체지향 설계 공부

클린 아키텍처 - 로버트 c 마틴 요약 part 2

by exdus3156 2023. 10. 19.
로버트 c 마틴의 클린 아키텍처를 읽고 공부한 내용을 바탕으로 작성한 요약 글입니다. 챕터의 구성은 책을 따르지만, 그 내용은 인용된 부분도 있으나 대부분은 제가 이해한 방식대로 제 언어로 풀어 기록했으니, 정보를 얻으실 때 주의하기 바랍니다.

4부. 컴포넌트 원칙

12장. 컴포넌트 (발전 과정)

  • 프로그래밍의 초기 시절, 라이브러리 함수를 사용하기 위해서는 코드에 라이브러리 소스 코드(!)를 직접 포함시켜 컴파일하는 식으로 사용했다. 즉, 애플리케이션의 소스코드는 사실상 모든 함수의 구현 코드가 있는 소스코드 덩어리였다고 해도 과언이 아니다. 실제로 초기의 라이브러리 함수들은 소스 코드로 유지되고 배포되었다.
  • 그러나 라이브러리 함수의 규모가 커지면 커질수록 컴파일하기 위한 자원이 크게 소모되었다. 소스코드의 규모가 불어났던 것이다. 이를 해결하기 위해 라이브러리의 함수들을 미리 컴파일하여 바이너리 코드로 만들고, 그 함수들의 메모리 상의 위치를 미리 특정 위치에 로드시켰다. 프로그래머들은 미리 고정된 위치의 주소(포인터)가 기록된 테이블을 사용하는 식으로 애플리케이션이 라이브러리 함수를 호출하도록 코드를 짰다.
  • 그러나 이러한 방식은 시간이 흐를수록 메모리 효율의 저하를 가져왔다. 애플리케이션과 라이브러리의 규모가 커질수록 특정 위치에 코드를 미리 고정하여 로드하는 방식은 자칫 애플리케이션과 라이브러리가 확장되는 과정에서 서로의 주소 범위를 침범할 수 있었던 것이다. (쉽게 말해 시작 메모리 주소를 하드코딩한 결과)
  • 이를 해결하기 위해 로더라는 기능을 만들었다. 발상은 단순하다. 코드를 메모리에 로드할 때, 고정된 주소가 아니라 로더가 그때그때 위치를 새롭게 배치할 수 있도록 만든 것이다. 컴파일러가 소스코드를 컴파일할 때, 최종 완성된 바이너리 코드 자체를 고정된 특정 메모리 주소에서 시작하도록 컴파일하지 않는다. 로더(loader)가 나중에 해당 바이너리 파일을 자유롭게 재배치(relocatable)할 수 있도록 만들었으며, 또한 로더의 이런 동작을 수용하는 형태의 바이너리로 소스코드를 컴파일할 수 있도록 컴파일러(프로그램)가 변경되었다.
    • 재배치의 원리는 다음과 같다. 컴파일된 각 바이너리 파일에는 플래그(flag)라는 것이 삽입되었다. 로더가 바이너리 파일을 메모리에 적재할 때, 적재할 위치(메모리 주소) 값으로 플래그를 수정하면, 해당 바이너리 파일은 그 메모리 위치에서 시작하는 바이너리 파일이 되는 것이다.
    • 그렇다면 각 바이너리 파일들의 내부에서 다른 바이너리 파일의 함수를 호출할 때는 어떻게 되는 것일까? 이전에는 호출 위치가 고정되었기 때문에 그 위치를 가리키는 식으로 처리되면 간단했으나, 재배치 가능한 코드들은 그때그때 메모리 주소가 달라지므로 이런 방식은 적절하지 않을 것이다.
    • 컴파일러는 소스코드를 바이너리로 번역할 때, 바이너리 안의 함수 이름에 대한 메타데이터를 생성하도록 수정되었다. 코드가 외부 함수(라이브러리)를 호출한다면 외부 참조로, 코드가 외부에 어떤 함수를 정의하고 제공해준다면 그 함수는 외부 정의로 분류한다. 이렇게 하면 나중에 컴파일된 바이너리 파일들을 로더가 읽어들여서 플래그를 지정하고 메모리에 적재할 때, 이 메타데이터를 참조하여 각 바이너리 코드들이 서로를 참조할 수 있도록 (참조하는 부분의 데이터를 수정하는 식) 만든 것이다.
  • 이것을 링킹 로더라고 부른다. 링킹 로더는 여러 바이너리 파일들을 입력받아 시작 메모리 위치를 지정하고, 각 파일들이 설정된 메모리 주소에 따라 서로를 참조할 수 있도록 링크(link)했다.
  • 그런데 라이브러리와 애플리케이션의 규모가 커지자, 링킹 로더의 효율도 저하되었다. 이를 해결하기 위해 가장 느린 부분인 링크와 가장 빠른 영역인 로드를 분할했다. 링커는 컴파일된 바이너리(c언어에서 .o 파일) 파일들의 외부 참조와 정의를 활용해 하나의 실행 파일로 연결하는 역할을 했다. 그러나 아직 로드될 메모리 주소 위치까지 지정하진 않는다. 나중에 로더 프로그램이 그 실행 파일의 메모리 위치를 지정해주면 된다. 물론 이렇게 분리해도 컴파일과 링크 과정은 로더에 비하면 매우 무겁고 버거운 과정이었다. 하지만 이후 하드웨어의 폭발적인 발전 덕에 활용 가능한 수준까지 그 비용이 내려갔다.
  • 이런 과정을 거쳐 결국 오늘날의 프로그램들은 여러 단위의 컴포넌트의 결합으로 구성될 수 있게 되었다. 이제 구시대적인 방식으로 모든 소스 코드를 편집해서 하나의 파일로 만들어 컴파일하지 않는다. 링크와 로드라는 소프트웨어적 혁신으로 인해 컴포넌트 단위 분할이 가능해졌으며, 이후 하드웨어의 발전으로 링크 시간이 극도로 축소되어 사실상 링크와 로드를 거의 동시에 처리한다. 이제 일부 컴포넌트를 변경하고 싶다면 마치 폴더에 데이터를 복사 붙여넣는 방식으로 수정해도 되는 시대가 되었다.

 

2. 컴포넌트 응집도

2-1. REP : 재사용/릴리스 등가 원칙

  • 컴포넌트의 적절한 단위는 릴리스를 통해 관리되는 단위와 같다는 원칙이다. MAVEN이나 NPM 등의 모듈 관리 도구를 통해 이미 현대 프로그래머들은 이 혜택을 직감적으로 인식하고 있다.
  • 재사용 가능하고 함께 릴리스 되는 컴포넌트 내부의 클래스와 모듈들은 항상 같이 움직여야 한다. 컴포넌트 내부의 어떤 하나의 모듈을 사소하게나마 바꾸어도 릴리스 버전과 문서를 통해 컴포넌트가 통째로 관리되어야 한다.
  • REP는 컴포넌트가 여러 다른 컴포넌트들에게 재사용될 수 있도록 중요한 테마나 목적이 있어야 함을 말해준다. 달리 말하면, REP는 재사용 가능한 단위로 여러 모듈이나 클래스, 컴포넌트를 그루핑해야 한다는 것을 말한다. 그리고 외부에는 해당 컴포넌트를 사용하는 방식, 수정되었을 때 상세 내역에 대한 문서 등을 제공해야 한다. 
  • REP는 컴포넌트 개발의 독립성을 보장해준다. 

2-2. CCP : 공통 폐쇄 원칙

  • 동일한 이유로 동일한 시점에 변경되는 클래스들은 하나의 컴포넌트 아래에 있어야 한다. 
  • 이 원칙은 사실 SRP와 동일하다.
  • 즉, 컴포넌트가 충분한 규모를 확보하지 못하고 너무 잘게 쪼개버리면 CCP를 위반하게 되는데, 하나의 컴포넌트 변경으로도 그것에 의존하는 다른 컴포넌트 또한 수정의 압박을 받기 때문이다. 밀접하게 연관된 클래스들은 하나의 컴포넌트 아래에 묶어서 한 묶음으로 관리하는 것이 좋다.

2-3. CRP: 공통 재사용 원칙

  • 같이 사용되는 경향이 있는 클래스들은 같은 컴포넌트 아래에 있어야 한다. (=관련 없거나 관련성이 약한 클래스는 포함하지 말라!)
  • 사실 그 어떤 클래스도 단독으로 사용되는 경우는 잘 없다. (만약 그렇다면 해당 클래스가 SRP 원칙을 제대로 지키고 있는 건지 평가해봐야 한다.) 따라서 컴포넌트 내부에서는 클래스들 사이에 많은 의존성이 복잡하게 얽혀 있기 마련이다.
  • CRP는 컴포넌트의 크기를 너무 높게 가져가서는 안 된다는 원리를 말해준다. 서로 밀접하게 응집성이 높은 클래스들이 모인 컴포넌트로 만들어야 한다. 이렇게 하지 않으면 컴포넌트 내부에 내용적으로나 기능적으로나 그다지 관련 없는 클래스도 함께 묶인다. 따라서 이 클래스가 변경되면 컴포넌트 전체를 변경해야 한다. 그리고 이는 이 컴포넌트에 의존하는 다른 컴포넌트 또한 재컴파일, 재검증, 재배포를 고려해야 할지도 모른다. 컴포넌트가 관심 없는, 그다지 관련 없는 클래스의 변경 때문에 이 모든 비용이 생긴다는 뜻이다!!
  • 따라서 CRP는 클래스를 컴포넌트로부터 탈락시키는 원칙에 가깝다. 응집성이란 기준이 명확하지 않기 때문에 상황에 따라 컴포넌트에 포함될 수도, 아닐 수도 있다. 

2-4. ADP : 의존성 비순환 원칙

  • 개발 일을 하면서 분명히 어떤 동작을 수행하는 코드를 적절하게 만들었는데도 나중에 해당 기능이 먹통이 되거나 버그를 일으키는 경우가 있다. 십중팔구 당신이 의존하고 있던 코드를 누군가가 수정했기 때문이다.
  • 이에 대한 가장 멋진 대응 방법이 바로 컴포넌트를 재사용가능한 단위로 분할(REP)하는 것이다. 전체 시스템이 컴포넌트로 분할이 되기만 하면, 일단 개발팀 각각은 자신들이 책임지고 있는 해당 컴포넌트를 개발하는 것에 집중하면 된다. 개발팀은 컴포넌트를 수정해가며 발전시키고 릴리스한다. 그 컴포넌트를 사용(의존)하는 다른 개발팀은 여러 릴리스 버전을 가지고 어떤 버전을 사용할지 그들이 직접 결정한다. 개발이 한 묶음으로 진행되는 것이 아니라 독립적으로 개발되는 것이다. 굳이 어느날 개발자들이 한데 모여 컴포넌트를 통합하는 작업을 할 필요가 사라진다.
  • 이것이 성공하려면 반드시 의존성 방향이 관리되어야 한다. 의존되는 코드의 변경으로 인한 의존하는 코드의 변경을 막을 수는 없다. 그러나 의존에 순환이 발생하면 관리조차 되지 않는다!