[CA] 아키텍처 - 2

rbw·2023년 7월 28일
0

TIL

목록 보기
85/99

소리치는 아키텍처

만약 주택의 설계도를 본다면, 주방은 어디에 있고 현관문은 어디에 있고 등이 표현되어 있을 것이다. 그렇다면 이 아키텍처는 나는 주방이다 라고 소리치는것과 같다. 도서관의 설계도도 마찬가지로 볼 수 있다.

아키텍처는 위처럼 시스템을 얘기해야 한다. 시스템에 적용되는 프레임워크에 대해서 얘기를 하면 안 된다.

만약 헬스 케어 시스템을 구성하고 있다면 새로 들어온 프로그래머가 봐도 이건 헬스 케어 시스템이라고 생각이 들게끔 설계를 해야한다.

유스케이스를 중심으로 구현된 아키텍처가 위를 지킬 수 있다

클린 아키텍처

먼저 여러 시스템 아키텍처를 소개하는데 이것들의 공통적인 목표는 관심사의 분리 라는것을 설명하고있다. 소프트웨어를 계층으로 분리하여 이 목표를 달성할 수 있다. 각 아키텍처는 최소한 업무 규칙을 위한 계층 하나와, 사용자와 시스템 인터페이스를 위한 계층 하나를 포함한다.

또 다음과 같은 특징들을 가지고 있다.

  • 프레임워크 독립성: 아키텍처는 프레임워크의 존재 여부에 의존하지 않아, 이를 도구로 사용할 수 있으며, 프레임워크가 지닌 제약사항 안으로 시스템을 욱여 넣도록 강제하지 않는다.
  • 테스트 용이성: 업무규칙은 UI, DB, Server 등 외부 요인이 없이도 테스트할 수 있다.
  • UI 독립성: 시스템의 나머지 부분을 변경하지 않고도 UI를 쉽게 변경할 수 있다.
  • DB 독립성: 업무규칙은 DB와 결합되지 않아, 다른 DB로 교체가 가능하다.
  • 모든 외부 에이전시에 대한 독립성: 실제로 업무 규칙은 외부 세계와의 인터페이스에 대해 알지 못한다.

그러고 클린 아키텍처의 유명한 다이어그램이 나오는데, 여기서 중요한 점은 의존성 규칙이다.

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

내부의 원에 속한 요소는 외부의 원에 속한 어떤 것도 알지 못한다.

경계를 횡단할 때는 의존성 역전 원칙을 사용해서 해결을 해야 한다. 제어흐름이 경계를 가로지르는 지점에서 소스 코드 의존성을 제어흐름과는 반대로 만들 수 있다.

그리고 경계를 가로지르는 데이터는 항상 내부 원에서 사용하기에 편리한 형태로 전달이 되어야 한다고 하심.

프레젠터와 험블 객체

humble: 보잘것없는, 뷰는 보잘것없다 라고 하심.

험블 객체 패턴은 테스트하기 어려운 행위와 테스트하기 쉬운 행위를 단위 테스트 작성자가 분리하기 쉽게 하는 방법으로 고안되었습니다. 테스트하기 어려운 행위를 모두 험블 객체로 옮기고, 나머지 모듈에는 테스트하기 쉬운 행위들을 옮깁니다.

GUI의 경우 테스트가 어렵지만 GUI에서 수행하는 행위의 대다수는 쉽게 테스트할 수 있습니다. 그래서 뷰는 험블객체이고 테스트하기 어렵습니다.

반면 프레젠터는, 테스트하기 쉬운 객체입니다. 프레젠터의 역할은 앱으로부터 데이터를 받아 화면에 표현할 수 있는 포맷으로 만드는 것입니다. 이를 통해 뷰는 데이터를 화면으로 전달하는 간단한 일만 처리하도록 만듭니다. 프레젠터는 Date 객체를 전달 받을 수 있으며, 이를 적절한 포맷의 문자열로 만들고, 뷰 모델로 전달합니다.

특정 조건에 따라서 데이터가 달라지는 경우도, 뷰 모델에 불 타입 플래그를 두고 프레젠터에서 적절하게 설정합니다.

각 아키텍처 경계에서 험블 객체 패턴을 발견할 수 있습니다. 이 패턴을 경계에서 잘 사용한다면 전체 시스템의 테스트 용이성을 크게 높일 수 있습니다.

부분적 경계

아키텍처의 경계를 완벽하게 만드는 일은 비용이 많이 듭니다. 쌍방향의 다형적 바운더리 인터페이스, 인풋과 아웃풋을 위한 데이터 구조, 두 영역을 독립적으로 컴파일하고 배포할 수 있는 컴포넌트로 격리하는 데 필요한 의존성 관리 등 많은 노력이 필요합니다.

이러한 선행적인 설계를 좋게 보지 않는 사람도 많이 있습니다. 이런 경우에 부분적 경계를 구현해볼 수 있습니다.

이를 생성하는 방법 중 하나는 독립적으로 컴파일하고 배포할 수 있는 컴포넌트를 만들기 위한 작업은 모두 수행한 후 단일 컴포넌트에 모두 모아 두는 것입니다.

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

다른 방법으로는, 추후 완벽한 형태로 확장가능한 공간을 확보하고자 할 때 활용할 수 있는 패턴이 바로 전략(Strategy) 패턴입니다.

ServiceBoundary 인터페이스는 클라이언트가 사용하며 ServiceImpl 클래스가 구현합니다.

다음으로는 퍼사드(Facade) 패턴이 있습니다. Facade 클래스에는 모든 서비스 클래스를 메서드 형태로 정의하고, 서비스 호출이 발생하면 해당 서비스 클래스로 호출을 전달합니다. 클라이언트는 이들 서비스 클래스에 직접 접근할 수 없습니다.

하지만 클라이언트가 모든 서비스 클래스에 대해 추이 종속성을 가지게 되는 점이 있습니다.

아키텍처 경계를 부분적으로 구현하는 방법을 살펴보았는데, 다 장단점이 있으므로 어느 것을 선택하는 것도 아키텍트의 역할입니다.

계층과 경계

이 장에서는 움퍼스 게임을 예시로 들어서 아키텍처의 경계에 대해서 설명합니다.

게임규칙(고수준)은 세부사항을 알지 않기를 바랍니다. 그래서 추상 컴포넌트를 정의하고 구현은 추상 컴포넌트의 위나 아래의 컴포넌트가 구현합니다. 여기서 API는 구현하는 쪽이 아닌 사용하는 쪽에 정의되고 소속됩니다.

위 게임은 조금 간단한 예시였지만, 만약 게임규칙보다 높은 수준이 있다면 점점 흐름은 복잡해집니다. 게임규칙도 세부적으로 나눌 수 있습니다. 이동에 관한 규칙, 플레이어의 상태에 관한 규칙 등으로 말입니다. 하지만 이것이 아키텍처 경계인지는 아직 애매할 수 있습니다.

만약 여기서 플레이어 상태를 관리하는 부분을 마이크로서비스로 제공한다면 이는 완벽한 형태의 아키텍처 경계가 존재한다고 볼 수 있습니다.

다른 요소로 의존을 많이 하게 된다면 이는 경계가 생겼다고 보는건지,,? 이 경계에 대한 내용은 살짝 어렵게 느껴짐,,

여기서 설명하고자 했던 것은 아키텍처 경계는 어디에서나 존재한다는 것을 알려주기 위함이라고 하십니다. 그리고 아키텍트는 미래를 내다봐야하고, 현명하게 추측해야 합니다. 그리고 완벽하게 구현할 경계는 무엇인지, 부분적으로 구현할 경계와 무시할 경계는 무엇인지를 결정해야합니다.

하지만 프로젝트 초반에는 이러한 부분을 알기가 힘듭니다. 그래서 지켜보라고 하심. 시스템의 발전에 따라 주의를 기울여야 한다고 합니다.

아키텍트의 목표는 경계의 구현 비용이 그걸 무시해서 생기는 비용보다 적어지는 그 변곡점에서 경계를 구현하는 것입니다

메인 컴포넌트

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

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

의존성 주입 프레임워크를 이용하는 일은 이 메인 컴포넌트에서 이뤄져야 합니다.

움퍼스 예제로 설명을 하는데, 여기서 게임을 생성할 때 클래스를 문자열로 넘기는 부분이 있습니다. 이렇게 하여 클래스에서 변경이 생겨도 메인을 재컴파일/재배포하지 않아도 되게 하기 위함입니다.

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

메인을 앱의 플러그인이라고 생각하면 됩니다. 초기 조건과 설정을 구성하고 외부 자원을 모두 수집한 후, 제어권을 앱의 고수준 정책으로 넘기는 플러그인입니다. 따라서 메인 컴포넌트를 앱의 설정별로 두어 둘 이상의 메인 컴포넌트를 만들 수도 있습니다. 개발용, 테스트용, 다른용도 등등

'크고 작은 모든' 서비스들

이번 장에서는 서비스, 새 기능 추가에 관한 내용입니다.

먼저 서비스를 사용하는 것은 본질적으로 아키텍처에 해당하지 않습니다. 시스템의 아키텍처는 의존성 규칙을 준수하며 고수준 정책을 저수준의 세부사항으로부터 분리하는 경계에 의해 정의됩니다.

따라서 앱의 행위를 분리할 뿐인 서비스라면 값비싼 함수 호출이라고 볼 수 있습니다. 서비스 자체가 아키텍처를 정의하지는 않습니다.

야옹이 문제로 예시를 들어서 설명을 하는데, 새로운 기능을 추가하려면 서비스 다이어그램에 나와있는 모든 부분을 변경해야 합니다. 이 문제를 횡단 관심사가 지닌 문제라고 합니다. 모든 소프트웨어 시스템은 서비스 지향이든 아니든 이 문제에 직면하게 마련이라고 합니다.

컴포넌트 기반 아키텍처에서는 이를 해결할 때 클래스 집합을 생성해 새로운 기능을 처리하여 해결하는것을 알 수 있습니다. 배차에 특화된 부분은 Rides 컴포넌트로 추출되고, 야옹이에 대한 신규 기능은 Kittens 컴포넌트에 들어갔습니다.

그리고 기능들을 구현하는 클래스들은 UI 제어하에 팩토리가 생성한다는 점입니다. 야옹이 기능을 구현하려면 어쩔 수 없이 Taxi UI는 변경해야 하지만 나머지 것들은 변경할 필요가 없습니다. 대신 야옹이 기능을 구현한 새로운 jar 파일이나 젬, DLL을 시스템에 추가하고 런타임에 동적으로 로드하면 됩니다.

따라서 야옹이 기능은 결합이 분리되며 독립적으로 개발하여 배포할 수 있습니다.

이제 서비스는 어떻게 이 부분을 해결할지인데 컴포넌트를 활용하면 됩니다. 서비스도 SOLID 원칙대로 구현이 가능하며, 컴포넌트 구조를 갖출 수도 있습니다. 자바로 예를 들어주셨는데, 서비스를 하나 이상의 jar 파일에 포함되는 추상 클래스들의 집합이라고 생각하고, 새로운 기능 추가는 새로운 jar 파일로 만듭니다. 이때 새로운 jar 파일을 구성하는 클래스들은 기존 jar 파일에 정의된 추상 클래스들을 확장해서 만들어집니다.

그러면 새로운 기능 배포는 서비스를 로드하는 경로에 새로운 jar 파일을 추가하는 문제로 바뀝니다. OCP를 준수하게 됩니다. 각 서비스의 내부는 컴포넌트 설계로 되어 있어서 파생 클래스로 만드는 방식으로 신규 기능을 추가할 수 있습니다.

시스템의 아키텍처는 시스템 내부에 그어진 경계와 경계를 넘나드는 의존성에 의해 정의됩니다

테스트 경계

아키텍처 관점에서 모든 테스트는 동일하다. 테스트는 태생적으로 의존성 규칙을 따른다. 테스트는 세부적이며 구체적이므로 의존성은 항상 테스트 대상이 되는 코드를 향한다(구체적 -> 추상적) 이런 부분을 생각한다면 아키텍처에서 제일 바깥 원으로 생각할 수 있다.

실제로 시스템의 내부의 어떤 것도 테스트를 의존하지 않으며, 테스트는 시스템의 컴포넌트를 향해, 안쪽을 향해 항상 의존한다.

또한 테스트는 독립적으로 배포가 가능하다. 상용 시스템에는 테스트를 보통 배포하지 않지만, 테스트 시스템에는 배포한다. 그리고 테스트는 시스템 컴포넌트 중에서 가장 고립되어 있다. 테스트가 시스템 운영에 꼭 필요하지는 않다.

테스트의 역할은 운영이 아니라 개발을 지원하는 데 있다

하지만 테스트의 고립성 때문에 개발자는 종종 시스템의 설계 범위 밖에 있다고 여긴다. 이는 잘못된 관점이다. 시스템의 설계와 테스트가 잘 통합되지 않으면 테스트는 깨지기 쉬워지고, 시스템은 뻣뻣해져서 변경하기가 어려워진다.

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

GUI를 사용하여 업무 규칙을 검사하는 예시를 들었는데, 페이지 구조가 살짝 바뀌어도 테스트는 다 망가질 수 있다. 이런 문제를 방지하기 위해, 테스트를 고려해서 설계해야 한다.

변동성이 있는 것에 의존하지 말라

소프트웨어 설계 첫 번째 규칙이다. GUI는 변동성이 크다. 따라서 GUI에 의존하는 테스트코드는 좋지 않다. 따라서 시스템을 설계할 때 GUI를 사용하지 않고 업무 규칙을 테스트할 수 있게 해야 한다.

위 목표를 달성하려면 테스트가 모든 업무 규칙을 검증하는데 사용할 수 있도록 특화된 API를 만들면 된다. 이 API는 보안 제약사항을 무시할 수 있고, 값비싼 자원은 건너뛰고 시스템을 테스트 가능한 특정 상태로 강제하는 강력한 힘을 지녀야만 한다.

이 API는 사용자 인터페이스가 사용하는 인터랙터와 인터페이스 어댑터들의 상위 집합이 될 거라고 하심. 테스트 API는 테스트를 앱으로부터 분리할 목적으로 사용하고, 테스트를 UI에서 분리하는 것만이 아닌,

테스트 구조를 앱 구조로부터 결합을 분리하는게 목표다, 테스트 API의 역할은 앱 구조를 테스트로부터 숨기는 데 있다

테스트는 시스템 외부가 아닌 오히려 시스템의 일부임을 명심해야 한다.

profile
hi there 👋

2개의 댓글

comment-user-thumbnail
2023년 7월 28일

이런 유용한 정보를 나눠주셔서 감사합니다.

1개의 답글