Clean Architecture - Architecture

versatile309·2021년 7월 22일
0

개발 책 읽기

목록 보기
14/18

1. 아키텍처(Architecture)란?

아키텍처란 시스템을 구축했던 사람들이 만들어낸 시스템의 형태다. 그 모양은 시스템을 컴포넌트로 분할하는 방법, 분할된 컴포넌트를 배치하는 방법, 컴포넌트가 서로 의사소통하는 방식에 따라 정해진다.

좋은 아키텍트

좋은 아키텍트는 세부사항을 정책으로부터 신중하게 가려내고, 정책이 세부사항과 결합되지 않도록 엄격하게 분리한다. 이를 통해 세부사항에 대한 결정을 가능한 한 오랫동안 미룰 수 있게 된다.

2. 독립성

유스케이스

  • 아키텍처는 시스템의 의도를 지원해야 한다.
  • 아키텍처에서 유스케이스는 최우선이다.
  • 좋은 아키텍처에서 유스케이스는 한눈에 드러나며 최상위 수준에서 알아볼 수 있어야 한다.

운영

  • 아키텍처는 요구되는 운영 작업을 허용할 수 있는 형태로 구조화되어야 한다.

개발

  • 팀 별로 독립적으로 행동하기 편한 아키텍처를 확보해야 하며, 이를 위해 잘 격리되어 독립적으로 개발 가능한 컴포넌트 단위로 시스템을 분할할 수 있어야 한다.

배포

  • 목표는 즉각적인 배포다.
  • 마스터 컴포넌트는 시스템 전체를 하나로 묶고, 각 컴포넌트를 올바르게 구동하고 통합하고 관리해야 한다.

계층과 유스케이스의 결합 분리

시스템을 독립적으로 변경되는 것들을 기준으로 수평적 계층으로 분할하면서 동시에 해당 계층을 가로지르는 수직적인 유스케이스로 시스템을 분할할 수 있다.
이를 통해 기존 요소에 지장을 주지 않고도 새로운 유스케이스를 계속해서 추가할 수 있게 된다.

중복

  • 진짜 중복: 인스턴스가 변경되면 동일한 변경을 그 인스턴스의 모든 복사본에 반드시 적용해야 하는 중복.
  • 우발적인 중복: 중복으로 보이지만 이후 각자의 경로로 변화하는 중복.

우발적인 중복인 코드를 통합하게 되면 나중에 다시 분리하느라 큰 수고를 감수해야 한다.
특히 뷰모델!!!

3. 경계: 선 긋기

경계선을 그리려면 먼저 시스템을 컴포넌트 단위로 분할해야 한다. 일부 컴포넌트는 핵심 업무 규칙에 해당한다. 나머지 컴포넌트들의 의존이 핵심 업무를 향하도록 배치한다.
의존성 화살표는 저수준 세부사항에서 고수준의 추상화를 향하도록 배치된다.

BloC 아키텍처에서 Bloc은 핵심 업무 규칙에 해당하면서 동시에 상태를 통해 UI를 컨트롤하는 역할을 한다. 이 부분을 한 블록에서 하지 않고 핵심 업무 규칙과 UI 컨트롤 부분을 별도의 블록에서 컨트롤한다면 좀 더 경계를 나눌 수 있지 않을까?

블록에서 repository 구현체를 직접 주입받는 부분도 인터페이스를 넣어야 겠다. dart에는 인터페이스가 없으니까 추상화 클래스를 사용해야 할듯.

경계의 종류

  • 단일체: 동일 프로세서 내에서 규칙에 따라 소스 수준으로 분리되어 있는 형태. 함께 배포된다.
  • 배포형: 분리 배포 가능한 수준의 형태. 이 외에는 단일체와 동일하다.
  • 로컬 프로세스: 독립된 주소 공간에서 컴포넌트가 실행되는 형태.
  • 서비스: 물리적 위치에 구애받지 않게 분리된 형태.

4. 정책

소프트웨어 시스템이란 정책을 기술한 것이다. 실제로 컴퓨터 프로그램의 핵심부는 이게 전부다.

5. 업무규칙

엔티티(Entity)

핵심 업무 규칙과 핵심 업무데이터의 결합. 업무에 핵심적인 개념을 구현한 부분으로 시스템에 대해 독립적이다.

유스케이스(Use Case)

필요한 입력, 처리단계, 보여줄 출력을 기술한 것으로 시스템이 사용되는 방법을 설명한다. 엔티티와 달리 애플리케이션에 특화된 업무규칙을 설명한다. 요청과 응답에 단순한 데이터 구조를 사용해 입출력 대상에 대해 독립적이다.

6. 클린 아키텍처(Clean Architecture)

여러가지 아키텍처들이 존재하지만 그 내용은 상당히 비슷한데 "관심사의 분리"를 목표로 한다는 것이다.

  • 프레임워크 독립성
  • 테스트 용이성
  • UI 독립성
  • 데이터베이스 독립성
  • 모든 외부 에이전시에 대한 독립성

클린 아키텍처는 이전에 이야기 되어 왔던 아키텍처 전부를 실행 가능한 하나의 아이디어로 통합하려 시도한 아키텍처이다.

클린 아키텍처 구조

안으로 들어갈수록 고수준의 소프트웨어가 된다.
소스 코드 의존성은 반드시 안쪽으로, 고수준의 정책을 향해야 한다.

  • 엔티티: 핵심 규칙으로 외부에 의해 변화될 가능성이 매우 낮다.
  • 유스케이스: 애플리케이션에 특화된 업무 규칙을 포함하며 엔티티에게 영향을 주면 안되고, 외부 요소에게 영향을 받으면 안된다.
  • 인터페이스 어댑터: 외부(UI, DB, Web, etc)와 내부(Use Case, Entity)사이에서 데이터를 각각에게 편안한 형식으로 변환한다.
  • 프레임워크와 드라이버: 모든 세부사항이 위치한 곳으로 최대한 내부에 영향을 주지 않도록 격리시켜야 한다.

7. 경계의 횡단

내부에서 사용할 인터페이스를 만들고 구현을 외부에게 맡기는 것으로 의존성을 역전시켜 경계간의 소통을 진행할 수 있다.
경계를 가로질러 데이터를 전달할 때, 데이터는 항상 내부의 원에서 사용하기에 가장 편리한 형태를 가져야만 한다.

비슷한 데이터 클래스가 많이 생겨서 중복을 줄이고 싶은 압박이 발생하는게 문제...

8. 험블 객체 패턴(Humble Object Pattern)

험블 객체 패턴은 테스트하기 어려운 행위와 테스트하기 쉬운 행위를 단위 테스트 작성자가 분리하기 슆게 하는 방법으로 고안되었다. 앵위들을 두 개의 모듈 또는 클래스로 나눈 후 가장 기본적인 본질은 남기고, 테스트하기 어려운 행위를 모두 험블 객체로 옮긴다.
이렇게 하는 것으로 테스트 시 험블 객체를 mocking해서 쉽게 테스트할 수 있다.

9. 부분적 경계

완벽한 경계를 만드는 데는 많은 비용이 든다. 쌍방향 인터페이스 구현 비용 때문에 고민이 될 때 부분적 경계를 생각해볼 수 있다.

  1. 마지막 단계를 건너뛰기
    소스 코드 자체는 쌍방향으로 구현하되 컴포넌트 독립을 포기. 버전관리 등 분리된 컴포넌트에서 발생할 수 있는 부담이 줄어든다.
  2. 일차원 경계
    일차원적으로 인터페이스를 사용(Interface and InterfaceImpl). 단방향으로만 의존성 독립이 보장되므로 비밀통로가 생길 위험이 있다.
    controller, presenter, view model 등이 view 사이에서 한쪽이 다른 한쪽의 의존하는 상태면 이 상황이 되는 것 같다.
    class Home {
    interface Presenter(View view) {}
    interface View {}
    }
    이런식으로 골격을 짜 놓고 각각 구현을 시켜서 경계를 만든 적이 있는데 이건 일차원인건가...?
  3. 퍼사드
    상위 컴포넌트가 퍼사드에만 의존하는 대신 퍼사드 안에 여러 서비스들이 의존되어져 있게 만드는 구조. 의존성 역전이 되어 있지 않기 때문에 상위 컴포넌트가 퍼사드에 종속되게 되므로 위험하다.

10. 계층과 경계

아키텍처의 경계는 어디에나 존재할 수 있다. 문제는 이 경계를 제대로 구현하는 것에도 비용이 많이 들고, 무시 했다가 나중에 다시 추가하는 것에도 비용이 많이 든다는 것이다. 결국 결론은 경계가 필요할 수도 있는 부분에 주목하고, 경계가 필요할 것 같은 조짐이 보이는 때에 구현 비용과 무시 비용을 잘 가늠해 경계의 구현 비용이 그걸 무시해서 생기는 비용보다 적어지는 변곡점에서 경계를 구현해야 한다.
말은 쉽구나...ㅎ

11. 메인 컴포넌트

메인 컴포넌트는 나머지 컴포넌트를 생성, 조정, 관리하는 컴포넌트로 궁극적인 세부사항, 가장 낮은 수준의 정책이다. 메인을 플러그인처럼 사용해 설정별로 여러개를 두고 사용할 수 있다. (dev, staging, prod)

실제로 이전에 환경별로 main.dart 파일을 여러개 만든 적이 있다. 지금은 nest.js 개발을 하다가 다시 flutter로 돌아왔더니 .env 파일이 편해서 .env 파일을 환경별로 두고 사용 중이다. 어떤 env 파일을 읽을지를 계속 고쳐줘야 하는게 귀찮은데 이 부분만 분리해서 main.dart를 여러개 만들어도 괜찮을 것 같다.

12. Service

'서비스 지향' 아키텍처와 '마이크로서비스' 아키텍처는 그 자체만으로는 상호 결합의 분리와 개발/배포 독립성을 보장하지 않는다. (서비스를 기능적으로 분리하는 경우 새로운 횡단 관심사 추가에 취약하다.)
단 서비스를 단일 컴포넌트 혹은 경계로 분리된 다수의 컴포넌트로 구성하는 것으로 위의 것들을 보장할 수 있다.

13. 테스트 경계

테스트는 컴포넌트 중 가장 고립되어 있다. 이 때문에 테스트가 시스템 설계 범위의 밖에 있다고 여겨지기 쉽다. 테스트가 시스템의 설계와 잘 통합되지 않으면 테스트는 깨지기 쉬워지고, 시스템은 뻣뻣해져서 변경하기가 어려워진다. 시스템의 변경으로 인해 수 많은 테스트가 깨지게 된다면(Fragile Tests Problem) 개발자는 변경에 보수적이게 된다.
독립적으로 업무규칙을 검증하기 위해서 이에 특화된 API를 만들 수 있다. API는 애플리케이션의 구조를 테스트로부터 숨겨 서로 영항을 주지 않게 한다.

이렇게 하면 애플리케이션 자체를 테스트 할 수 없을 것 같은데... 딱 메인 비즈니스 로직을 위한 API인건가?

0개의 댓글