클린아키텍처 5부: 아키텍처(2)

Jihyun·2021년 12월 19일
0

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


22장. 클린 아키텍처

관심사의 분리 (separationof concerns)

아키텍처는 소프트웨어를 계층으로 분리함으로써 관심사의 분리라는 목표를 달성한다.

  • 프레임워크 독립성

    아키텍처는 다양한 기능의 라이브러리를 제공하는 소프트웨어, 즉 프레임워크의 존재 여부에 의존하지 않는다. 이를 통해 이러한 프레임워크를 도구로 사용할 수 있으며, 프레임워크가 지닌 제약사항 안으로 시스템을 강제하지 않는다.

  • 테스트 용이성

    업무 규칙은 UI, 데이터베이스, 웹 서버, 또는 여타 외부 요소가 없이도 테스트할 수 있다.

  • UI 독립성

    시스템의 나머지 부분을 변경하지 않고도 UI롤 쉽게 변경할 수 있다.

  • 데이터베이스독립성

    오라클이나 MSSQL서버를 MongoDB, BigTable, cauchDB 등으로 교체할 수 있다. 업무 규칙은 데이터베이스에 결합되지 않는다.

  • 모든 외부 에이전시에 대한 독립성

    실제로 업무 규칙은 외부 세계와의 인터페이스에 대해 전혀 알지 못한다.


의존성 규칙 (Dependency Rule)

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


엔티티

엔티티는 전사적인 핵심 업무 규칙을 캡슐화한다.

운영 관점에서 특정 애플리케이션에 무언가 변경이 필요하더라도 엔티티 계층에는 절대로 영향을 주어서는 안 된다.

유스케이스

유스케이스 계층의 소프트웨어는 애플리케이션에 특화된 업무 규칙을 포함한다.

유스케이스 계층의 소프트웨어는 시스템의 모든 유스케이스를 캡슐화하고 구현한다. 유스케이스는 엔티티로 들어오고 나가는 데이터 흐름을 조정하며, 엔티티가 자신의 핵심 업무 규칙을 사용해서 유스케이스의 목적을 달성하도록 이끈다.

운영 관점에서 애플리케이션이 변경된다면 유스케이스가 영향을 받으며, 따라서 이 계층의 소프트웨어에도 영향을 줄 것이다. 유스케이스의 세부사항이 변하면 이 계층의 코드 일부는 분명 영향을 받을 것이다.

인터페이스 어댑터 (lnterface Adaptor)

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

이 계층에는 데이터를 외부 서비스와 같은 외부적인 형식에서 유스케이스나 엔티티에서 사용되는 내부적인 형식으로 변환하는 또 다른 어댑터 가 필요하다.

프레임워크와 드라이버

프레임워크와 드라이버 계층은 모든 세부사항이 위치하는 곳이다. 웹은 세부사항이다. 데이터베이스는 세부사항이다. 우리는 이러한 것들을 모두 외부에 위치시켜서 피해를 최소화한다.


결론

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



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

험블 객체 패턴

험블 객체 패턴은 디자인 패턴으로, 테스트하기 어려운 행위와 테스트하기 쉬운 행위를 단위 테스트 작성자가 분리하기 쉽게 하는 방법으로 고안되었다.

행위들을 두 개의 모듈 또는 클래스로 나눈다. 가장 기본적인 본질은 남기고 , 테스트하기 어려운 행위를 모두 험블 객체로 옮긴다. 나머지 모듈에는 험블 객체 에 속하지 않은, 테스트하기 쉬운 행위를 모두 옮긴다.

험블 객체 패턴을 사용하면 두 부류의 행위를 분리하여 '프레젠터'와 '뷰' 라는 서로 다른 클래스로 만들 수 있다.

프레젠터와 뷰

'뷰'는 험블 객체이고 테스트하기 어렵다. 이 객체에 포함된 코드는 가능한 한 간단하게 유지한다.

'프레젠터'는 테스트하기 쉬운 객체다. 프레젠터의 역할은 애플리케이션으로부터 데이터를 받아 화면에 표현할 수 있는 포맷으로 만드는 것이다. 이를 통해 뷰는 데이터를 화면으로 전달하는 간단한 일만 처리하도록 만든다.

테스트와 아키텍처

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


결론

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



24장. 부분적 경계

이 장에서는 아키텍처 경계를 부분적으로 구현하는 간단한 방법 세 가지를 살펴본다.

마지막 단계를 건너뛰기

부분적 경계를 생성하는 방법 하나는 Boundary 인터페이스, Input과 Output을 위한 데이터 구조를 모두 완성한 후, 독립적으로 컴파일하고 배포할 수 있는 컴포넌트를 만들기 위한 작업을 수행만 한 뒤 단일 컴포넌트에 그대로 모 아만 두는 것이다.

이 방법은 완벽한 경계를 만들 때 만큼의 코드량과 사전 설계가 필요하다. 하지만 다수의 컴포넌트를 관리하는 작업은 하지 않아도 된다. 추적을 위한 버전 번호도 없으며, 배포 관리 부담도 없다.

일차원 경계

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

전통적인 전략(strategy) 패턴을 사용하여 추후 완벽한 형태의 경계로 확장할 수 있는 공간을 확보할 수 있는 구조를 만들 수는 있으나, 쌍방향 인터페이스가 없고 개발자와 아키텍트가 근면 성실하고 제대로 훈련되어 있지않다면, 이러한 분리는 매우 빠르게 붕괴될 수 있다는 점 역시 분명하다.

퍼사드

이보다 훨씬 더 단순한 경계는 퍼사드(Facade) 패턴으로, 경계는 퍼사드 클래스로만 간단히 정의된다. 퍼사드 클래스에는 모든 서비스 클래스를 메서드 형태로 정의하고, 서비스 호출이 발생하면 해당 서비스 클래스로 호출을 전달한다. 클라이언트는 이들 서비스 클래스에 직접 접근할 수 없다.



25장. 계층과 경계

아키텍처 경계는 어디에나 존재한다.


아키텍트로서 우리는 아키텍처 경계가 언제 필요한지를 신중하게 파악해내야 한다. 또한 우리는 이러한 경계를 제대로 구현하려면 비용이 많이 든다는 사실도 인지하고 있어야 한다. 이와 동시에 이러한 경계가 무시되었다면 나중에 다시 추가하는 비용이 크다는 사실도 알아야 한다.

아키텍트인 우리는 어떻게 해야 할까? 추상화가 필요하리라고 미리 예측해서는 안 된다.

바로 YAGNI(You Aren’t Going to Need lt)가 말하는 철학이다. 오버 엔지니어링(over engineering)이 언더 엔지니어링(under engineering)보다 나뿔 때가 훨씬 많기 때문이다.

그러면서도 어떤 아키텍처 경계도 존재하지 않는 상황에서 경계가 정말로 필요하다는 사실을 발견한 경우, 그때서야 경계를 추가하려면 비용이 많이 들고 큰 위험을 감수해야 한다.

아키텍트는 현명하게 추측해야만 한다. 비용을 산정하고, 어디에 아키텍처 경계를 둬야 할지, 그리고 완벽하게 구현할 경계는 무엇인지와 부분적으로 구현할 경계와 무시할 경계는 무엇인지를 결정해야만 한다.

하지만 이는 일회성 결정은 아니다. 프로젝트 초반에는 구현할 경계가 무엇인지와 무시할 경계가 무엇인지를 쉽게 결정할 수 없다. 대신 지켜봐야 한다. 시스템이 발전함에 따라 주의를 기울여야 한다. 경계가 필요할 수도 있는 부분에 주목하고, 경계가 존재하지 않아 생기는 마찰의 어렴풋한 첫 조짐을 신중하게 관찰해야 한다.

목표를 달성하려면 빈틈없이 지켜봐야 한다.



26장. 메인 컴포넌트

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


메인 컴포넌트는 궁극적인 세부사항으로, 가장 낮은 수준의 정책이다. 메인은 시스템의 초기 진입점이다. 운영체제를 제외하면 어떤 것도 메인에 의존 하지 않는다. 메인은 모든 팩토리와 전략, 그리고 시스템 전반을 담당하는 나머지 기반 설비를 생성한 후, 시스템에서 더 높은 수준을 담당하 는 부분으로 제어권을 넘기는 역할을 맡는다.

의존성 주입 프레임워크를 이용해 의존성을 주입하는 일은 바로 이 메인 컴포넌트에서 이뤄져야 한다. 메인에 의존성이 일단 주입되고 나면, 메인은 의존성 주입 프레임워크를 사용하지 않고도 일반적인 방식으로 의존성을 분배할 수 있어야 한다.

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



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

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

  • 서비스를 사용하면 상호 결합이 철저하게 분리되는 것처럼 보인다.
  • 서비스를 사용하면 개발과 배포 독립성을 지원하는 것처럼 보인다.

먼저 서비스를 사용한다는 것이 본질적으로 아키텍처에 해당하는지에 대해 생각해 보자. 이 개념은 명백히 사실이 아니다.

시스템의 아키텍처는 의존성 규칙을 준수하며 고수준의 정책을 저수준의 세부사항으로부터 분리하는 경계에 의해 정의된다. 단순히 애플리케이션의 행위를 분리할 뿐인 서비스라면 값비싼 함수 호출에 불과하며 , 아키텍처 관점 에서 꼭 중요하다고 볼 수는 없다. 결국 서비스는 프로세스나 플랫폼 경계를 가로지르는 함수 호출에 지나지 않는다.

서비스를 사용하면 상호 결합이 철저하게 분리된다?

시스템을 서비스들로 분리함으로써 얻게 되리라 예상되는 큰 이점 하나는 서비스 사이의 결합이 확실히 분리된다는 점이다. 어쨌든 각 서비스는 서로 다른 프로세스에서, 심지어는 서로 다른 프로세서에서 실행된다. 따라서 서비스는 다른 서비스의 변수에 직접 접근할 수 없다.

물론 서비스는 개별 변수 수준에서는 각각 결합이 분리된다. 하지만 프로세서 내의 또는 네트워크 상의 공유 자원 때문에 결합될 가능성이 여전히 존재한다. 더욱이 서로 공유하는 데이터에 의해 이들 서비스는 강력하게 결합되어 버린다.

서비스를 사용하면 개발과 배포 독립성을 지원한다?

서비스를 사용함에 따라서 예측되는 또 다른 이점은 전담팀이 서비스를 소유하고 운영한다는 점이다. 그래서 데브옵스(devops) 전략의 일환으로 전담팀에서 각 서비스를 작성하고, 유지보수하며, 운영하는 책임을 질 수 있다. 이러한 개발 및 배포 독립성은 확장 가능한(scalable) 것으로 간주된다. 대규모 엔터 프라이즈 시스템을 독립적으로 개발하고 배포 가능한 수십, 수백, 수천 개의 서비스들을 이용하여 만들 수 있다고 믿는다. 시스템의 개발, 유지보수, 운영 또한 비슷한 수의 독립적인 팀 단위로 분할할 수 있다고 여긴다.

이러한 믿음에도 어느 정도 일리가 있지만, 극히 일부일 뿐이다. 대규모 엔터프라이즈 시스템은 서비스 기반 시스템 이외 에도, 모노리틱 시스템이나 컴포넌트 기반 시스템으로도 구축할 수 있다는 사실은 역사적으로 증명되어 왔다. 따라서 서비스는 확장 가능한 시스템을 구축하는 유일한 선택지가 아니다.

또한 서비스라고 해서 항상 독립적으로 개발 하고, 배포하며, 운영할 수 있는 것은 아니다. 데이터나 행위에서 어느 정도 결합되어 있다면 결합된 정도에 맞게 개발, 배포, 운영을 조정해야만 한다.



28장. 테스트 경계

시스템 컴포넌트인 테스트

테스트는 시스템의 일부인가? 아니면 별개인가? 어떤 종류의 테스트가 있는가? 단위 테스트와 통합 테스트는 서로 다른가? 인수 테스트, 기능 테스트, Cucumber 테스트, TDD 테스트, BDD 테스트, 컴포넌트 테스트 등은 어떻지?

이 책은 이러한 논란에 휘말릴 필요가 없다. 아키텍처 관점에서는 모든 테스트가 동일하기 때문이다.


테스트는 태생적으로 의존성 규칙을 따른다. 테스트는 세부적이며 구체적인 것으로, 의존성은 항상 테스트 대상이 되는 코드를 향한다. 또한 테스트는 독립적으로 배포 가능하다. 사실 대다수의 경우 테스트는 테스트 시스템에만 배포하며, 상용 시스템에는 배포하지 않는다. 따라서 심지어 배포 독립성이 달리 필요하지 않은 시스템에서도 테스트는 독립적으로 배포될 것이다.

테스트는 시스템 외부에 있지 않다. 오히려 시스템의 일부다. 테스트는 시스템 컴포넌트 중에서 가장 고립되어 있다. 테스트의 역할은 운영이 아니라 개발을 지원하는 데 있다. 테스트는 다른 모든 시스템 컴포넌트가 반드시 지켜야 하는 모델을 표현해준다.

테스트를 고려한 설계

테스트가 시스템의 설계와 잘 통합되지 않으면, 테스트는 깨지기 쉬워지고, 시스템은 뻣뻣해져서 변경하기가 어려워진다.

문제는 결합이다. 시스템에 강하게 결합된 테스트라면 시스템이 변경될 때 함께 변경되어야만 한다. 시스템 컴포넌트에서 생긴 아주 사소한 변경도, 이와 결합된 수많은 테스트를 망가뜨릴 수 있다.

이 문제를 해결하려면 테스트를 고려해서 설계해야 한다. GUI로 시스템을 조작하는 테스트 스위트는 분명 깨지기 쉽다. 따라서 시스템과 테스트를 설 계할 때, GUI를 사용하지 않고 업무 규칙을 테스트할 수 있게 해야 한다.

이 목표를 달성하려면 테스트가 모든 업무 규칙을 검증하는 데 사용할 수 있도록 특화된 API를 만들면 된다.

테스트 API는 테스트를 애플리케이션으로부터 분리할 목적으로 사용한 다. 단순히 테스트를 UI에서 분리하는 것만이 아닌, 테스트 구조를 애플리케이션 구조로부터 결합을 분리하는 게 목표다.



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

소프트웨어는 닳지 않지만, 펌웨어와 하드웨어에 대한 의존성을 관리하지 않으면 안에서부터 파괴될 수 있다.

잠재적으로 오래 살아남을 수 있던 임베디드 소프트웨어가 하드웨어 의존성에 오염되는 바람에 짧게 삶을 마감하는 일은 드물지 않다.

모든 코드가 펌웨어가 되도록 내버려두면 제품이 오래 살아남을 수 없게 된다. 오직 타깃 하드웨어에서만 테스트할 수 있는 제품도 마찬가지다. 클린 임베디드 아키텍처는 제품이 장기간 생명력을 유지하는 데 도움을 준다.

0개의 댓글