Skip to main content

5. 아키텍처

15장. 아키텍처란?#

  • 소프트웨어 아키텍처는 코드와 동떨어져서는 안됩니다.
  • 소프트웨어 시스템의 아키텍처란 시스템을 구축했던 사람들이 만들어낸 시스템의 형태입니다.
    • 그 모양은 시스템을 컴포넌트로 분할하는 방법, 분할된 컴포넌트를 배치하는 방법, 컴포넌트가 서로 의사소통하는 방식에 따라 정해집니다.
  • 시스템의 형태는 아키텍처 안에 담긴 소프트웨어 시스템이 쉽개 개발, 배포, 운영, 유지보수되도록 만들어집니다.

이러한 일을 용이하게 만들기 위해서는 가능한 많은 선택지를, 가능한 오래 남겨두는 전략을 따라야 합니다.

  • 시스템 아키텍처는 시스템의 동작 여부와 거의 상관이 없습니다.
    • 아무런 역할을 하는 것은 또 아닙니다. 이 역할은 수동적이며 피상적인 것이지, 능동적이거나 본질적인 것은 아닙니다.
  • 아키텍처의 주된 목적은 시스템의 생명주기를 지원하는 것입니다.
    • 좋은 아키텍처는 시스템을 쉽게 이해하고, 쉽게 개발하며, 쉽게 유지보수하고, 또 쉽게 배포하게 해줍니다.
    • 아키텍처의 궁극적인 목표는 시스템의 수명과 관련된 비용은 최소화하고, 프로그래머의 생산성을 최대화하는 데 있습니다.

개발#

  • 개발하기 힘든 시스템이라면 수명이 길지도 않고 건강하지도 않을 것입니다. 따라서 시스템 아키텍처는 개발팀(들)이 시스템을 쉽게 개발할 수 있도록 뒷 받침해야만 합니다.
  • 개발팀이 여러개로 될 수록, 시스템을 신뢰할 수 있고 안정된 인터페이스를 갖춘, 잘 설계된 컴포넌트 단위로 분리하지 않으면 개발이 진척되지 않습니다. 이 경우, 시스템의 아키텍처는 팀의 갯수만큼의 컴포넌트로 발전될 가능성이 높습니다.
  • 위의 '팀별 단일 컴포넌트' 아키텍처가 시스템을 배포, 운영, 유지보수하는 데 최적일 가능성은 거의 없습니다.
    • 일정에 쫓겨 일한다면 이 아키텍처로 귀착될 예정입니다.

배포#

  • 소프트웨어 시스템이 사용될 수 있으려면 반드시 배포할 수 있어야 합니다.
    • 배포 비용이 높을수록 시스템의 유용성은 떨어집니다.
    • 소프트웨어 아키텍처는 시스템을 단 한번에 싑게 배포할 수 있도록 만드는 데 그 목표를 두어야 합니다.
  • 초기 개발 단계에서는 배포 전략을 거의 고려하지 않기 때문에, 개발하기는 쉬울지 몰라도 배포하기는 어려운 아키텍처가 만들어집니다.
  • 아키텍처가 배포 문제를 초기에 고려했다면, 다른 결정을 내릴 것입니다.

운영#

  • 아키텍처가 시스템 운영에 미치는 영향은 개발, 배포, 유지보수에 미치는 영향보다는 덜 극적입니다.
    • 운영에서 대다수의 어려움은 하드웨어를 더 투입해서 해결할 수 있습니다.
  • 소프트웨어 아키텍처가 비효율적이라면 단순히 스토리지와 서버를 추가하는 것만으로 제대로 동작하도록 만들 수 있을 대가 많습니다.
  • 시스템을 쉽게 운영하게 해주는 아키텍처가 바람직하지 않다는 말이 아니며 이러한 아키텍처는 바람직합니다. 다만 비용 공식 관점에서 운영보다는 개발, 배포, 유지보수 쪽으로 더 기웁니다.
  • 좋은 소프트웨어 아키텍처는 시스템을 우녕하는 데 필요한 요구도 알려줍니다.
  • 시스템 아키텍처는 유스케이스, 기능, 시스템의 필수 행위를 일급(first-class) 엔티티로 격상시키고, 이들 요소가 개발자에게 주요 목표로 인식되도록 해야 합니다.
    • 시스템을 이해하기 쉬워지며, 개발과 유지보수에 큰 도움이 됩니다.

유지보수#

  • 모든 측면에서 봤을 때 소프트웨어 시스테멩서 비용이 가장 많이 발생합니다.
    • 새로운 기능과, 뛰따라 발생하는 결함, 결함을 수정하는 인적 자원 소모 등이 있습니다.
  • 유지보수의 가장 큰 비용은 탐사(spelunking) 와 이로 인한 위험부담이 있습니다.
    • 기존 소프트웨어에 새로운 기능을 추가하거나 결함을 수정할 대, 소프트웨어를 파헤쳐서 어디를 고치는 게 최선인지, 그리고 어떤 전략을 쓰는게 최적일지를 경정할 때 드는 비용입니다.
    • 이러한 변경사항을 반영할 때 의도치 않은 결함이 발생할 가능성은 항상 존재하며, 이로 인한 위험부담 비용이 추가됩니다.
  • 주의를 기울여 신중하게 아키텍처를 만들면 이 비용을 크게 줄일 수 있습니다.
    • 시스템을 컴포넌트로 분리하고, 안정된 인터페이스를 두어 서로 격리합니다.

선택사항 열어 두기#

  • 소프트웨어는 행위적 가치와 구조적 가치를 지닙니다. 이중에서 두 번째 가치가 더 중요한데, 소프트웨어를 부드럽게(soft) 만드는 것은 바로 구조적 가치입니다.
  • 소프트웨어를 만든 이유는 기계의 행위를 빠르고 쉽게 변경하는 방법이 필요하기 때문입니다.
    • 이러한 유연성은 시스템의 형태, 컴포넌트의 배치 방식, 컴포넌트가 상호 연결되는 방식에 상당히 크게 의존합니다.
  • 소프트웨어를 부드럽게 유지하는 방법은 서택사항을 가능한 많이, 그리고 가능한 오랫동안 열어 두는 것입니다.
    • 열어 둬야 할 선택사항은 중요하지 않은 세부사항을 의미합니다.
  • 소프트웨어 시스템은 주요한 두 가지 구성요소로 분해할 수 있습니다. 즉, 정책과 세부사항입니다.
    • 정책 요소는 모든 업무 규칙과 업무 절차를 구체화합니다. 즉, 시스템의 진정한 가치가 살아있습니다.
    • 세부사항은 사람, 외부 시스템, 프로그래머가 정책과 소통할 때 필요한 요소이며, 정책이 가진 행위에는 조금도 영향을 미치지 않습니다. 예시로 입출력 장치, 데이터베이스, 웹 시스템, 서버, 프레임워크, 통신 프로토콜 등이 있습니다.
  • 아키텍트의 목표는 시스템에서 정책을 가장 핵심적인 요소로 식별하고, 동시에 세부사항은 정책에 무관하게 만들 수 있는 형태의 시스템을 구축하는 데 있습니다. 이를 통해 세부사항을 결정하는 일은 미루거나 연기할 수 있게 됩니다.
    • 개발 초기에는 데이터베이스 시스템을 선택할 필요가 없습니다.
    • 개발 초기에는 웹 서버를 선택할 필요가 없습니다.
      • 시스템을 웹을 통해 전송할 것인지조차도 결정할 필요가 없습니다.
    • 개발 초기에는 REST를 적용할 필요가 없습니다.
    • 개발 초기에는 의존성 주입(DI) 프레임워크를 적용할 필요가 없습니다.
      • 고수준의 정책은 의존성을 해석하는 방식에 대해 신경 써서는 안됩니다.
  • 세부사항에 몰두하지 않은 채 고수준의 정책을 만들 수 있다면, 이러한 세부사항에 대한 결정을 오랫동안 미루거나 연기할 수 있습니다.
    • 이러한 결정을 더 오래 참을 수 있다면, 더 많은 정보를 얻을 수 있고, 이를 기초로 제대로 된 결정을 내릴 수 있습니다.
  • 선택사항을 더 오랫동안 열어 둘 수 있다면 더 많은 실험을 해볼 수 있고 더 많은 것을 시도할 수 있습니다.
  • 뛰어난 아키텍트라면 이러한 결정이 아직 내려지지 않은 것처럼 행동하며, 여전히 결정을 가능한 오랫동안 연기하거나 변경할 수 있는 형태로 시스템을 만듭니다.

좋은 아키텍트는 결정되지 않은 사항의 수를 최대화합니다.

장치 독립성#

  • 코드를 입출력 장치와 직접 결합한 것은 가장 큰 실수 중 하나입니다.
    • 이러한 코드는 장치 종속적(device dependent) 입니다.
  • 1960년대에 후반에 이르어서야 장치 독립성(device independent) 을 생각했습니다.
  • 이제는 동일한 프로그램을 아무런 변경 없이도 진행할 수 있습니다.
    • 개방 폐쇄 원칙이 이로써 탄생했습니다.

광고 우편#

  • 장치 독립성은 어떤 장치를 사용할지 전혀 모르더라도, 프로그램을 작성할 수 있습니다.

물리적 주소 할당#

  • 시스템에서 고수준의 정책이 디스크의 물리적 구조로부터 독립되는 것 처럼, 일종의 결정사항을 애플리케이션에서 분리하는 것은 중요합니다.

결론#

  • 좋은 아키텍트는 세부사항을 정책으로부터 신중하게 가려내고, 정책이 세부사항과 결합되지 않도록 엄격하게 분리합니다.
  • 정책은 세부사항에 관한 어떠한 지식도 갖지 못하게 되며, 어떤 경우에도 세부사항에 의존하지 않게 됩니다.
  • 좋은 아키텍트는 세부사항에 대한 결정을 가능한 오랫동안 미룰 수 있는 방향으로 정책을 설계합니다.

16장. 독립성#

좋은 아키텍처는 다음을 지원해야 합니다.

  • 시스템의 유스케이스
  • 시스템의 운영
  • 시스템의 개발
  • 시스템의 배포

유스케이스#

  • 유스케이스의 경우, 시스템의 아키텍처는 시스템의 의도를 지원해야 한다는 뜻입니다.
  • 좋은 아키텍처가 행위를 지원하기 위해 할 수 있는 일 중에서 가장 중요한 사항은 행위를 명확히 하고 외부로 드러내며, 이를 통해 시스템이 지닌 의도를 아키텍처 수준에서 알아볼 수 있게 만드는 것입니다.
  • 애플리케이션이 좋은 아키텍처를 갖춘다면, 해당 시스템의 유스케이스는 시스템 구조 자체에서 한눈에 드러날 것입니다.
    • 이들 요소는 클래스이거나 함수 또는 모듈로서 아키텍처 내에서 핵심적인 자리를 차지할 뿐만 아니라, 자신의 기능을 분명하게 설명하는 이름을 가집니다.

운영#

  • 시스템의 운영 지원 관점에서 볼 때 아키텍처는 더 실질적이며 덜 피상적인 역할을 맡습니다.
  • 형태를 결정하는 것은 뛰어난 아키텍트라면 열어 두어야 하는 선택사항 중 하나입니다.
    • 만약 시스템이 monolith으로 작성되어 모노리틱 구조를 갖는다면, 다중 프로세스, 다중 스레드, 또는 마이크로서비스 형태가 필요해질 때 개선하기가 어렵습니다.
    • 아키텍처에서 각 컴포넌트를 적절히 격리하여 유지하고 컴포넌트 간 통신 방식을 특정 형태로 제한하지 않는하지 않는다면, 시간이 지나 운영에 필요한 요구사항이 바뀌더라도 스레드, 프로세스, 서비스로 구성된 기술 스펙트럼 사이를 전환하는 일이 훨씬 쉬워집니다.

개발#

  • 아키텍처는 개발환경을 지원하는 데 있어 핵심적인 역할을 수행합니다.
    • 콘웨이(Conway)의 법칙이 이 때 작용합니다.

콘웨이 법칙. 시스템을 설계하는 조직이라면 어디든지 그 조직의 의사소통 구조와 동일한 구조의 설계를 만들어 낼 것입니다.

  • 많은 팀으로 구성되며 관심사가 다양한 조직에서 어떤 시스템을 개발해야 한다면, 각 팀이 독립적으로 행동하기 편한 아키텍처를 반드시 확보하여 팀들이 서로를 방해하지 않도록 해야 합니다.
  • 아키텍처를 만들려면 잘 격리되어 독립적으로 개발 가능한 컴포넌트 단위로 시스템을 분할할 수 있어야 합니다.

배포#

  • 아키텍처는 배포 용이성을 결정하는 데 중요한 역할을 합니다.
    • 목표는 '즉각적인 배포(immediate deployment)' 입니다.
  • 좋은 아키텍처는 꼭 필요한 디렉터리나 파일을 수작업으로 생성하게 내버려 두지 않습니다.
  • 좋은 아키텍처라면 시스템이 빌드된 후 즉각 배포할 수 있도록 지원해야 합니다.
  • 아키텍처를 만들려면 시스템을 컴포넌트 단위로 적절하게 분할하고 격리시켜야 합니다.
    • 마스터 컴포넌트는 시스템 전체를 하나로 묶고, 각 컴포넌트를 올바르게 구동하고 통합하고 관리해야 합니다.

선택사항 열어놓기#

  • 좋은 아키텍처는 컴포넌트 구조와 관련된 이 관심사들 사이에서 균형을 맞추고, 각 관심사 모두를 만족시킵니다.
  • 현실에서는 이러한 균형을 잡기가 어렵습니다.
  • 몇몇 아키텍처원칙은 구현하는 비용이 비교적 비싸지 않으며, 관심사들 사이에서 균형을 잡는데 도움이 됩니다.
  • 좋은 아키텍처는 선택사항을 열어 둠으로써, 향후 시스템에 변경이 필요할 때 어떤 방향으로든 쉽게 변경할 수 있도록 합니다.

계층 결합 분리#

  • 뛰어난 아키텍트는 유스케이스에서 UI 부분과 업무 규칙 부분을 서로 분리하고자 합니다.
  • 업무 규칙은 서로 분리하고, 독립적으로 변경할 수 있도록 만들어야 합니다.

유스케이스 결합 분리#

  • 각 유스케이스는 UI의 일부, 애플리케이션 특화 업무 규칙의 일부, 애플리케이션 독립적 업무 규칙의 일부, 그리고 데이터베이스 기능의 일부르 사용합니다.
    • 즉, 시스템을 수평적 계층으로 분할하면서 동시에 해당 계층을 가로지르는 얇은 수직적인 유스케이스로 시스템을 분할할 수 있습니다.
  • 시스템에서 다른 이류로 변경되는 요소들의 결합을 분리하면 기존 요소에 지장을 주지 않고도 새로운 유스케이스를 계속해서 추가할 수 있게됩니다.

결합 분리 모드#

결합을 분리한 경우

  • 위의 형태처럼, 분리된 컴포넌트는 반드시 독립된 서비스가 되어야 하고, 일종의 네트워크를 통해 서로 통신해야 합니다.
  • 많은 아키텍트가 이러한 컴포넌트를 '서비스' 혹은 '마이크로서비스'라고 하며, 실제로 서비스에 기반한 아키텍처를 흔히들 서비스 지향 아키텍처(service-oriented architecture)라고 부릅니다.
  • 좋은 아키텍처는 선택권을 열어 둔다는 사실이며, 결합 분리 모드는 이러한 선잭지 중 하나입니다.

개발 독립성#

  • 컴포넌트가 완전히 분리되면 팀 사이의 간섭이 줄어듭니다.
  • 기능 팀, 컴포넌트 팀, 계층 팀, 혹은 또 다른 형태의 팀이라도, 계층과 유스케이스의 결합이 분리되는 한 시스템의 아키텍처는 그 팀 구조를 뒷받침해줄 것입니다.

배포 독립성#

  • 유스케이스와 계층의 결합이 분리되면 배포 측면에서도 고도의 유연성이 생깁니다.
  • 실제로 결합을 제대로 분리했다면 운영 중인 시스템에서도 계층과 유스케이스를 교체할 수 있습니다.

중복#

  • 소프트웨어에서 중복은 일반적으로 나쁩니다.
  • 우발적 중복을 피하는 것이 중요합니다.
    • 뷰가 비슷하다고, 중복을 하는 것은 우발적입니다.
    • 뷰 모델을 별도로 만드는 일은 그다지 많은 노력이 들지 않습니다.

결합 분리 모드#

  • 계층과 유스케이스의 결합을 분리하는 방법을 다양합니다.
    • 소스 수준 분리 모드
      • 소스 코드 모듈 사이의 의존성을 제어할 수 있습니다.
      • 하나의 모듈이 변하더라도 다른 모듈을 변경하거나 재컴파일하지 않도록 만들 수 있습니다.
    • 배포 수준 분리 모드
      • jar 파일, DLL, 공유 라이브러리와 같이 배포 가능한 단위들 사이의 의존성을 제어할 수 있습니다.
      • 한 모듈의 소스 코드가 변하더라도 다른 모듈을 재빌드하거나 재배포하지 않도록 만들 수 있습니다.
    • 서비스 수준 분리 모드
      • 의존하는 수준을 데이터 구조 단위까지 낮출 수 있고, 순전히 네트워크 패킷을 통해서만 통신하도록 만들 수 있습니다.
      • 모든 실행 가능한 단위는 소스와 바이너리 변경에 대해 서로 완전히 독립적이게 됩니다.
  • 현재에서 한 가지 해결책은 단순히 서비스 수준에서 에서의 분리를 기본 정책으로 삼는 것입니다.
    • 이 방식은 비용이 많이 들고, 결합이 큰 단위에서 분리된다는 문제가 있습니다.
  • 서비스 수준의 결함 분리의 문제는 개발 시간 측면과 시스템 자원 측면에서도 비용이 많이든다는 사실입니다.

좋은 아키텍처는 시스템이 모노리틱 구조로 태어나서 단일 파일로 배포되더라도, 이후에는 독립적으로 배포 가능한 단위들의 집합으로 성장하고, 또 독립적인 서비스나 마이크로서비스 수준까지 성장할 수 있도록 만들어져야합니다.

좋은 아키텍처는 이러한 변경으로부터 소스 코드 대부분을 보호합니다.

결혼#

시스템의 결합 분리 모드는 시간이 지나면서 바뀌기 쉬우며, 뛰어난 아키텍트라면 이러한 변경을 예측하여 큰 무리 없이 반영할 수 있도록 만들어야 합니다.


17장. 경계: 선 긋기#

  • 소프트웨어 아키텍처는 선을 긋는 기술이며, 이러한 선을 경계(boundary) 라 부를 수 있습니다.
    • 경계는 소프트웨어 요소를 서로 분리하고, 경계 한편에 있는 요소가 반대편에 있는 요소를 알지 못하게 막습니다.
  • 초기에 그어지는 선들은 가능한 한 오랫동안 결정을 연기시키기 위해, 결정이 핵심적이 업무 로직을 오염시키지 못하게 만들려는 목적으로 쓰입니다.
  • 아키텍트의 목표는 필요한 시스템을 만들고 유지하는 데 드는 인적 자원을 최소화하는 것입니다.
    • 인적 자원의 효율성을 떨어트리는 요인은 바로 결합(coupling) 입니다. (이른 결정)
  • 이른 결정이란 시스템의 업무 요구사항와 관련이 없는 결정입니다.
  • 좋은 시스템 아키텍처는 이러한 결정에 의존하지 않습니다.
    • 결정에 따른 영향이 크지 않게 만듭니다.

두 가지 슬픈 이야기#

  • 너무 빠른 결정으로 인해, 개발 비용의 증가와 종속성이 생겼습니다.

FitNesse#

  • 반대로 경계선을 그음으로서 결정을 늦추고 연기하는데 도움이 되며 더 나아가 시간을 엄청나게 절약해주었으며 골치를 썪지 않게 해주었습니다.

어떻게 선을 그을까? 그리고 언제 그을까?#

  • 관련이 있는 것과 없는 것 사이에 선을 긋습니다.
    • GUI와 업무 규칙은 관련이 없으므로 선이 있어야합니다.
    • 데이터베이스와 GUI는 관련이 없으므로 선이 있어야합니다.
    • 데이터베이스는 업무 규칙과 관련이 없으므로, 둘 사이에 선이 있어야합니다.
  • 데이터베이스는 업무 규칙이 간접적으로 사용될 수 있는 도구입니다.

인터페이스 뒤로 숨은 데이터베이스

  • 업무 규칙은 전혀 개의치않으며, 이 같은 사실은 데이터베이스에 대한 결정을 연기할 수 있고 데이터베이스를 결정하기에 앞서 업무 규칙을 먼저 작성하고 테스트하는 데 집중할 수 있습니다.

입력과 출력은?#

  • 개발자와 고객은 종종 시스템이 무엇인지에 대해 혼란스러워 합니다.
    • GUI는 시스템이 아닙니다.
    • 그러나 중요한 원칙은 입력과 출력은 중요하지 않습니다.
  • 중요한 것은 업무 규칙입니다.

플러그인 아키텍처#

  • 데이터베이스와 GUI에 대해 내린 두 가지 결정을 하나로 합쳐서 보면 컴포넌트 추가와 관련된 일종의 패턴이 만들어집니다.
  • 소프트웨어 개발 기술의 역사는 플러그인을 손쉽게 생성하여, 확장 가능하며 유지보수가 쉬운 시스템 아키텍처를 확립할 수 있게 만드는 방법에 대한 이야기입니다.
  • 플러그인 구조를 가정함 채 시작함으로써, 변경 작업이 현실적이게 되었습니다.

플러그인에 대한 논의#

  • 의존성 구조를 통해 어떤 팀이 다른 팀을 위험하게 하는지 확인할 수 있습니다.
  • 특정 모듈이 나머지 모듈에 영향받지 않도록 하는 것이 중요합니다.
  • 시스템을 플러그인 아키텍처로 배치함으로써 변경이 전파될 수 없는 방화벽을 생썽할 수 있습니다.
  • 경계는 변경의 축(axios of change) 가 있는 지점에 그어집니다.
  • 이는 단일 책임 원칙에 해당합니다. 단일 책임 원칙은 어디에 경계를 그어야 할지를 알려줍니다.

결론#

  • 소프트웨어 아키텍처에서 경계선을 그리려면 먼저 시스템을 컴포넌트 단위로 분할해야 합니다.
    • 일부 컴포넌트는 핵심 업무 규칙에 해당합니다.
    • 컴포넌트는 플러그인으로, 핵심 업무와는 직접적인 관련이 없지만 필수 기능을 포함해야 합니다.
    • 컴포넌트 사이의 화살표가 특정 방향, 즉 핵심 업무를 향하도록 이들 컴포넌트의 소스를 배치합니다.
  • 위는 의존성 역전 원칙과 안정된 추상화 원칙을 의존한 것입니다.
    • 의존성 화살표는 저수준 세부사항에서 고수준의 추상화를 향하도록 배치됩니다.

18장. 경계 해부학#

시스템 아키텍처는 일련의 소프트웨어 컴포넌트와 그 컴포넌트들을 분리하는 경계에 의해 정의됩니다.

경계 횡단하기#

  • '런타임에 경계를 횡단한다' 함은 경계 한쪽에 있는 기능에서 반대편 기능을 호출하여 데이터를 전달하는 일에 불과합니다.
  • 적절한 위치에서 경계를 횡단하게 하는 비결은 소스 코드 의존성 관리에 있습니다.
  • 경계는 이러한 변경이 전파되는 것을 막는 방화벽을 구축하고 관리하는 수단으로써 존재합니다.

두려운 단일체#

  • 아키텍처 경계 중 가장 단순하며 가장 흔한 형태는 물리적으로 엄격하게 구분되지 않는 형태입니다.
  • 함수와 데이터가 단일 프로세스에서 같은 주소 공간을 공유하며 그저 나름의 규칙에 따라 분리되어 있을 뿐입니다.
  • 배포 관점에서 보면 이는 소위 단일체(monolith)라고 불리는 단일 실행 파일에 지나지 않습니다.
  • 배포 관점에서 볼 때 단일체는 경계가 드러나지 않습니다.
  • 아키텍처는 거의 모든 경우에 특정한 동적 다형성에 의존하여 내부 의존성을 관리합니다.
    • 객체 지향 개발이 중요한 패러다임이 되었습니다.
  • 가장 단순한 형태의 경계 횡단은 저수준 클라이언트에서 고수준 서비스로 향하는 함수 호출입니다.

제어흐름의 방향

  • 고수준 클라이언트가 저수준 서비스를 호출해야 한다면 동적 다형성을 사용하여 제어흐름과는 반대 방향으로 의존성을 역전시킬 수 있습니다.
    • 이 때는 런타임 의존성은 컴파일타임 의존성과는 반대가 됩니다.

제어흐름과 반대로 경계를 횡단

  • 정적 링크된 모노리틱 구조의 실행 파일이라도 이처럼 규칙적인 방식으로 구조를 분리하면 프로젝트를 개발, 테스트, 배포하는 작업에 큰 도움이 됩니다.
    • 팀들은 서로의 영역에 침범하지 않은 채 자신만의 컴포넌트를 독립적으로 작업할 수 있습니다.
    • 고수준 컴포넌트는 저수준 세부사항으로부터 독립적으로 유지됩니다.
  • 단일체에서 컴포넌트 간 통신은 매우 빠르고 값쌉니다.

배포형 컴포넌트#

  • 아키텍처의 경계가 물리적으로 드러나는 단순한 형태는 동적 링크 라이브러리입니다.
  • 단일체와 마찬가지로 배포형 컴포넌트의 경계를 가로지르는 통신은 순전히 않으므로 매우 값쌉니다.
    • 동적 링크와 런타임 로딩으로 인해 최초의 함수 호출은 오래 걸릴 수 있지만, 대체로 이들 경계를 가로지르는 통신은 매우 빈번합니다.

스레드#

  • 단일체와 배포형 컴포넌트는 모두 스레드를 활용할 수 있습니다.
  • 스레드는 아키텍처 경계도 아니며 배포 단위도 아닙니다.
  • 스레드는 실행 계획과 순서를 체계화하는 방법에 가깝습니다.
  • 모든 스레드가 단 하나의 컴포넌트에 포함될 수도 있고, 많은 컴포넌트에 걸쳐 분산될 수도 있습니다.

로컬프로세스#

  • 훨씬 강한 물리적 형태를 띠는 아키텍처 경계로는 로컬 프로세스가 있습니다.
    • 일반적으로는 메모리 보호를 통해 프로세스들이 메모리를 공유하지 못하게 합니다.
  • 일반적인 경우, 로컬 프로세스는 소켓이나 메일박스, 메시지 큐와 같이 운영체에제서 제공하는 통신 기능을 이용하여 서로 통신합니다.
  • 로컬 프로세스는 정적으로 링크된 단일체이거나 동적으로 링크된 여러개의 컴포넌트로 구성될 수 있습니다.
  • 로컬 프로세스는 컴포넌트 간 의존성을 동적 다형성을 통해 관리하는 저수준 컴포넌트로 구성됩니다.
  • 로컬 프로세스 또한 항상 고수준 컴포넌트를 향합니다.
  • 로컬 프로세스에서는 통신이 비용이 크므로, 빈번하게 이뤄지지 않도록 신중하게 제한해야 합니다.

서비스#

  • 물리적인 형태를 띠는 가장 강력한 경계는 서비스입니다.
    • 서비스는 프로세스로, 일반적으로 명령행 또는 그와 동등한 시스템 호출을 통해 구동됩니다.
    • 서비스는 자신의 물리적 위치에 구애받지 않습니다. 서비스들은 모든 통신이 네트워크를 통해 이뤄진다고 가정합니다.
  • 서비스 경계를 지나는 통신은 함수 호출에 비해 매우 느립니다.
    • 이러한 통신에서는 지연(latency)에 따른 문제를 고수준에서 처리할 수 있어야 합니다.
  • 위를 제외하고는 로컬 프로세스에 적용한 규칙이 서비스에도 그대로 적용됩니다.

결론#

  • 단일체를 제외한 대다수의 시스템은 한 가지 이상의 경계 전략을 사용합니다.
    • 서비스 경계를 활용하는 시스템이라면 로컬 프로세스 경계도 일부 포함하고 있을 수 있습니다.
    • 개별 서비스 도는 로컬 프로세스는 거의 언제나 소스 코드 컴포넌트로 구성된 단일체거나, 혹은 동적으로 링크된 배포형 컴포넌트의 집합입니다.
  • 대체로 한 시스템 안에서도 통신이 빈번한 로컬 경계와 지연을 중요하게 고려해야 하는 경계가 혼합되어 있음을 의미합니다.

19장. 정책과 수준#

  • 소프트웨어 시스템이란 정책을 기술한 것입니다.
  • 대다수의 주요 시스템에서 하나의 정책은 이 정책을 서술하는 여러 개의 조그만 정책들로 쪼갤 수 있습니다.
  • 소프트웨어 아키텍처를 개발하는 기술에는 이러한 정책을 신중하게 분리하고, 정책이 변경되는 양상에 따라 정책을 재편성하는 일도 포함됩니다.
  • 아키텍처 개발은 재변성된 컴포넌트들을 비순환 방향 그래프로 구성하는 기술을 포함합니다.
    • 이러한 의존성은 소스 코드, 컴파일타임의 의존성입니다.
  • 좋은 아키텍처라면 각 컴포넌트를 연결할 때 의존성의 방향이 컴포넌트의 수준을 기반으로 연결되도록 만들어야 합니다.
    • 즉, 저수준 컴포넌트가 고수준 컴포넌트에 의존하도록 설계되어야 합니다.

수준#

  • '수준(level)'을 엄밀하게 정의하자면 '입력과 출력까지의 거리'입니다.
    • 즉, 시스템의 입력과 출력 모두로부터 멀리 위치할수록 정책의 수준은 높아집니다.

간단한 암호화 프로그램

잘 설계했다면, 소스 코드 의존성은 곧은 점선처럼 표시됩니다.

  • 주목할 점은 데이터 흐름과 소스 코드 의존성이 항상 같은 방향을 가리키지는 않는다는 사실입니다.
  • 소스 코드 의존성은 그 수준에 따라 결합되어야 하며, 데이터 흐름을 기준으로 결합되어서는 안됩니다.

시스템의 더 나은 아키텍처

  • 고수준의 정책을 저수준의 입력/출력 정책으로부터 분리시킨 방식에 주목합니다.
  • 정책을 컴포넌트로 묶는 기준은 정책이 변경되는 방식에 달려있다는 사실입니다.
  • 고수준 정책, 즉 입력과 출력에서부터 멀리 떨어진 정책은 저수준 정책에 비해 덜 빈번하게 변경되고, 보다 중요한 이유로 변경되는 영향이 있습니다.
  • 모든 소스 코드 의존성의 방향이 고수준 정책을 향할 수 있도록 정책을 분리했다면 변경의 영향도를 줄일 수 있습니다.
    • 시스템의 최저 수준에서 중요하지 않지만 긴급한 변경이 발생하더라도, 보다 높은 위치의 중요한 수준에 미치는 영향은 거의 없게 됩니다.
  • 저수준 컴포넌트가 고수준 컴포넌트에 플러그인되어야 한다는 관점으로 볼 수 있습니다.

결론#

  • 정책에 대한 논의는 단일 책임 원칙, 개방 폐쇄 원칙, 공통 폐쇄 원칙, 의존성 역전 원칙, 안정된 추상화 원칙을 모두 포함합니다.

20장. 업무 규칙#

  • 애플리케이션을 업무 규칙과 플러그인으로 구분하려면 업무 규칙이 실제로 무엇인지 잘 이해해야 합니다.
  • 엄밀하게 말하면 업무 규칙은 사업적으로 수익을 얻거나 비용을 줄일 수 있는 규칙 또는 절차입니다.
  • 대표적인 예시로 N%의 이자를 부과한다는 사실은 은행이 돈을 버는 업무 규칙입니다.
    • 이러한 규칙을 핵심 업무 규칙(Critical Business Rule) 이라 부릅니다.
  • 핵심 업무 규칙은 보통 데이터를 요구합니다. -이러한 데이터를 핵심 업무 데이터(Critical Business Data) 라고 부릅니다.
  • 핵심 규칙과 핵심 데이터는 본질적으로 결합되어 있기 때문에 객체로 만들 좋은 후보가 됩니다. 이러한 유형의 객체를 엔티티(Entity) 라고 합니다.

엔티티#

  • 엔티티는 컴퓨터 시스템 내부의 객체로서, 핵심 업무 데이터를 기반으로 동작하는 일련의 조그만 핵심 업무 규칙을 구체화해야 합니다.
    • 엔티티 객체는 핵심 업무 데이터를 직접 포함하거나 핵심 업무 데이터에 매우 쉽게 접근할 수 있습니다.
    • 엔티티의 인터페이스는 핵심 업무 데이터를 기반으로 동작하는 핵심 업무 규칙을 구현한 함수들로 구성됩니다.
  • 클래스를 생성할 때, 업무에서 핵심적인 개념을 구현하는 소프트웨어는 한데 모으고, 구축 중인 자동화 시스템의 나머지 모든 고려사항과 분리시킵니다.
    • 덴티티는 순전히 업무에 대한 것이며, 이외의 것은 없습니다.
  • 엔티티를 만들 때 꼭 객체 지향 언어를 사용할 필요가 없으며 유일한 요구조건은 핵심 업무 데이터와 핵심 업무 규칙을 하나로 묶어서 별도의 소프트웨어 모듈로 만들어야 한다는 것입니다.

유스케이스#

  • 모든 업무 규칙이 엔티티처럼 순수한 것은 아니며, 자동화된 시스템이 동작하는 방법을 정의하고 제약함으로써 수익을 얻거나 비용을 줄이는 업무 규칙도 존재합니다.
  • 유스케이스(use case) 는 자동화된 시스템이 사용되는 방법을 설명합니다.
    • 유스케이스는 애플리케이션에 특화된(application-specific) 업무 규칙을 설명합니다.
  • 인터페이스로 들어오는 데이터와 인터페이스에서 되돌려주는 데이터를 형식 없이 명시한다는 점만 빼면, 유스케이스는 사용자 인터페이스를 기술하지 않습니다.
    • 이는 유스케이스는 시스템이 사용자에게 어떻게 보이는지를 설명하지 않고, 애플리케이션에 특화된 규칙을 설명하며 이를 통해 사용자와 엔티티 사이의 상호작용을 규정합니다.
  • 유스케이스는 객체입니다. 유스케이스는 애플리케이션에 특화된 업무 규칙을 구현하는 하나 이상의 함수를 제공합니다.
  • 엔티티는 자신을 제어하는 유스케이스에 대해 아무것도 알지 못하며, 이는 의존성 역전 원칙을 준수하는 의존성 방향에 대한 또 다른 예시입니다.

엔티티는 고수준이며, 유스케이스가 저수준인 이유는 유스케이스는 단일 애플리케이션에 특화되어 있으며, 따라서 해당 시스템의 입력과 출력에 보다 가깝게 위치하기 때문입니다. 엔티티는 수많은 다양한 애플리케이션에서 사용될 수 있도록 일반화된 것이므로, 각 시스템의 입력이나 출력에서 더 멀리 떨어져 있습니다.

유스케이스는 엔티티에 의존하며 엔티티는 유스케이스에 의존하지 않습니다.

요청 및 응답 모델#

  • 유스케이스는 입력 데이터를 받아서 출력 데이터를 생성합니다.
  • 의존성을 제거하는 일은 매우 중요합니다.
  • 엔티티 객체를 가리키는 참조를 요청 및 응답 데이터 구조에 포함한다면 시간이 지나면 두 객체는 완전히 다른 이유로 경되며, 공통 폐쇄 원칙과 단일 책임 원칙을 위배하게 됩니다. 즉, 수많은 떠돌이 데이터(tramp data)가 만들어지고, 수많은 조건문이 추가됩니다.

결론#

  • 업무 규칙은 소프트웨어 시스템이 존재하는 이유입니다.
    • 업무 규칙은 핵심적인 기능입니다.
  • 업무 규칙은 사용자 인터페이스나 데이터베이스와 같은 저수준의 관심사로 인해 오염되어서는 안되며, 원래 그대로의 모습으로 있어야합니다.

업무 규칙은 시스템에서 가장 독립적이며 가장 많이 재사용할 수 있는 코드여야 합니다.


21장. 소리치는 아키텍처#

아키텍처의 테마#

  • 소프트웨어 아키텍처는 시스템의 유스케이스를 지원하는 구조입니다.
  • 아키텍처는 프레임워크에 대한 것이 아니며 절대로 그래서도 안됩니다.
  • 프레임워크는 사용하는 도구일 뿐, 아키텍처가 준수해야 할 대상이 아닙니다.
  • 아키텍처를 프레임워크 중심으로 만들어버리면 유스케이스가 중심이 되는 아키텍처는 절대 나올 수 없습니다.

아키텍처의 목적#

  • 좋은 소프트웨어 아키텍처는 프레임워크, 데이터베이스, 웹 서버, 그리고 여타 개발 환경 문제나 도구에 대해서는 결정을 미룰 수 있도록 만듭니다.
  • 프레임워크는 열어둬야 할 선택사항입니다.
  • 좋은 아키텍처는 유스케이스에 중점을 두며, 지엽적인 관심사에 대한 결합은 분리시킵니다.

하지만 웹은?#

  • 웹은 아키텍처가 아니며, 전달 메커니즘이며 애플리케이션 아키텍처에서도 그와 같이 다뤄야 합니다.
  • 애플리케이션이 웹을 통해 전달된다는 사실은 세부사항이며, 시스템 구조를 지배해서는 절대 안됩니다.
  • 시스템 아키텍처는 시스템이 어떻게 전달될지에 대해 가능하다면 아무것도 몰라야 합니다.

프레임워크는 도구일 뿐, 삶의 방식은 아니다#

  • 프레임워크는 매우 강력하고 상당히 유용할 수 있습니다.
  • 프레임워크가 아키텍처의 중심을 차지하는 일을 막을 수 있는 전략을 개발하는 것이 중요합니다.

테스트하기 쉬운 아키텍처#

  • 아키텍처가 유스케이스를 최우선으로한다면, 그리고 프레임워크와는 적당한 거리를 둔다면, 프레임워크를 전혀 준비하지 않더라도 필요한 유스케이스 전부에 대해 단위 테스트를 할 수 있어야합니다.
  • 프레이무어크로 인한 어려움을 겪지 않고도 반드시 이 모두를 있는 그대로 테스트할 수 있어야 합니다.

결론#

  • 아키텍처는 시스템을 이야기해야 하며, 시스템에 적용한 프레임워크에 대해 이야기해서는 안됩니다.

22장. 클린 아키텍처#

  • 아키텍처는 모두 시스템이 다음과 같은 특징을 지니도록 만듭니다.
    • 프레임워크 독립성, 아키텍처는 다양한 기능의 라이브러리를 제공하는 소프트웨어, 즉 프레임워크의 존재 여부에 의존하지 않습니다.
    • 테스트 용이성, 업무 규칙은 UI, 데이터베이스, 웹 서버, 또는 여타 외부 요소가 없이도 테스트할 수 있습니다.
    • UI 독립성, 시스템의 나머지 부분을 변경하지 않고도 UI를 쉽게 변경할 수 있습니다.
    • 데이터베이스 독립성, DB 등을 교체할 수 있습니다.
    • 모든 외부 에이전시에 대한 독립성, 실제로 업무 규칙은 외부 세계와의 인터페이스에 대해 전혀 알지 못합니다.

클린 아키텍처

의존성 규칙#

  • 각각의 동심원은 소프으웨어에서 서로 다른 영역을 표현하며, 안으로 들어갈술록 고수준의 소프트웨어가 됩니다.
    • 바깥쪽이 메커니즘이고 안쪽 원은 정책입니다.
  • 이러한 아키텍처가 동작하도록 하는 가장 중요한 규칙은 의존성 규칙(Dependency Rule) 입니다.

소스 코드 의존성은 반드시 안쪽으로, 고수준의 정책을 향해야 합니다.

  • 내부의 원에 속한 요소는 외부의 원에 속한 어떤 것도 알지 못해야 합니다.
  • 외부 원에 위치한 어떤 것도 내부의 원에 영향을 주지 않는 것이 좋습니다.

엔티티#

  • 엔티티는 전사적인 핵심 업무 규칙을 캡슐화합니다.
  • 전사적이지 않은 단순한 단일 애플리케이션을 작성하고 있다면 엔티티는 해당 애플리케이션의 업무 객체가 됩니다.

유스케이스#

  • 유스케이스 계층의 소프트웨어는 애플리케이션에 특화된 업무 규칙을 포함합니다.
  • 이 계층에서 발생한 변경이 엔티티에 영향을 줘서는 안됩니다. 또한 데이터베이스, UI, 또는 여타 공통 프레임워크와 같은 외부 요소에서 발생한 변경이 이 계층에 영향을 줘서는 안됩니다.
  • 운영 관점에서 애플리케이션이 변경된다면 유스케이스가 영향을 받으며, 따라서 이 계층의 소프트웨어에도 영향을 줄 것입니다.

인터페이스 어댑터#

  • 인터페이스 어댑터(Interface Adaptor) 계층은 일련의 어댑터들로 구성됩니다.
  • 어댑터는 데이터를 유스케이스와 엔티티에게 가장 편리한 형식에서 데이터베이스나 웹 같은 외부 에이전시에게 가장 편리한 형식으로 변환합니다.

프레임워크와 드라이버#

  • 가장 바깥측 계층은 일반적으로 데이터베이스나 웹 프레임워크 같은 프레임워크나 도구들로 구성됩니다.
  • 프레임워크와 드라이브 계층은 모든 세부사항이 위치하는 곳입니다.
    • 웹이나 데이터베이스는 세부사항입니다.

원은 네 개여야만 하나?#

  • 위는 하나의 예시이며 더 많은 원칙이 필요합니다.
  • 어떤 경우에도 의존성 규칙은 적용됩니다.
  • 안쪽으로 이동할수록 추상화 정책의 수준은 높아지고, 바깥쪽 원은 저수준의 구체적인 세부사항으로 구성됩니다.

경계를 횡단하기#

  • 제어흐름과 의존성의 방향이 명백히 반대여야 하는 경우, 대체로 의존성 역전 원칙을 사용하여 해결합니다.
  • 아키텍처 경계를 횡단할 때 언제라도 동일한 기법을 사용할 수 있습니다. 우리는 동적 다형성을 이용하여 소스 코드 의존성을 제어흐름과 반대로 만들 수 있고, 이를 통해 제어흐름이 어느 방향으로 흐르더라도 의존성 규칙을 준수할 수 있습니다.

경계를 횡단하는 데이터는 어떤 모습인가#

  • 경계를 가로지르는 데이터는 흔히 간단한 데이터 구조로 이뤄져있습니다.
  • 기본적인 구조체나 간단한 데이터 전송 객체(data transfer object) 등 원하는 대로 고를 수 있습니다.
  • 다만 데이터 구조가 어떤 의존성을 가져 의존성 규칙을 위배하게 되는 일은 좋지 않습니다.
  • 데이터베이스의 로우를 바로 보내는 것은 좋지 않습니다. 이는 의존성 규칙을 위배하게 됩니다.
  • 즉, 경계를 가로질러 데이터를 전달할 때, 데이터는 항상 내부의 원에서 사용하기에 가장 편리한 형태를 가져야합니다.

전형적인 시나리오#

데이터베이스를 사용하는, 웹 기반 자바 시스템의 전형적인 시나리오

  • 의존성의 방향을 주목해야 하며, 모든 의존성은 경계선을 안쪽으로 가로지르며, 따라서 의존성 규칙을 준수합니다.

결론#

  • 이러한 규칙들을 준수하는 일은 수많은 고통거리를 덜어줄 것입니다.
  • 소프트웨어을 계층으로 분리하고 의존성 규칙을 준수한다면 본질적으로 테스트하기 쉬운 시스템을 만들게 될 것이며, 그에 따른 이점을 누릴 수 있습니다.
  • 데이터베이스나 웹 프레임워크와 같은 시스템의 외부 요소가 구식이 되더라도, 이들 요소를 야단스럽지 않게 교체할 수 있습니다.

23장. 프레젠터와 험블 객체#

  • 프레젠터는 험블 객체(Humble Object) 패턴을 따른 형태로, 아키텍처 경계를 식별하고 보호하는 데 도움이 됩니다.

험블 객체 패턴#

  • 함플 객체 패턴은 디자인 패턴으로, 테스트하기 어려운 행위와 테스트하기 쉬운 행위를 단위 테스트 작성자가 분리하기 쉽게 하는 방법으로 고안되었습니다.
    • 아이디어는 매우 단순하며, 행위들을 두 개의 모듈 또는 클래스로 나눕니다. 이들 모듈 중 하나가 험블(humble) 입니다.
    • 가장 기본적인 본질은 남기고, 테스트하기 어려운 행위를 모두 험블 객체로 옮깁니다.
  • 예시로 GUI 테스트 경우, 단위 테스트가 어려운데 험블 객체 패턴을 사용하면 두 부류의 행위를 분리하여 프레젠터와 뷰라는 서로 다른 클래스로 만들 수 있습니다.

프레젠터와 뷰#

  • 뷰는 험블 객체이고 테스트하기 어렵습니다.
  • 프레젠터는 테스트하기 쉬운 객체입니다. 프레젠터의 역할은 애플리케이션으로부터 데이터를 받아 화면에 표현할 수 있는 포맷으로 만드는 것입니다.
  • 화면에 표시되고 애플리케이션에서 어느 정도 제어할 수 있는 요소라면 무조건 뷰 모델 내부에 문자열, 불(boolean), 또는 열거형(enum) 형태로 표현합니다.
    • 즉, 뷰는 뷰 모델의 데이터를 화면으로 로드할 뿐이며, 이 외에 뷰가 많은 역할은 전혀없습니다. 따라서 뷰는 보잘것없습니다.(humble)

테스트와 아키텍처#

  • 테스트 용이성은 좋은 아키텍처가 지녀야 할 속성으로 오랫동안 알려져왔습니다.
    • 험블 객체 패턴이 좋은 예이며, 행위를 테스트하기 쉬운 부분과 테스트하기 어려운 부분으로 분리하면 아키텍처 경게가 정의되기 때문입니다.

데이터베이스 게이트웨이#

  • 유스케이스 인터랙터와 데이터베이스 사이에는 데이터베이스 게이트웨이(Database Gateway)가 위치합니다.
    • 이 게이트웨이는 다형적 인터페이스로, 애플리케이션이 데이터베이스에 수행하는 생성, 조회, 갱신, 삭제 작업과 관련된 모든 메서드를 포함합니다.
  • 인터랙터는 애플리케이션에 특화된 업무 규칙을 캡슐화하기 때문에 험블 객체가 아닙니다. 따라서 테스트하기 쉬우며, 게이트웨이는 스텁(stub)이나 테스트 더블(test-double) 로 적당히 교체할 수 있기 때문입니다.

데이터 매퍼#

  • 객체 관계 매퍼(Object Relational Mapper, ORM) 같은 것은 사실 존재하지 않으며 이는 데이터 구조가 아니기 때문입니다.
  • 객체와 달리 데이터 구조는 함숙된 행위를 가지지 않는 public 데이터 변수의 집합입니다.
  • ORM은 데이터베이스 계층에 속해야 하며, 게이트웨이 인터페이스와 데이터베이스 사이에서 일종의 또 다른 험블 객체 경계를 형성합니다.

서비스 리스너#

  • 데이터를 수신하는 서비스의 경우, 서비스 리스너(service listener)가 서비스 인터페이스로부터 데이터를 수신하고, 데이터를 애플리케이션에서 사용할 수 있게 간단한 데이터 구조로 포맷을 변경합니다. 그 후 이 데이터 구조는 서비스 경계를 가로질러서 내부로 전달됩니다.

결론#

  • 아키텍처 경계마다 경계 가까이 숨어 있는 험블 객체 패턴으르 발견할 수 있습니다.
  • 경계를 넘나드는 통신은 모두 간단한 데이터 구조를 수반할 때가 많고, 대개 그 경계는 테스트하기 어려운 것과 쉬운 것으로 분리됩니다.
  • 아키텍처 경계에서 험블 객체 패턴을 사용하면 전체 시스템의 테스트 용이성을 크게 높일 수 있습니다.

24장. 부분적 경계#

  • 선형적 설계는 YAGNI(You Aren't Going to Need It) 원칙을 위배하기 때문에 선형적인 설계를 좋아하지 않는 경우가 있습니다.
  • 그러나 필요하다고 판단하는 경우, 부분적 경계(partial boundary)을 구현해 볼 수 있습니다.

마지막 단계를 건너뛰기#

  • 부분적 경계를 생성하는 방법 하나는 독립적으로 컴파일하고 배포할 수 있는 컴포넌트를 만들기 위한 작업은 모두 수행한 후, 단일 컴포넌트에 그대로 모아만 두는 것입니다.
  • 부분적 경계를 만들려면 완벽한 경계를 만들 때 만큼의 코드량과 사전 설계가 필요합니다.

일차원 경계#

  • 완벽한 형태의 아키텍처 경계는 양방향으로 격리된 상태를 유지해야 하므로 쌍방향 Boundary 인터페이스를 사용합니다. 양방향으로 격리된 상태를 유지하려면 초기 설정할 때나 지속적으로 유지할 때도 비용이 많이 듭니다.

퍼사드#

  • 더 단순한 경계는 퍼사드(Facade) 패턴이며, 이 경우에는 심지어 의존성 역전까지도 희생합니다.
  • Facade 클래스에는 모든 서비스 클래스를 메서드 형태로 정의하고, 서비스 호출이 발생하면 해당 서비스 클래스로 호출을 전달합니다.
  • 클라이언트는 이들 서비스 클래스에 직접 접근할 수 없습니다.

퍼사드 패턴

  • 다만, Client가 이 모든 서비스 클래스에 대해 추이 종속성을 가지게 됩니다.

결론#

  • 아키텍처 경계가 언제, 어디에 존재해야 할지, 그리고 그 경계를 완벽하게 구현할지 아니면 부분적으로 구현할지를 결정하는 일 또한 아키텍트의 역할입니다.

25장. 계층과 경계#

  • 흔히 시스템이 세 가지 컴포넌트(UI, 업무 규칙, 데이터베이스)로만 구성된다고 생각하기 쉽지만, 시스템에서 컴포넌트의 개수는 이보다 훨씬 더 많습니다.

움퍼스 사냥 게임#

  • 의존성 규칙을 준수할 수 있도록 의존성이 적절한 방향을 가리키게 만들어야합니다.

클린 아키텍처?#

  • 변경의 축에 의해 정의되는 아키텍처 경계가 잠재되어 있을 수 있습니다.

개선된 다이어그램

  • 점선으로 된 테두리는 API를 정의하는 추상 컴포넌트를 가리키며, 해당 API는 추상 컴포넌트 위나 아래의 컴포넌트가 구현합니다.
  • 순전히 API 컴포넌트만 집중하면 다이어그램을 단순화할 수 있습니다.

단순화된 다이어그램

  • 이 구성은 데이터 흐름을 두 개의 흐름으로 효과적으로 분리합니다.
    • 왼쪽은 사용자와의 통신에 관여하며, 오른쪽의 흐름은 데이터 영속성에 관여합니다.

흐름 횡단하기#

  • 위 구성은 데이터 흐름을 세 개의 흐름으로 분리하며, 이들 흐름은 모두 GameRules 가 제어하는 예시입니다.

Network 컴포넌트 추가

  • 시스템이 복잡해질수록 컴포넌트 구조는 더 많은 흐름으로 분리됩니다.

흐름 분리하기#

  • 현실은 더 복잡합니다.

결론#

  • 위의 예제는 아키텍처 경계가 어디에나 존재한다는 사실을 보여주기 위함입니다.
  • 아키텍트로서 우리는 아키텍처 경계가 언제 필요한지를 신중하게 파악해내야 합니다.
  • 아키텍처는 추상화가 필요할 것이라고 미리 예측해서는 안됩니다.

26장. 메인(Main) 컴포넌트#

  • 모든 시스템에는 최소한 하나의 컴포넌트가 존재하고, 이 컴포넌트가 나머지 컴포넌트를 생성하고 조정하며 관리합니다. 이 컴포넌트를 메인(Main) 이라 부릅니다.

궁극적인 세부사항#

  • 메인 컴포넌트는 궁극적인 세부사항으로, 가장 낮은 수준의 정책입니다.
  • 메인은 시스템의 초기 진입점이며, 운영체제를 제외하면 어떤 것도 메인에 의존하지 않습니다.
  • 메인은 모든 팩토리(Factory)와 전략(Strategy), 그리고 시스템 전반을 담당하는 나머지 기반 설비를 생성한 후, 시스템에서 더 높은 수준을 담당하는 부분으로 제어권을 넘기는 역할을 맡습니다.
  • 의존성 주입 프레임워크를 이용해 의존성을 주입하는 일은 바로 이 메인 컴포넌트에서 이뤄져야합니다.

요지는 메인은 클린 아키텍처에서 가장 바깥 원에 위치하는, 지저분한 저수준 모듈입니다. 메인은 고수준의 시스템을 위한 모든 것을 로드한 후, 제어권을 고수준의 시스템에게 넘깁니다.

결론#

  • 메인을 애플리케이션의 플러그인이라고 생각하면 편합니다.
  • 메인은 초기 조건과 설정을 구성하고, 외부 자원을 모두 수집한 후, 제어권을 애플리케이션의 고수준 정책으로 넘기는 플러그인입니다.
  • 메인을 플러그인 컴포넌트로 여기고, 그래서 아키텍처 경계 바깥에 위치한다고 보면 설정 관련 문제를 훨씬 쉽게 해결할 수 있씁니다.

27장. '크고 작은 모든' 서비스들#

서비스 지향 '아키텍처'와 마이크로서비스 '아키텍처'는 최근에 큰 인기를 끌고 있으며 이유는 다음과 같습니다.

  • 서비스를 사용하면 상호 결합이 철저하게 분리되는 것처럼 보입니다. 다만 이는 일부만 맞습니다.
  • 서비스를 사용하면 개발과 배포 독립성을 지원하는 것처럼 보입니다. 이또한 일부만 맞습니다.

서비스 아키텍처?#

  • 서비스에서 기능을 프로세스나 플랫폼에 독립적이 되게끔 서비스들을 생성하면 의존성 규칙 준수 여부와 상관없이 의존성 규칙 준서 여부와 상관없이 큰 더움이될때가 많습니다.
  • 서비스는 결국 프로세스나 플랫폼 경계를 가로지르는 함수 호출에 지나지 않습니다.

서비스의 이점?#

결함 분리의 이점#

  • 시스템을 서비스들로 분리함으로써 얻게 되리라 예상되는 이점 하나는 서비스 사이의 결합이 확실히 분리된다는 점입니다.
  • 인터페이스가 잘 정의되어야 있어야함은 사실이며, 함수 또한 마찬가지입니다.

개발 및 배포 독립성의 오류#

  • 서비스를 사용함에 따라서 예측되는 또 다른 이점은 전담팀이 서비스를 소유하고 운영한다는 점입니다.
  • '결합 분리 오류'에 따르면 서비스라고 해서 항상 독립적으로 개발하고, 배포하며, 운영할 수 있는 것은 아닙니다. 데이터나 행위에서 어느 정도 결합되어 있다면 결합된 정도에 맞게 개발, 배포, 운영을 조정해야만 합니다.

야옹이 문제#

  • 서비스들이 결합되어 있으면, 독립적으로 개발하고 배포하거나, 유지될 수 없습니다.
  • 모든 소프트웨어 시스템은 서비스 지향이든 아니든 횡단 관심사(cross-cutting concern) 에 대해 문제를 지니게 됩니다.

객체가 구출하다#

  • 컴포넌트 기반 아키텍처에서는 템플릿 메서드(Template Method)나 전략(Strategy) 패턴 등을 이용해서 오버라이딩합니다.

컴포넌트 기반 서비스#

  • 각 서비스의 내부는 자신만의 컴포넌트 설계로 되어 있어서 파생 클래스를 만드는 방식으로 신규 기능을 추가할 수 있습니다. 파생 클래들은 각자의 컴포넌트 내부에 놓입니다.

횡단 관심사#

  • 아키텍처 경계는 서비스 사이에 있지 않으며, 서비스를 관통하며, 서비스를 컴포넌트 단위로 분할합니다.
  • 아키텍처 경계를 정의하는 것은 서비스 내에 위치한 컴포넌트입니다.

결론#

  • 서비스는 시스템의 확장성과 개발 가능성 측면에서 유용하지만, 그 자체로는 아키텍처적으로 중요한 요소는 아닙니다.
  • 시스템의 아키텍처는 시스템 내부에 그어진 경계와 경계를 넘나드는 의존성에 의해 정의됩니다.
  • 서비스는 단 하나의 아키텍처 경계로 둘러싸인 단일 컴포넌트, 혹은 다수의 컴포넌트로 구성할 수 있습니다.

28장. 테스트 경계#

  • 테스트는 시스템의 일부이며 아키텍처에도 관여합니다.

시스템 컴포넌트인 테스트#

  • 아키텍처 관점에서 모든 테스트는 동일합니다.
  • 테스트는 태생적으로 의존성 규칙을 따릅니다.
  • 테스트는 독립적으로 배포 가능합니다.
  • 테스트는 시스템 컴포넌트 중에 가장 고립되어 있습니다.
    • 테스트는 다른 모든 시스템 컴포넌트가 반드시 지켜야 하는 모델로 표현해줍니다.

테스트를 고려한 설계#

  • 테스트가 시스템의 설계와 잘 통합되지 않으면 테스트는 깨지기 쉬워지고, 시스템은 뻣뻣해재서 변경하기가 어려워집니다.
  • 시스템에 상하게 결합된 테스트라면 시스템이 변경될 때 함께 변경되어야 합니다.
  • 이러한 문제는 깨지기 쉬운 테스트 문제(Fragile Tests Problem) 이라고 합니다.
  • 이 문제를 해결하려면 테스트를 고려해서 설계해야 합니다.
    • 소프트웨어 설계의 첫 번째 규칙은 변동성이 있는 것에 의존하지 말 것입니다.

테스트 API#

  • 이러한 목표를 달성하려면 테스트가 모든 업무 규칙을 검증하는 데 사용할 수 있도록 특화된 API를 만들면 됩니다.
  • API는 사용자 인터페이스가 사용하는 인터랙터(Interactor)인터페이스 어댑터(Interface Adaptor) 들의 상위 집합이 될 것입니다.
  • 테스트 API는 테스트를 애플리케이션으로부터 분리할 목적으로 사용합니다.
    • 즉, 테스트 구조를 애플리케이션 구조로부터 결합을 분리하는 것 이 목표입니다.

구조적 결합#

  • 구조적 결합은 테스트 결합 중 가장 강하며, 가장 은밀하게 퍼져 나가는 유형입니다.
  • 구조적 결합이 강하면 필수적인 진화 과정을 방해할 뿐만 아니라, 상용 코드의 범용성과 유연성이 충분히 좋아지지 못하게 막습니다.

보안#

  • 테스트 API가 지닌 강력한 힘을 운영 시스템에 배포하면 위험에 처할 수도 있습니다.

결론#

  • 테스트는 시스템의 외부에 있지 않으며, 시스템의 일부입니다.
  • 테스트에서 기대하는 안정성과 회귀의 이점을 얻을 수 있으려면 테스트는 잘 설계돼어야 합니다.
  • 테스트를 시스템의 일부로 설계하지않으면 깨지기 쉽고 유지보수하기 어려워지는 경향이 있습니다.

29장. 클린 임베디드 아키텍처#

  • '소프트웨어는 닳지 않지만, 펌웨어와 하드웨어는 낡아 가므로 결국 소프트웨어도 수정해야합니다.'
    • 조금 더 살은 붙이면, '소프트웨어는 닳지 않지만, 펌웨어와 하드웨어에 대한 의존성을 관리하지 않으면 안으로부터 파괴될 수 있습니다.'
  • 소프트웨어와 하드웨어는 분리해야합니다.
  • 펌웨어를 양산하는 일을 멈추고, 코드에게 유효 수명을 주는 것도 필요합니다.

앱-티튜드 테스트#

  • 잠재적인 임베디드 소프트웨어가 펌웨어로 바꾸는 이유는 코드가 동작하게 만드는 데 대부분의 노력을 집중하고, 오랫동안 유용하게 남도록 구조화하는 데는 그리 신경 쓰지 않기 때문으로 보입니다.
  • 많은 앱들이 수명을 신경쓰지 않고 그저 동작하도록만 만들어지는 문제를 들 고 있습니다.
  • 앱이 동작하도록 만드는 것을 개발자용 앱-티튜드 테스트(App-titude test) 라고 무릅니다.
  • 다음과 같이 함수들이 방해가 되는 장애물이 됩니다.
    • 도메인 로직을 포함하는 함수
    • 하드웨어 플랫폼을 설정하는 함수들
    • 전원 버튼에 반응하는 함수들
    • 하드웨어로부터 A/D 입력을 읽어 들이는 함수
    • 영구 저장소에 값을 저장하는 함수들
    • 함수의 이름과 함수가 하는 일이 다른 함수

타킷-하드웨어 병목현상#

  • 임베이드 개발자들은 제한된 메모리 공간, 실시간성 제약과, 처리완료 시간, 제한된 입출력, 특이한 사용자 인터페이스 등등의 고려사항이 존재합니다.
  • 임베디드가 지닌 특수한 문제 중 하나는 타킷-하드웨어 병목현상(target-hardware bottleneck) 입니다.
    • 임베디드 코드가 클린 아키텍처 원칙과 실천법을 따르지 않고 작성된다면, 대개의 경우 코드를 테스트할 수 있는 환경이 해당 특정 타킷으로 국한됩니다.
    • 그 타킷이 테스트가 가능한 유일한 자소라면 타킷-하드웨어 병목현상이 발생하여 진척이 느려질 것입니다.

클린 임베디드 아키텍처는 테스트하기 쉬운 임베디드 아키텍처다#

몇 가지 아키텍처 원칙을 임베디드 소프트웨어와 펌웨어에 적용하여 타킷 하드웨어 병목현상을 줄이는 방법이 있습니다.

계층#
  • 소프트웨어, 펌웨어, 하드웨어를 분리해야 합니다.
  • 소프트웨어와 펌웨어가 서로 섞이는 일은 안티 패턴(anti-pattern)이며, 변화가 어렵습니다.

세 개의 계층

하드웨어는 세부사항입니다.#
  • 소프트웨어와 펌웨어 사이의 경계는 코드와 하드웨어 사이의 경계와는 달리 잘 정의하기가 어렵습니다.
  • 임베디드소프트웨어 개발자가 해야 할 일 하나는 이 경계를 분명하게 만드는 것이며, 소프트웨어와 펌웨어 사이의 경계는 하드웨어 추상화 계층(Hardware Abstraction Layer, HAL) 이라 부릅니다.
  • HAL의 API는 소프트웨어의 필요에 맞게 만들어져야합니다.
  • 계층은 또 다른 계층을 포함할 수도 있으며, 계층의 수가 정해진 상태로 구성되기 보다는 프랙털(fractal) 패턴에 더 가깝습니다.

하드웨어 추상화 계층

HAL 사용자에게 하드웨어 세부사항을 드러내지 말라#

  • 클린 임베디드 아키텍처로 설계된 소프트웨어는 타깃 하드웨어에 관계없이 테스트가 가능합니다.
프로세서는 세부사항입니다.#
  • 모든 소프트웨어는 반드시 프로세서에 독립적이야 함이 분명하지만, 모든 펌웨어가 그럴 수는 없습니다.
  • 클린 임베디드 아키텍처라면 이들 장치 접근 레지스터를 직접 사용하는 코드는 소수의, 순전히 펌웨어로만 한정시켜야 합니다.
운영체제는 세부사항이다#
  • 클린 임베디드 아키텍처는 운영체제 추상화 계층(Operation System Abstraction Layer, OSAL) 을 통해 소프트웨어를 운영체제로부터 격리시킵니다.

운영체제 추상화 계층

  • 제대로 만든 OSAL은 타깃과는 별개로 테스트할 수 있도록 해주는 경계층 또는 일련의 대체 지점을 제공합니다.

인터페이스를 통하고 대체 가능성을 높이는 방향으로 프로그램하라#

  • 계층형 아키텍처(layered architecture)는 인터페이스를 통해 통해 프로그래밍하자는 발상을 기반으로 합니다.
  • 경험법칙에 따르면 인터페이스 정의는 헤더 파일에 해야 합니다.
  • 오직 구현체에서만 필요한 데이터 구조, 상수, 타입 정의들로 인터페이스 헤더 파일을 어지럽히지 않습니다.
  • 클린 임베디드 아키텍처에서는 모듈들이 인터페이스를 통해 상호작용하기 때문에 각각의 계층 내부에서 테스트가 가능합니다.

DRY 원칙: 조건부 컴파일 지시자를 반복하지 말라#

  • 일련의 인터페이스를 제공한다면, 링커 혹은 어떤 형태의 실시간 바인딩을 사용해서 소프트웨어 하드웨어와 연결할 수 있습니다.

결론#

  • 클린 임베디드 아키텍처는 제품이 장기간 생명력을 유지하는데 도움을 줍니다.
Last updated on