본문 바로가기
독서/클린 아키텍처

클린 아키텍처 5장. 아키텍처 - 2

by yoon_seon 2024. 7. 24.

22장. 클린 아키텍처


  • 지난 수십년간 시스템 아키텍처와 관련된 여러 가지 아이디어가 나왔다.
    • 육각형 아키텍처(Hexagonal Architecture)
    • DCI(Data, Content and Interaction)
    • BCE(Boundary-Control-Entity)
  • 위 아키텍처는 모두 시스템이 다음과 같은 특징을 지닌다.
    • 프레임워크 독립성 : 아키텍처는 프레임워크를 도구로 사용하며, 프레임워크가 지닌 제약사항으로 시스템을 강제하지 않는다. 즉, 프레임워크 존재 여부에 의존하지 않는다.
    • 테스트 용이성 : 업무 규칙은 UI, DB, 웹 서버, 기타 외부 요소가 없이 테스트할 수 있다.
    • UI 독립성 : 시스템의 변경 없이 UI를 쉽게 변경할 수 있다.
    • 데이터베이스 독립성 : 오라클이나 MS-SQL 서버 등을 다른 DB로 교체할 수 있고, 업무 규칙은 데이터베이스에 결합하지 않는다.
    • 모든 외부 에이전시에 대한 독립성 : 실제로 업무 규칙은 외부 세계와의 인터페이스에 대해 전혀 알지 못한다.

 

의존성 규칙

  • 각각의 동심원은 소프트웨어에서 서로 다른 영역을 표현한다.
  • 보통 안으로 들어갈수록 고수준의 소프트웨어가 되며 바깥쪽 원은 메커니즘으로, 안쪽 원은 정책이다.
  • 이러한 아키텍처가 동작하도록 하는 가장 중요한 규칙은 의존성 규칙이다.
  • 소스 코드 의존성은 반드시 안쪽으로, 고수준의 정책을 향해야 한다.
    • 내부 원에 속한 요소는 외부 원에 속한 어떤 것도 알지 못한다.
    • 외부의 원에 선언된 데이터 형식도 내부의 원에서 절대로 사용해서는 안된다.

 

엔티티

  • 엔티티는 전사적인 핵심 업무 규칙을 캡슐화하며 엔티티는 메서드를 가지는 객체이거나 일련의 데이터 구조와 함수의 집합일 수도 있다.
  • 다양한 애플리케이션에서 엔티티를 재사용할 수 만 있다면, 그 형태는 중요하지 않다.
  • 외부의 무언가가 변경되더라도 엔티티가 변경될 가능성은 지극히 낮다.
  • 운영 관점에서 특정 애플리케이션에 변경이 생겨도 엔티티 계층에는 절대로 영향을 주어서는 안 된다.

 

유스케이스

  • 유스케이스 계층의 소프트웨어는 애플리케이션에 특화된 업무 규칙을 포함하며, 시스템의 모든 유스케이스를 캡슐화하고 구현한다.
  • 유스케이스는 엔티티로 들어오고 나가는 데이터 흐름을 조정하며 엔티티가 자신의 핵심 업무 규칙을 사용해서 유스케이스의 목적을 달성하도록 이끈다.
  • 이 계층에서 발생한 변경이 엔티티에 영향을 주어서는 안 되며, 외부 요소 변경이 이 계층에 영향을 줘서도 안 된다.
  • 하지만 운영 관점에서 애플리케이션이 변경된다면 유스케이스가 영향을 받고, 유스케이스 세부사항이 변하면 일부 코드는 영향을 받음

 

인터페이스 어댑터

  • 일련의 어댑터들로 구성되며 프레젠터, 뷰, 컨트롤러가 모두 이 계층에 속한다.
  • 컨트롤러에서 유스케이스로 전달되고, 다시 유스케이스에서 프레젠터와 뷰로 되돌아간다.
  • 어댑터는 데이터를 유스케이스와 엔티티에 맞는 형식에서 데이터베이스나 웹 같은 외부 요소에 맞는 형식으로 변환한다.
  • 데이터를 외부 서비스와 같은 외부적인 형식에서 유스케이스나 엔티티에서 사용되는 내부적인 형식으로 변환하는 또 다른 어댑터가 필요하다.

 

프레임워크와 드라이버

  • 가장 바깥쪽 계층은 일반적으로 데이터베이스나 웹 프레임워크 같은 도구들로 구성된다.
  • 이 계층에서는 안쪽 원과 통신하기 위한 접합 코드 외에는 특별히 더 작성해야 할 코드가 그다지 많지 않다.
  • 이 계층은 모든 세부사항이 위치한 곳으로, 웹과 데이터베이스는 세부사항이기에 모두 외부에 위치시켜 피해를 최소화한다.

 

원은 네 개여야만 하나?

  • 위 그림은 그저 설명을 위한 예시일 뿐, 더 많은 원이 필요할 수도 있다.
  • 하지만 어떤 경우에도 소스코드 의존성은 항상 안쪽을 향하며 안쪽으로 이동할수록 추상화와 정책 수준은 높아진다.
  • 가장 바깥원은 저수준의 구체적인 세부사항으로 구성되며 안쪽으로 이동할 수록 추상화되고 더 높은 수준의 정책들을 캡슐화한다.
  • 따라서 가장 안쪽 원은 가장 범용적이며 높은 수준을 가진다.

 

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

  • 경계를 가로진 데이터는 흔히 간단한 데이터 구조로 이루어지며, 기본적인 구조체나 DTO 등 원하는 대로 고를 수 있다.
  • 또는 함수를 호출할 때 간단한 인자를 사용해서 데이터로 전달하거나, 해시맵으로 묶던지 객체로 구성할 수도 있다.
  • 중요한 점은 격리되어 있는 간단한 데이터 구조가 경계를 가로질러 전달된다는 사실이다.
  • 데이터가 어떤 의존성을 가져 의존성 규칙을 위배하게 되는 일은 바라지 않음
  • 따라서 경계를 가로질러 데이터를 전달할 때, 데이터는 항상 내부의 원에서 사용하기에 가장 편리한 형태를 가져야 한다.

 

전형적인 시나리오

  1. 웹 서버는 사용자로부터 입력 데이터를 모아서 Controller(좌측 상단)로 전달한다.
  2. Controller는 데이터를 POJO로 묶은 후 InputBoundary 인터페이스를 통해 UseCaseInteractor로 전달한다.
  3. UseCaseInteractor는 이 데이터를 해석해서 Entities가 어떻게 동작할지 제어하는 데 사용한다. UseCaseInteractor는 DataAccessInterface를 사용하여 Entities가 사용할 데이터를 DB에서 불러와서 메모리로 로드한다. Entities가 완성되면, UseCaseInteractor는 Entities로부터 데이터를 모아서 또 다른 POJO객체인 OutputData를 구성한다.
  4. OutputData는 OutputBoundary 인터페이스를 통해 Presenter로 전달된다.
    • Presenter가 맡은 역할은 OutputData를 ViewModel과 같이 화면에 출력할 수 있는 형식으로 재 구성하는 일이다. ViewModel 또한 POJO이다.
  5. View에서는 이 데이터를 화면에 출력한다.
  6. Presenter는 ViewModel을 로드할 때 Date와 같은 객체를 사용자가 보기에 적절한 형식의 문자열로 변환한다. 따라서 ViewModel에서 HTML 페이지로 데이터를 옮기는 일을 빼면, View에서 해야 할 일은 거의 남아 있지 않다. (View를 위한 데이터 변환 처리는 Presenter에서 하기 때문에)

 

결론

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

 

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


  • 22장에서 소개한 프레젠터는 험블 객체(Humble Object) 패턴을 따른 형태로, 아키텍처 경계를 식별하고 보호하는데 도움이 된다.
  • “클린 아키텍처”는 험블 객체 구현체들로 가득 차 있다.

 

험블 객체 패턴

  • 험블 객체 패턴은 디자인 패턴으로, 테스트하기 어려운 행위와 쉬운 행위를 단위 테스트 작성자가 쉽게 분리하는 방법으로 고안됨
  • 행위들을 두 개의 모듈 또는 클래스로 나누는데 이 모듈 중 하나가 험블이다.
    • 가장 기본적인 본질은 남기고, 테스트하기 어려운 행위를 모두 험블 객체로 옮김
    • 나머지 모듈에는 험블 객체에 속하지 않은, 테스트하기 쉬운 행위를 모두 옮김
  • 예를 들어 GUI는 각 요소가 필요한 위치에 표시되었는지 검사하는 단위테스트가 어렵지만, 수행하는 행위에 대한 테스트는 쉽다.
  • 험블 객체 패턴을 사용하면 두 부류의 행위를 분리하여 프레젠터와 뷰라는 서로 다른 클래스로 만들 수 있다.

 

프레젠터와 뷰

    • 뷰는 험블 객체
    • 테스트하기 어렵기에 가능한 간단하게 유지함
    • 뷰는 데이터를 GUI로 이동시키지만, 데이터를 직접 처리하지는 않음
  • 프레젠터
    • 프레젠터는 테스트하기 쉬운 객체
    • 애플리케이션으로부터 데이터를 받아 화면에 표현할 수 있는 포맷으로 만드는 역할을 한다.
    • 이를 통해 뷰는 데이터를 화면으로 전달하는 간단한 일만 처리하도록 만든다.
  • 뷰는 뷰 모델의 데이터를 화면으로 로드할 뿐이며, 이 외에 뷰가 맡은 역할은 전혀 없으므로 보잘것없다.
    • 예를 들어 애플리케이션에서 화면에 금액을 표시하고자 한다면, 애플리케이션은 프레젠터에 Currency 객체를 전달함
    • 그러면 프레젠터는 객체를 소수점과 통화 표시가 된 포맷으로 적절하게 변환하여 문자열을 생성하고, View Model에 담음
    • 그러면 뷰는 View Model에서 데이터를 찾음

 

테스트와 아키텍처

  • 테스트 용이성은 좋은 아키텍처가 지녀야 할 속성으로 알려져 있고, 험블 객체 패턴이 좋은 예임
  • 행위를 테스트하기 쉬운 부분과 테스트하기 어려운 부분으로 분리하면 아키텍처 경계가 정의되기 때문임
  • 프레젠터와 뷰 사이의 경계는 이러한 경계 중 하나이며, 이 밖에도 수많은 경계가 존재함

 

데이터베이스 게이트웨이

  • 유스케이스 인터랙터와 데이터베이스 사이에는 데이터베이스 게이트웨이가 위치함
  • 유스케이스는 SQL을 허용하지 않기에 필요한 메서드를 제공하는 게이트웨이 인터페이스를 호출하고, 구현체(험블 객체)는 데이터베이스 계층에 위치함
  • 게이트웨이
    • 다형적 인터페이스로 애플리케이션이 데이터베이스에 수행하는 CRUD 작업과 관련된 메서드를 포함함
    • 예를 들어 애플리케이션에서 어제 로그인한 모든 사용자의 성을 알아야 한다면, UserGateway 인터페이스는 getLastNameOfUserWhoLoggedInAfter라는 메서드를 제공하고 해당 메서드는 날짜를 인자로 받아서 사용자 성을 담은 목록을 반환할 것임
  • 인터렉터
    • 애플리케이션에 특화된 업무 규칙을 캡슐화하기 때문에 험블 객체가 아니다.
    • 따라서 테스트하기 쉬우며, 게이트웨이는 스텁이나 테스트 더블로 적당히 교체할 수 있기 때문임

 

결론

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

 

 

25장. 계층과 경계


  • 시스템이 3가지 컴포넌트(UI, 업무 규칙, 데이터베이스)로만 구성된다고 생각하기 쉽다.
  • 몇몇 단순한 시스템에서는 이 정도로 충분하지만, 대다수 시스템에서 컴포넌트의 개수는 이보다 많다.

 

결론

  • 아키텍처 경계는 어디에나 존재하며, 아키텍처 경계가 언제 필요한지 신중하게 파악해내야 한다.
    • 이러한 경계를 제대로 구현하려면 비용이 많이 든다는 사실도 인지해야 함
    • 동시에 이런 경계가 무시되었다면 나중에 다시 추가하는 비용이 더 크다는 것을 알아야 함
  • 추상화가 필요하리라고 미리 예측해서는 안 된다.(YAGNI)
    • 오버 엔지니어링이 언더 엔지니어링보다 나쁠 때가 훨씬 많음
    • 어떤 아키텍처 경계도 존재하지 않는 상황에서 경계가 필요하다는 사실을 발견할 경우, 그때서야 경계를 추가하려면 많은 비용과 위험을 감수해야 함
  • 미래를 내다보고 현명하게 추측해야 한다.
    • 비용을 산정하고 어디에 아키텍처 경계를 두어야 할지, 완벽하게 구현할 경계는 무엇인지와 부분적으로 구현할 경계, 무시할 경계는 무엇인지 결정해야 함
    • 이는 일회성이 아니며 프로젝트 초반에는 구현할 경계와 무시할 경계를 쉽게 결정할 수 없으므로 지켜봐야 함
    • 경계가 필요할 수 있는 부분에 주목하고, 경계가 존재하지 않아 생기는 조짐을 신중하게 관찰해야 함
    • 첫 조짐이 보이는 시점이 되면, 해당 경계를 구현하는 비용과 무시할 때 감수할 비용을 가늠해 보고, 결정 사항을 자주 검토해야 함
    • 목표는 경계 구현 비용이 무시해서 생기는 비용보다 적어지는 바로 그 변곡점에서 경계를 구현하는 것
    • 목표를 달성하려면 빈틈없이 지켜봐야 한다.

 

 

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


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

 

궁극적인 세부사항

  • 메인 컴포넌트는 궁극적인 세부사항으로, 가장 낮은 수준의 정책이다.
  • 메인은 시스템의 초기 진입점으로 운영체제를 제외하면 어떤 것도 메인에 의존하지 않는다.
  • 메인은 모든 팩토리와 전략, 시스템 전반을 담당하는 나머지 기반 설비를 생성한 후, 시스템에서 더 높은 수준을 담당하는 부분으로 제어권을 넘긴다.
  • 의존성 주입 프레임워크를 이용해 의존성을 주입하는 일은 메인 컴포넌트에서 이뤄져야 한다.
  • 메인에 의존성이 일단 주입되고 나면, 메인은 의존성 주입 프레임워크를 사용하지 않고도 일반적인 방식으로 의존성을 분배할 수 있어야 함
  • 메인을 지저분한 컴포넌트 중에서도 가장 지저분한 컴포넌트라고 생각하자(클린 아키텍처에서 가장 바깥 원에 위치하는 지저분한 저수준 모듈)
  • 메인은 고수준의 시스템을 위한 모든 것을 로드한 후, 제어권을 고수준의 시스템에게 넘긴다.

 

결론

  • 메인은 초기 조건과 설정을 구성하고, 외부 자원을 모두 수집한 후, 제어권을 애플리케이션의 고수준 정책으로 넘기는 플러그인이다.
  • 메인은 플러그인이므로 메인 컴포넌트를 애플리케이션의 설정별로 하나씩 두도록 하여 둘 이상의 메인 컴포넌트를 만들 수도 있다.
  • 메인을 플러그인 컴포넌트로 여기고 아키텍처 경계 밖에 위치한다고 보면 설정 관련 문제를 훨씬 쉽게 해결할 수 있다.

 

 

27장. ‘크고 작은 모든’ 서비스


  • 서비스 지향 ‘아키텍처’와 마이크로서비스 ‘아키텍처’는 최근에 큰 인기를 끌고 있다. 그 이유는 다음과 같음
    • 서비스를 사용하면 상호 결합이 철저하게 분리되는 것처럼 보인다. 나중에 보겠지만, 이는 일부만 맞는 말이다.
    • 서비스를 사용하면 개발과 배포 독립성을 지원하는 것처럼 보인다. 나중에 보겠지만, 이 역시도 일부만 맞는 말이다.

 

서비스 아키텍처?

  • 서비스를 사용한다는 것은 본질적으로 아키텍처에 해당하는 게 아니다.
  • 시스템의 아키텍처는 의존성 규칙을 준수하며 고수준의 정책을 저수준의 세부사항으로부터 분리하는 경계에 의해 정의된다.
  • 단순히 애플리케이션의 행위를 분리할 뿐인 서비스라면 값비싼 함수 호출에 불과하며, 아키텍처 관점에서 꼭 중요하다고 볼 수 없다.
  • 결국 서비스는 프로세스나 플랫폼 경계를 가로지르는 함수 호출에 지나지 않는다.
  • 아키텍처적으로 중요한 서비스도 있지만, 중요하지 않은 서비스도 존재하는데, 이 장에서 우리가 관심을 가지는 서비스는 전자다.

 

서비스의 이점?

결합 분리의 오류

  • 시스템을 서비스로 분리함으로써 얻는 큰 이점 하나는 서비스 사이의 결합이 확실히 분리된다는 것
  • 어쨌든 각 서비스는 서로 다른 프로세스와 프로세서에서 실행되므로, 개별 변수 수준에서는 각각 결합이 분리된다.
  • 하지만 프로세스 내의 네트워크 상의 공유 자원 때문에 결합될 가능성이 여전히 존재하며, 서로 공유되는 데이터에 의해 강력하게 결합되어 버린다.
    • 예를 들어 서비스 사이를 오가는 데이터 레코드에 새로운 필드가 추가된다면, 이 필드를 사용하는 모든 서비스는 변경되어야 함
    • 따라서 서비스들은 이 데이터 레코드에 강하게 결합되고, 서비스들 사이는 서로 간접적으로 결합되어 버림

개발 및 배포 독립성의 오류

  • 서비스를 사용함에 따라서 예측되는 또 다른 이점은 서비스를 소유 운영한다는 점이다.
  • 그래서 데브옵스 전략의 일환으로 전담팀에서 각 서비스를 작성하고, 유지보수하며, 운영하는 책임을 질 수 있다.
    • 이러한 개발 및 배포 독립성은 확장 가능한 것으로 간주됨
    • 대규모 엔터프라이즈 시스템을 독립적으로 개발 및 배포 가능한 서비스들을 이용하여 만들 수 있다고 믿는다.
    • 시스템의 개발, 유지보수, 운영 또는 비슷한 수의 독립적인 팀 단위로 분할할 수 있다고 여김
  • 이러한 믿음에도 어느 정도 일리가 있지만, 극히 일부일 뿐임
    • 첫째로, 대규모 엔터프라이즈 시스템은 서비스 기반 시스템 이외에도, 모노리틱 시스템이나 컴포넌트 기반 시스템으로도 구축할 수 있다는 사실은 역사적으로 증명되어 옴
    • 따라서 서비스는 확장 가능한 시스템을 구축하는 유일한 선택지가 아니다.
    • 둘째로, ‘결합 분리의 오류’에 따르면 서비스라고 해서 항상 독립적으로 개발하고, 배포하며, 운영할 수 있는 것은 아니다.
    • 데이터나 행위에서 어느 정도 결합되어 있다면 결합된 정도에 맞게 개발, 배포, 운영을 조정해야 함

 

야옹이 문제

앞의 두 가지의 오류에 대한 예를 택시 통합 시스템으로 다시 살펴봄

  • 택시 통합 시스템은 해당 도시에서 운영되는 많은 택시 업체를 알고 있고, 고객은 승차 요청을 할 수 있음
  • 고객은 승차 시간, 비용, 고급 택시 여부, 운전사 경력 등 다양한 기준에 따라 택시를 선택할 수 있음
  • 확장 가능한 시스템을 위해 수많은 작은 마이크로 서비스를 기반으로 구축하기로 결정함
  • 개발팀 직원을 소규모 팀으로 세분화했고, 각 팀이 팀 규모에 맞게 적당한 수의 서비스를 개발, 유지보수, 운영하는 책임을 지도록 함

  • 위 그림은 가상의 아키텍트가 서비스를 배치하여 이 애플리케이션을 어떻게 구현했는지 보여줌
    • TaxiUI 서비스는 고객을 담당하며, 고객은 모바일 기기를 이용해서 택시를 호출함
    • TaxiFinder 서비스는 여러 TaxiSupplier의 현황을 검토하여 사용자에게 적합한 택시 후보들을 선별함
    • TaxiFinder 서비스는 해당 사용자에게 할당된 단기 데이터 레코드에 후보 택시들의 정보를 저장함
    • TaxiSelector 서비스는 사용자가 지정한 비용, 시간, 고급 여부 등의 조건을 기초로 후보 택시 중에서 적합한 택시를 선택함
    • TaxiSelector 서비스가 해당 택시를 TaxiDispatcher 서비스로 전달하면, TaxiDispatcher 서비스는 배차를 지시함
  • 어느 날, 마케팅 부서에서 도시에 야옹이를 배달하는 서비스를 제공하겠다는 요구사항이 들어옴
    • 사용자는 자신의 집이나 사무실로 야옹이를 배달해 달라고 주문할 수 있음
    • 회사는 도시 전역에 야옹이를 태울 다수의 승차 지점을 설정해야 할 것임
    • 야옹이가 배달 주문이 오면, 근처의 택시가 선택되고, 승차 지점 중 한 곳에서 야옹이를 태운 후, 올바른 야옹이를 배달함
    • 택시 업체 한 곳이 이 프로그램에 참여하기로 협의했고, 다른 업체도 뒤따를 것이고, 거부하는 업체로 분명 있을 것임
    • 어떤 운전자는 고양이 알레르기가 있을 수 있어서 해당 운전자는 이 서비스에서 제외되어야 함
    • 또한 배차를 신청한 고객이 알러지가 있다고 밝힌 경우라면 지난 3일 사이에 야옹이를 배달했던 차량은 배차되지 않아야 함
  • 이 기능을 구현하려면 이들 서비스 전부를 변경해야 한다.
    • 의심의 여지없이 야옹이 배달 기능을 추가하려면 개발과 배포 전략을 매우 신중하게 조정해야 함
    • 다시 말해 이 서비스들은 모두 결합되어 있어서 독립적으로 개발하고 배포하거나 유지될 수 없다.
  • 이게 바로 횡단 관심사가 지닌 문제다.
    • 모든 소프트웨어 시스템은 서비스 지향이든 아니든 이 문제에 직면하기 마련이다.
    • 위의 서비스 다이어그램에서 묘사된 것과 같은 종류의 기능적 분해는 새로운 기능이 기능적 행위를 횡단하는 상황에 매우 취약하다.

 

객체가 구출하다.

  • 컴포넌트 기반 아키텍처에서는 다형적으로 확장할 수 있는 클래스 집합을 생성해 새로운 기능을 처리하도록 함을 알 수 있다.
  • 위 다이어그램 클래스들은 이전 서비스와 거의 일치하지만 경계와 의존성 규칙을 준수한다는 점을 주목해야 함
  • 하지만 배차에 특화된 로직은 Rides 컴포넌트로 추출되고, 야옹이에 대한 신규 기능은 Kittens 컴포넌트로 추출됐다.
  • 이 두 컴포넌트는 기존 컴포넌트들에 있는 추상 기반 클래스를 템플릿 메서드나 전략 패턴들을 이용해서 오버라이드한다.
  • 두 개의 신규 컴포넌트인 Rides와 Kitten가 의존성 규칙을 준수함에 주목해야 함
  • 또한 이 기능들을 구현하는 클래스들은 UI의 제어하에 팩토리가 생성한다는 점에도 주목해야 함
  • 이 전략을 따르더라도 야옹이 기능을 구현하려면 TaxiUI는 어쩔 수 없이 변경해야만 하지만 그 외의 것들은 변경할 필요가 없다.
  • 대신 야옹이 기능을 구현한 새로운 jar을 시스템에 추가하고, 런타임에 동적으로 로드하면 된다.
  • 따라서 야옹이 기능은 결합이 분리되며, 독립적으로 개발하여 배포할 수 있다.

 

컴포넌트 기반 서비스

  • 서비스가 반드시 소규모 단일체여야 할 이유는 없고 SOLID 원칙대로 설계할 수 있으며 컴포넌트 구조를 갖출 수도 있다.
  • 이를 통해 서비스 내의 기존 컴포넌트들을 변경하지 않고도 새로운 컴포넌트를 추가할 수 있다.
  • 자바의 경우, 서비스를 하나 이상의 jar 파일에 포함되는 추상 클래스들의 집합이라고 생각해라.
  • 새로운 기능 추가 및 확장은 새로운 jar를 만드는데 이때 기존 jar에 정의된 추상 클래스들을 확장해서 만들어진다.
  • 그러면 새로운 기능 배포는 서비스를 재배포하는 문제가 아니라, 단순히 새로운 jar파일을 추가하는 문제가 된다.(개방 폐쇄 원칙 준수)

  • 서비스들의 존재는 이전과 달라진 게 없지만, 각 서비스의 내부는 자신만의 컴포넌트 설계로 되어 있어서 파생 클래스를 만드는 방식으로 신규 기능을 추가할 수 있다.(파생 클래스는 각 컴포넌트 내부에 위치)

 

횡단 관심사

  • 아키텍처 경계가 서비스 사이에 있지 않고, 오히려 서비스를 관통하며, 컴포넌트 간위로 분할한다.
  • 모든 주요 시스템이 직면하는 횡단 관심사를 처리하려면, 서비스 내부는 의존성 규칙도 준수하는 컴포넌트 아키텍처로 설계해야 한다.
  • 이 서비스들은 아키텍처 경계를 정의하지 않으며, 아키텍처 경계를 정의하는 것은 서비스 내의 컴포넌트임

 

결론

  • 서비스는 시스템의 확장성과 개발 가능성 측면에서 유용하지만, 아키텍처적으로 그리 중요한 요소는 아님
  • 시스템의 아키텍처는 시스템 내부에 그어진 경계와 경계를 넘나드는 의존성에 의해 정의된다.
  • 시스템의 구성 요소가 통신하고 실행되는 물리적인 메커니즘에 의해 아키텍처가 정의되는 것은 아니다.

 

 

28장. 테스트 경계


  • 테스트는 시스템의 일부이며, 시스템의 나머지 요소가 아키텍처에 관여하는 것과 동등하게 테스트도 아키텍처에도 관여한다.
  • 어떤 면에서는 평범하게 관여하고, 다른 면에서는 독톡 하게 관여한다.

 

시스템 컴포넌트인 테스트

  • 아키텍처 관점에서 테스트는 세부적이며 구체적인 것으로, 태생적으로 의존성 규칙을 따른다.(의존성이 항상 테스트 대상이 되는 코드를 향함)
  • 시스템 내부의 어떤 것도 테스트에는 의존하지 않으며, 테스트는 시스템의 컴포넌트를 향해, 항상 원의 안쪽으로 의존한다.
  • 테스트 독립적으로 배포 가능하며 고립되어 있고 테스트가 시스템 운영에 꼭 필요하지 않고 어떤 사용자도 테스트에 의존하지 않는다.
  • 테스트의 역할은 운영이 아니라 개발을 지원하는 데 있으며, 그렇다고 해서 테스트가 시스템 컴포넌트가 아니라는 뜻은 아님

 

테스트를 고려한 설계

  • 테스트가 시스템의 설계와 잘 통합되지 않으면, 테스트는 깨지기 쉬워지고, 시스템은 뻣뻣해져서 변경하기 어려워진다.
  • 시스템에 강하게 결합된 테스트라면 시스템이 변경될 때 함께 변경되어야만 한다.
    • 시스템 컴포넌트에서 생긴 아주 사소한 변경도, 이와 결합된 수많은 테스트를 망가뜨릴 수 있음
    • 시스템의 공통 컴포넌트가 변경되면 수백, 수천 개의 테스트가 망가지며, 이는 ‘깨지기 쉬운 테스트 문제’로 알려져 있음
    • 깨지기 쉬운 테스트는 시스템을 뻣뻣하게 만드는 부작용을 낳을 때가 많은데, 시스템에 가한 간단한 변경이 대량의 테스트 실패로 이어진다면, 개발자는 변경을 하지 않을 것임

 

테스트 API

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

 

구조적 결합

  • 구조적 결합은 테스트 결합 중 가장 강하며, 가장 은밀하게 퍼져나가는 유형이다.
  • 모든 상용 클래스에 테스트 클래스가 각각 존재하고 메서드에도 각각 존재하는 테스트 스위트는 강하게 결합된다.
  • 상용 클래스나 메서드 중 하나라도 변경되면 딸려 있는 다수의 테스트가 변경되어야 하고, 결과적으로 테스트는 깨지기 쉬워 상용 코드를 뻣뻣하게 만든다.
  • 구조적 결합이 강하면 필수적인 진화 과정을 방해할 뿐만 아니라, 상용 코드의 범용성과 유연성이 충분히 좋아지지 못한다.

 

결론

  • 테스트는 시스템 외부에 있지 않으며 오히려 시스템의 일부다.
  • 따라서 테스트에서 기대하는 안정성과 회귀의 이점을 얻을 수 있으려면 테스트는 잘 설계되어야 한다.
  • 테스트를 시스템의 일부로 설계하지 않으면 테스트는 깨지기 쉽고 유지보수하기 어려워지는 경향이 있다.
  • 이러한 테스트는 유지보수하기가 힘들기에 결국 방바닥의 휴지처럼 버려지는 최후를 맡는다.

 


모든 내용은 [클린 아키텍처] 서적의 정리한 내용이며, 망나니 개발자님의 블로그에서 정리 방법을 참고했습니다.

 

클린 아키텍처: 소프트웨어 구조와 설계의 원칙 | 로버트 C. 마틴 - 교보문고

클린 아키텍처: 소프트웨어 구조와 설계의 원칙 | 살아있는 전설이 들려주는 실용적인 소프트웨어 아키텍처 원칙 소프트웨어 아키텍처의 보편 원칙을 적용하면 소프트웨어 수명 전반에서 개발

product.kyobobook.co.kr

 

[개발서적] 클린 아키텍처 4부 컴포넌트 원칙 - 내용 정리 및 요약

이번에는 로버트 C 마틴의 클린 아키텍처를 읽은 내용을 정리해보도록 하겠습니다. 개인적인 설명은 기울임으로 표시해두었으니, 읽으면서 참고하시면 될 것 같습니다. 0. 서론 [ 도입 ] SOLID 원

mangkyu.tistory.com

 

댓글