[Book] Clean Architecture

로버트 C.마틴의 <Clean Architecture> 정리

Clean Architecture

  • 로버트 C.마틴 지음

1부. 소개

1장. 설계와 아키텍처란?

설계와 아키텍처 사이에는 차이가 없다.

소프트웨어 아키텍처의 목표는 필요한 시스템을 만들고 유지보수하는 데 투입되는 인력을 최소화하는 데 있다.

2장. 두 가지 가치에 대한 이야기

모든 소프트웨어 시스템은 이해관계자에게 서로 다른 두 가지 가치를 제공하는데, 행위구조가 바로 그것이다.

행위


프로그래머는 이해관계자가 기능 명세서나 요구사항 문서를 구체화할 수 있도록 돕는다. 그리고 이해관계자의 기계가 이러한 요구사항을 만족하도록 코드를 작성한다. 하지만 이러한 활동이 프로그래머가 해야 할 일의 전부는 아니다.

아키텍처


소프트웨어는 부드러움을 지니도록 만들어졌다.소프트웨어를 만든 이유는 기계의 행위를 쉽게 변경할 수 있도록 하기 위해서다.

아키텍처가 특정 형태를 다른 형태보다 선호하면 할수록, 새로운 기능을 이 구조에 맞추는게 더 힘들어진다. 따라서 아키텍처는 형태에 독립적이어야 하고, 그럴수록 더 실용적이다.

아이젠하워 매트릭스


 긴급 O긴급 X
중요 O중요함
긴급함
중요함
긴급하지 않음
중요 X중요하지 않음
긴급함
중요하지 않음
긴급하지 않음

기능의 긴급성이 아닌 아키텍처의 중요성을 설득하는 일은 소프트웨어 개발팀이 마땅히 책임져야 한다.

아키텍처가 후순위가 되면 시스템을 개발하는 비용이 더 많이 들고, 일부 또는 전체 시스템에 변경을 가하는 일이 현실적으로 불가능해진다. 이러한 상황이 발생하도록 용납했다면, 소프트웨어 개발팀이 스스로 옳다고 믿는 가치를 위해 충분히 투쟁하지 않았다는 뜻이다.

2부. 벽돌부터 시작하기 : 프로그래밍 패러다임

구조적 프로그래밍 : 제어흐름의 직접적인 전환에 대한 규칙을 부과

객체 지향 프로그래밍 : 제어흐름의 간접적인 전환에 대한 규칙을 부과

함수형 프로그래밍 : 할당문에 대한 규칙을 부과

패러다임은 무엇을 해야 할지를 말하기보다는 무엇을 해서는 안 되는지를 말해준다.

4장. 구조적 프로그래밍

모든 프로그램을 순차, 분기, 반복이라는 세 가지 구조만으로 표현할 수 있다는 사실이 증명됐다. → 모듈을 증명 가능하게 하는 바로 그 제어 구조가 모든 프로그램을 만들 수 있는 제어 구조의 최소 집합과 동일하다.

구조적 프로그래밍을 통해 모듈을 증명 가능한 더 작은 단위로 재귀적으로 분해할 수 있게 됐고, 이는 결국 모듈을 기능적으로 분해할 수 있음을 뜻했다. 이 기법을 사용하면 대규모 시스템을 모듈과 컴포넌트로 나눌 수 있고, 더 나아가 모듈과 컴포넌트는 입증할 수 있는 아주 작은 기능들로 세분화할 수 있다.

구조적 프로그래밍은 프로그램을 증명 가능한 세부 기능 집합으로 재귀적으로 분해할 것을 강요한다. 그러고 나서 테스트를 통해 증명 가능한 세부 기능들이 거짓인지를 증명하려고 시도한다. 이처럼 거짓임을 증명하려는 테스트가 실패한다면, 이 기능들은 목표에 부합할 만큼은 충분히 참이라고 여기게 된다.

5장. 객체지향 프로그래밍

객체지향?

  • “데이터와 함수의 조합”, “실제 세계를 모델링하는 새로운 방법” : 모두 애매모호한 정의
  • 캡슐화, 상속, 다형성

1. 캡슐화

데이터와 함수를 쉽고 효과적으로 캡슐화하는 방법을 객체지향 언어가 제공. 캡슐화를 통해 데이터와 함수가 응집력 있게 구성된 집단을 서로 구분짓는 선을 그을 수 있음.

하지만, C 언어에서도 캡슐화 구현 가능. 클래스 선언과 정의를 구분하는 게 아예 불가능한 자바, C#에서 오히려 캡슐화가 더 심하게 훼손됨.

객체지향이 강력한 캡슐화에 의존한다는 정의는 받아들이기 힘듦

객체지향 프로그래밍은 프로그래머가 충분히 올바르게 행동함으로써 캡슐화된 데이터를 우회해서 사용하지 않을 거라는 믿음을 기반으로 함.

2. 상속

상속이란 단순히 어떤 변수와 함수를 하나의 유효 범위로 묶어서 재정의하는 일에 불과.

객체지향 언어가 완전히 새로운 개념을 만들지는 못했지만, 데이터 구조에 가면을 씌우는 일을 상당히 편리한 방식으로 제공했다고 볼 수 있음

3. 다형성

C언어를 통해서 구현 가능. 함수를 가리키는 포인터를 응용한 것이 다형성.

객체지향 언어가 다형성을 새롭게 만든 것이 아니지만, 다형성을 좀 더 안전하고 더욱 편리하게 사용할 수 있게 해준다.

객체지향 언어는 제어흐름을 간접적으로 전환하는 규칙을 부과한다

의존성 역전

다형성을 안전하고 편리하게 적용할 수 있는 메커니즘이 등장하기 전 소프트웨어는 min 함수가 고수준 함수를 호출하고, 고수준 함수가 다시 중간 수준 함수를 호출하며, 중간 수준 함수는 다시 저수준 함수를 호출한다. 이러한 호출 트리에서 소스 코드 의존성의 방향은 반드시 제어흐름(flow of control)을 따르게 된다. 제어 흐름은 시스템의 행위에 따라 결정되며, 소스코드 의존성은 제어흐름에 따라 결정된다.

다형성을 통해, 소스 코드 의존성(상속관계)이 제어흐름과는 반대가 된다. 이를 의존성 역전이라고 부른다. 객체지향 언어가 다형성을 안전하고 편리하게 제공한다는 사실은 소스코드 의존성을 어디에서든 역전시킬 수 있다는 뜻이다. 소스 코드 의존성이 제어흐름의 방향과 일치되도록 제한하지 않는다. 호출하는 모듈이든 호출 받는 모듈이든 관계없이 소프트웨어 아키텍트는 소스 코드 의존성을 원하는 방향으로 설정할 수 있다.

결론

소프트웨어 아키텍트 관점에서 객체지향이란 다형성을 이용해 전체 시스템의 모든 소스 코드 의존성에 대한 절대적인 제어 권한을 획득할 수 있는 능력.

의존성 역전을 통해 모듈의 독립성 확보 → 배포 독립성, 개발 독립성 확보

6장. 함수형 프로그래밍

자바 프로그램은 가변 변수를 사용. 가변변수는 프로그램 실행 중에 상태가 변할 수 있다. 이와 달리 함수형 언어에서 변수는 변경되지 않는다.

불변성과 아키텍처

변수의 가변성이 중요한 이유? 경합 조건, 교착상태 조건, 동시 업데이트 문제 모두 가변 변수로 인해 발생하기 때문. 동시성 애플리케이션에서 마주치는 모든 문제, 즉 다수의 스레드와 프로세스를 사용하는 애플리케이션에서 마주치는 모든 문제는 가변 변수가 없다면 절대로 생기지 않는다.

가변 컴포넌트와 불변 컴포넌트를 분리를 통해 불변성을 구현한다. 애플리케이션을 제대로 구조화하려면 변수를 변경하는 컴포넌트와 변경하지 않는 컴포넌트를 분리해야 한다.

현명한 아키텍트라면 가능한 한 많은 처리를 불변 컴포넌트로 옮겨야 하고, 가변 컴포넌트에서는 가능한 한 많은 코드를 빼내야 한다.

이벤트 소싱

이벤트 소싱은 상태가 아닌 트랜잭션을 저장하자는 전략이다. 상태가 필요해지면 단순히 상태의 시작점부터 모든 트랜잭션을 처리한다. 데이터 저장소에서 삭제되거나 변경되는 것이 하나도 없다. 애플리케이션은 CRUD가 아니라 그저 CR만 수행. 데이터 저장소에서 변경과 삭제가 전혀 발생하지 않으므로 동시 업데이트 문제 또한 일어나지 않는다.

저장공간과 처리 능력이 충분하면 애플리케이션이 완전한 불변성을 갖도록 만들 수 있고, 따라서 완전한 함수형으로 만들 수 있음.

eg. 소스 코드 버전 관리 시스템

각 패러다임은 우리가 코드를 작성하는 방식의 형태를 한정시킴.

소프트웨어, 즉 컴퓨터 프로그램은 순차, 분기, 반복, 참조로 구성된다. 그 이상도 이하도 아니다.

3부. 설계 원칙

SOLID 원칙

  • 함수와 데이터 구조를 클래스로 배치하는 방법, 그리고 이들 클래스를 서로 결합하는 방법을 설명해주는 원칙.
  • 객체 지향 소프트웨어에만 적용 ❌. ‘클래스’는 단순히 함수와 데이터를 결합한 집합을 의미.
  • 목적
    • 변경에 유연
    • 이해하기 쉽다
    • 많은 소프트웨어 시스템에 사용될 수 있는 컴포넌트의 기준이 된다

7장. SRP : 단일 책임 원칙

단일 모듈은 변경의 이유가 하나, 오직 하나뿐이어야 한다.

단 하나의 일만 해야 한다는 원칙은 따로 있으니 헷갈리지 말 것!

하나의 모듈은 하나의, 오직 하나의 액터에 대해서만 책임져야 한다

서로 다른 액터가 의존하는 코드를 서로 분리하라!

단일 책임 원칙은 메서드와 클래스 수준의 원칙.

8장. OCP : 개방-폐쇄 원칙

소프트웨어 개체는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.

아키텍트는 기능이 어떻게, 왜, 언제 발생하는지에 따라서 기능을 분리하고, 분리한 기능을 컴포넌트의 계층구조로 조직화한다. 컴포넌트 계층구조를 이와 같이 조직화하면 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있다.

추이 종속성을 가지게 되면, 소프트웨어 엔티티는 ‘자신이 직접 사용하지 않는 요소에는 절대로 의존해서는 안 된다’는 소프트웨어 원칙을 위반하게 된다. 추이 종속성은 인터페이스를 활용한 정보 은닉 달성을 통해 해결할 수 있다.

OCP의 목표

  • 시스템을 확장하기 쉬운 동시에 변경으로 인해 시스템이 너무 많은 영향을 받지 않도록 하는 데 있음
  • 이러한 목표 달성을 위해 시스템을 컴포넌트 단위로 분리하고, 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있는 형태의 의존성 계층구조가 만들어지도록 해야 한다.

🙋🏻추이 종속성*

클래스 A가 클래스 B에 의존하고, 다시 클래스 B가 클래스 C에 의존한다면, 클래스 A는 클래스 C에 의존하게 된다. 이를 추이 종속성이라고 부르며, 클래스 이외의 소프트웨어의 모든 엔티티(패키지, 컴포넌트 등)에도 동일하게 적용됨.

9장. LSP : 리스코프 치환 원칙

S 타입의 객체 o1 각각에 대응하는 T 타입 객체 o2가 있고, T 타입을 이용해서 정의한 모든 프로그램 P에서 o2의 자리에 o1을 치환하더라도 P의 행위가 변하지 않는다면, S는 T의 하위 타입이다.

초창기 LSP는 상속을 사용하도록 가이드하는 방법 정도로 간주됨. 하지만 LSP는 인터페이스와 구현체에도 적용되는 더 광범위한 소프트웨어 설계 원칙으로 변모.

10장. ISP : 인터페이스 분리 원칙

범용적인 인터페이스보다 여러 인터페이스로 분리하는 것이 낫다

11장. DIP

소스 코드 의존성이 추상에 의존하며 구체에는 의존하지 않는다

자바와 같은 정적 타입 언어에서 이 말은 use, import, include 구문은 오직 인터페이스나 추상 클래스 같은 추상적인 선언만을 참조해야 한다는 뜻으로, 구체적인 대상에는 절대로 의존해서는 안 된다. 안정적이고 변경이 자주 일어나지 않으리라 확신할 수 있는 것에 대한 의존은 무시한다. 의존하지 않도록 피하고자 하는 것은 변동성이 큰 구체적인 요소이다.

인터페이스는 구현체보다 변동성이 낮다. 안정된 소프트웨어 아키텍처란 변동성이 큰 구현체에 의존하는 일은 지양하고, 안정된 추상 인터페이스를 선호하는 아키텍처이다.

  • 변동성이 큰 구체 클래스를 참조하지 말라. 대신 추상 인터페이스를 참조하라.
    • 객체 생성 방식을 강하게 제약하며, 일반적으로 추상 팩토리를사용하도록 강제한다.
  • 변동성이 큰 구체 클래스로부터 파생하지 말라.
    • 상속은 소스 코드에 존재하는 모든 관계 중 가장 강력한 동시에 뻣뻣해서 변경하기 어렵다. 따라서 상속은 아주 신중하게 사용
  • 구체 함수를 오버라이드 하지 말라.
  • 구체적이며 변동성이 크다면 절대로 그 이름을 언급하지 말라 (DIP 원칙)

⭐️의존성 역전

소스 코드 의존성은 제어흐름과는 반대 방향으로 역전

4부. 컴포넌트 원칙

SOLID 원칙이 벽과 방에 벽돌을 배치하는 방법. 컴포넌트 원칙은 빌딩에 방을 배치하는 방법.

12장. 컴포넌트

컴포넌트?

  • 배포 단위. 컴포넌트는 시스템의 구성 요소로 배포할 수 있는 가장 작은 단위.

  • eg. 자바 : jar 파일이 컴포넌트.
  • 잘 설계된 컴포넌트는 반드시 독립적으로 배포 가능한, 따라서 독립적으로 개발 가능한 능력을 갖춰야 한다.

13장. 컴포넌트 응집도

어느 클래스들을 묶어서 컴포넌트로 만들지를 결정할 때, 재사용성개발가능성이라는 상충하는 힘을 반드시 고려.

어떤 클래스를 어떤 컴포넌트에 포함시켜야 하는지에 대한 원칙 3가지.

1. REP : 재사용/릴리스 등가 원칙

재사용 단위는 릴리스 단위와 같다

단일 컴포넌트는 응집성 높은 클래스와 모듈들로 구성되어야 한다. 컴포넌트를 구성하는 모든 모듈은 서로 공유하는 중요한 테마나 목적이 있어야 한다.

하나의 컴포넌트로 묶인 클래스와 모듈은 반드시 함께 릴리스할 수 있어야 한다.

2. CCP : 공통 폐쇄 원칙

동일한 이유로 동일한 시점에 변경되는 클래스를 같은 컴포넌트로 묶어라.

서로 다른 시점에 다른 이유로 변경되는 클래스는 다른 컴포넌트로 분리하라.

단일 책임 원칙을 컴포넌트 관점에서 다시 쓴 것. 단일 컴포넌트는 변경의 이유가 여러 개 있어서는 안 된다는 의미.

SRP와의 유사성

CCP는 컴포넌트 수준의 SRP.

  • SRP : 서로 다른 이유로 변경되는 메서드를 서로 다른 클래스로 분리하라

  • CCP : 서로 다른 이유로 변경되는 클래스를 서로 다른 컴포넌트로 분리하라

3. CRP : 공통 재사용 원칙

컴포넌트 사용자들을 필요하지 않은 것에 의존하게 강요하지 말라

클래스와 모듈을 어느 컴포넌트에 위치시킬지 결정할 때 도움이 되는 원칙.

같이 재사용되는 경향이 있는 클래스와 모듈들은 같은 컴포넌트에 포함해야 한다.

CRP는 강하게 결합되지 않은 클래스들을 동일한 컴포넌트에 위치시켜서는 안 된다고 말한다.

ISP와의 관계

CRP는 인터페이스 분리 원칙의 포괄적인 버전.

  • ISP : 사용하지 않은 메서드가 있는 클래스에 의존하지 말라
  • CRP : 사용하지 않는 클래스를 가진 컴포넌트에 의존하지 말라

14장. 컴포넌트 결합

컴포넌트 사이의 관계

ADP : 의존성 비순환 원칙

컴포넌트 의존성 그래프에 순환이 있어서는 안 된다

구성요소 간 의존성을 파악하고 있으면 시스템을 빌드하는 방법을 알 수 있다. 시스템 전체를 릴리스해야 할 때가 오면 릴리스 절차는 상향식으로 진행된다. (Entities → Database, Interactors → Presenters, View → Main).

순환이 생기면 컴포넌트를 분리하기 어려워진다. 또한, 컴포넌트를 어떤 순서로 빌드해야 올바를지 파악하기가 상당히 힘들어진다.

순환 의존 해결법 2가지

  1. DIP 적용
  2. 새로운 컴포넌트 생성

컴포넌트 의존성 다이어그램은 애플리케이션의 기능을 기술하는 일과는 거의 관련이 없음. 오히려 컴포넌트 의존성 다이어그램은 애플리케이션의 빌드가능성유지보수성을 보여주는 지도와 같다. → 이에, 컴포넌트 구조는 프로젝트 초기에 설계할 수 없음.

애플리케이션이 계속 성장함에 따라 우리는 재사용 가능한 요소를 만드는 일에 관심을 기울이기 시작. 이 시점이 되면 컴포넌트를 조합하는 과정에 공통 재사용 원칙(CRP)이 영향을 미치기 시작. 결국 순환이 발생하면 ADP가 적용됨. 컴포넌트 의존성 그래프는 조금씩 흐트러지고 또 성장한다.

❗️클래스 설계가 되지도 않은 상태에서 컴포넌트 의존성 구조를 설계하려고 시도하면 실패할 수 있음.

SDP : 안정된 의존성 원칙

더 안정된 쪽에 의존하라

SAP : 안정된 추상화 원칙

컴포넌트는 안정된 정도만큼만 추상화되어야 한다.

안정된 컴포넌트는 추상 컴포넌트여야 하며, 이를 통해 안정성이 컴포넌트를 확장하는 일을 방해해서는 안 된다. 불안정한 컴포넌트는 반드시 구체 컴포넌트여야 한다. 컴포넌트가 불안정하므로 컴포넌트 내부의 구체적인 코드를 쉽게 변경할 수 있어야 하기 때문이다.

5부. 아키텍처

15장. 아키텍처란?

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

소프트웨어를 부드럽게 유지하는 방법은 선택사항을 가능한 한 많이, 그리고 가능한 한 오랫동안 열어 두는 것이다. → 열어 둬야 할 선택사항? 중요치 않은 세부사항

소프트웨어 시스템은 주요한 두 가지 구성요소로 분해 가능

  1. 정책 : 모든 업무 규칙과 업무 절차를 구체화. 시스템의 진정한 가치가 살아 있는 곳
  2. 세부사항 : 사람, 외부 시스템, 프로그래머가 정책과 소통할 때 필요한 요소. 정책이 가진 행위에는 조금도 영향을 미치지 않음.
    • eg) 입출력 장치, 데이터베이스, 웹 시스템, 서버, 프레임워크, 통신 프로토콜

아키텍트의 목표 : 시스템에서 정책을 가장 핵심적인 요소로 식별하고, 동시에 세부사항은 정책에 무관하게 만들 수 있는 형태의 시스템을 구축.

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

16장. 독립성

좋은 아키텍처의 조건

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

유스케이스

시스템의 아키텍처는 시스템의 의도를 지원해야 한다. 아키텍트의 최우선 관심사는 유스케이스이며, 아키텍처에서도 유스케이스가 최우선이다.

좋은 아키텍처는 행위를 명확히 하고 외부로 드러내며, 이를 통해 시스템이 지닌 의도를 아키텍처 수준에서 알아볼 수 있게 만드는 것.

운영

운영 작업을 허용할 수 있는 형태의 아키텍처를 구조화해야 한다.

개발

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

잘 격리되어 독립적으로 개발 가능한 컴포넌트 단위로 시스템을 분할할 수 있어야 한다.

배포

목표는 즉각적인 배포다.

시스템을 제대로 격리된 컴포넌트 단위로 분할할 때 도움이 되며, 이를 통해 선택사항을 가능한 한 많이, 그리고 가능한 한 오랫동안 열어 둘 수 있게 해준다.

좋은 아키텍처는 선택사항을 열어 둠으로써, 향후 시스템에 변경이 필요할 때 어떤 방향으로든 쉽게 변경할 수 있도록 한다.

중복

소프트웨어에서 중복은 일반적으로 나쁜 것이다. 하지만 중복에도 여러 종류가 있다.

  • 진짜 중복 : 한 인스턴스가 변경되면, 동일한 변경을 그 인스턴스의 모든 복사본에 반드시 적용해야 한다.
  • 거짓된/우발적인 중복 : 중복으로 보이는 두 코드 영역이 각자의 경로로 발전한다면, 즉 서로 다른 속도와 다른 이유로 변경된다면 이 두 코드는 진짜 중복이 아니다.

자동반사적으로 중복을 제거해버리는 잘못을 저지르는 유혹을 떨쳐내라. 중복이 진짜 중복인지 확인하라

  1. 소스 수준 분리 모드
    • 소스 코드 모듈 사이의 의존성 제어 가능 : 하나의 모듈이 변하더라도 다른 모듈을 변경하거나 재컴파일하지 않도록 만들 수 있다.
    • 모든 컴포넌트가 같은 주소 공간에서 실행되고, 서로 통신할 때는 간단한 함수 호출을 사용.
    • 컴퓨터 메모리에는 하나의 실행 파일만 로드
    • 모노리틱 구조
  2. 배포 수준 분리 모드
    • jar 파일, DLL, 공유 라이브러리와 같이 배포 가능한 단위들 사이의 의존성을 제어 가능 : 한 모듈의 소스 코드가 변하더라도 다른 모듈을 재빌드하거나 재배포하지 않도록 만들 수 있음
    • 많은 컴포넌트가 같은 주소 공간에 상주, 단순한 함수 호출을 통해 통신 가능
    • 중요한 특징 : 결합이 분리된 컴포넌트가 jar 파일, Gem 파일, DLL과 같이 독립적으로 배포할 수 있는 단위로 분할되어 있다는 점
  3. 서비스 수준 분리 모드
    • 의존하는 수준을 데이터 구조 단위까지 낮출 수 있고, 순전히 네트워크 패킷을 통해서만 통신하도록 만들 수 있다.
    • 모든 실행 가능한 단위는 소스와 바이너리 변경에 대해 서로 완전히 독립적
    • eg) 서비스 또는 마이크로서비스

17장. 경계 : 선 긋기

소프트웨어 아키텍처는 선을 긋는 기술이며, 이러한 선을 경계라고 부름.

경계는 소프트웨어 요소를 서로 분리하고, 경계 한편에 있는 요소가 반대편에 있는 요소를 알지 못하도록 막는다.

→ 이를 통해, 결합을 줄일 수 있다. 프레임워크, 데이터베이스, 웹서버, 유틸리티 라이브러리, 의존성 주입에 대한 결정 등은 이른 결정이다. 이들은 시스템의 업무 요구사항, 즉 유스케이스와 아무런 관련이 없는 결정이다. 부수적인 결정을 연기할 수 있는 것이 좋은 아키텍처이다.

어떻게? 언제? 선을 그을까?

관련이 있는 것과 없는 것 사이에 선을 긋는다.

GUI - 업무 규칙, DB - GUI, DB - 업무 규칙 각각 선을 그어 경계짓는다.

업무 규칙이 DB에 신경 쓰지 않아야 한다는 부분에서 동의하지 않는 사람이 많을 것이다. 데이터베이스는 업무 규칙이 간접적으로 사용할 수 있는 도구. 업무 규칙은 스키마, 쿼리 언어, 데이터베이스와 관련된 나머지 세부사항에 대해 어떤 것도 알아서는 안된다. 업무 규칙이 알아야 할 것은 데이터를 가져오고 저장할 때 사용할 수 있는 함수 집합이 있다는 사실이 전부. 이러한 함수 집합을 통해서 우리는 데이터베이스를 인터페이스 뒤로 숨길 수 있다.

입력과 출력

입력과 출력은 중요하지 않다는 사실! UX는 인터페이스에 의해 좌우된다. 화면, 마우스, 버튼, 음향 등이 인터페이스. 인터페이스 뒤에는 인터페이스를 조작하는 모델(데이터 구조와 함수로 구성된 정교한 집합)이 존재한다. 중요한 사실은 모델은 인터페이스를 전혀 필요로 하지 않는다는 점. 결국 중요한 것은 업무 규칙이다.

경계는 변경의 축이 있는 지점에 그어진다. 경계의 한쪽에 위치한 컴포넌트는 경계 반대편의 컴포넌트와는 다른 속도로, 그리고 다른 이유로 변경된다. 단일 책임 원칙은 어디에 경계를 그어야 할지를 알려준다.

경계선을 그리려면 먼저 시스템을 컴포넌트 단위로 분할해야 한다. 컴포넌트 사이의 화살표가 특정 방향, 즉 핵심 업무를 향하도록 이들 컴포넌트의 소스를 배치한다.

이는 의존성 역전 원칙과 안정된 추상화 원칙을 응용한 것이다. 의존성 화살표는 저수준 세부사항에서 고수준의 추상화를 향하도록 배치 된다.

18장. 경계 해부학

아키텍처 경계는 다양한 형태로 나타남.

1. 물리적으로 엄격하게 구분되지 않는 형태


함수와 데이터가 단일 프로세서에서 같은 주소 공간을 공유하며 그저 나름의 규칙에 따라 분리되어 있을 뿐. (= 소스 수준 분리 모드)

배포 관점에서 보면 단일체라고 불리는 단일 실행 파일에 지나지 않음.

거의 모든 경우에 특정한 동적 다형성에 의존하여 내부 의존성을 관리한다.

가장 단순한 형태의 경계 횡단은 저수준 클라이언트에서 고수준 서비스로 향하는 함수 호출이다. 이 경우 런타임 의존성과 컴파일타임 의존성은 모두 같은 방향, 즉 저수준 컴포넌트에서 고수준 컴포넌트로 향한다.

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

💡 이처럼 규칙적인 방식으로 구조를 분리하면 프로젝트를 개발, 테스트, 배포하는 작업에 큰 도움이 됨. 서로의 영역에 침범하지 않은 채 자신만의 컴포넌트를 독립적으로 작업할 수 있음. 고수준 컴포넌트는 저수준 세부사항으로부터 독립적으로 유지됨.

단일체를 배포하는 일은 일반적으로 컴파일과 정적 링크 작업을 수반. → 컴포넌트는 소스 코드 형태로 전달됨.

2. 물리적으로 드러나는 형태 : 동적 링크 라이브러리


예) .NET DLL, 자바 jar 파일, 루비 Gem, 유닉스 공유 라이브러리 등.

컴포넌트를 이 형태로 배포하면 따로 컴파일하지 않고 곧바로 사용 가능. 대신 컴포넌트는 바이너리와 같이 배포 가능한 형태로 전달됨.(= 배포 수준 결함 분리 모드)

배포 과정에서만 차이 날 뿐, 배포 수준의 컴포넌트는 단일체와 동일.

3. 훨씬 강한 물리적 형태 아키텍처 경계 : 로컬 프로세스


로컬 프로세스는 주로 명령행이나 그와 유사한 시스템 호출을 통해 생성됨.

  • 동일한 프로세서 또는 하나의 멀티코어 시스템에 속한 여러 프로세서들에서 실행됨. 각각이 독립된 주소 공간에서 실행됨.
  • 일반적으로 메모리 보호를 통해 프로세스들이 메모리를 공유하지 못하게 함.
  • 앞의 경계와 달리, 소켓, 메일박스, 메시지 큐와 같이 운영체제에서 제공하는 통신 기능을 이용해 통신.
  • 저수준 프로세스가 고수준 프로세스의 플러그인이 되도록 만드는 것이 아키텍처 관점의 목표

4. 물리적 형태 중 가장 강력한 경계 : 서비스


  • 서비스는 자신의 물리적 위치에 구애받지 않음
  • 서로 통신하는 두 서비스는 물리적으로 동일한 프로세서나 멀티코어에서 동작할 수도 있고, 아닐 수도 있음.
  • 모든 통신이 네트워크를 통해 이뤄진다고 가정
  • 고수준 서비스의 소스 코드에는 저수준 서비스를 특정 짓는 어떤 물리적인 정보도 절대 포함해서는 안됨.

19장. 정책과 수준

소프트웨어 시스템이란 정책을 기술한 것. 대다수의 주요 시스템에서 하나의 정책은 이 정책을 서술하는 여러 개의 조그만 정책들로 쪼갤 수 있다. 소프트웨어 아키텍처를 개발하는 기술에는 이러한 정책을 신중하게 분리하고, 정책이 변경되는 양상에 따라 정책을 재편성하는 일도 포함된다.

흔히 아키텍처 개발은 재편성된 컴포넌트들은 비순환 방향 그래프로구성하는 기술을 포함한다.

의존성은 소스 코드, 컴파일타임의 의존성이다. 자바의 경우 import 구문에 해당한다.

좋은 아키텍처라면 저수준 컴포넌트가 고수준 컴포넌트에 의존하도록 설계되어야 한다.

⭐️수준(Level)⭐️

입력과 출력까지의 거리. 시스템의 입력과 출력 모두로부터 멀리 위치할수록 정책의 수준은 높아진다.

데이터 흐름과 소스 코드 의존성이 항상 같은 방향을 가리키지 않는다! 소스 코드 의존성은 그 수준에 따라 결합되어야 하며, 데이터 흐름을 기준으로 결합되어서는 안 된다.

function encrypt() {
  while(true)
    writeChar(translate(readChar()));
}

위는 잘못된 아키텍처이다. 고수준인 encrypt 함수가 저수준인 readChar와 writeChar 함수에 의존하기 때문이다.

clean arch 19_2

고수준의 암호화 정책을 저수준의 입력/출력 정책으로부터 분리시킴. → 입력과 출력에 변화가 생기더라도 암호화 정책은 거의 영향을 받지 않아 암호화 정책을 더 넓은 맥락에서 사용할 수 있음.

  • 고수준 정책 : 입력과 출력에서부터 멀리 떨어진 정책
    • 저수준 정책에 비해 덜 빈번하게 변경
    • 보다 중요한 이유로 변경되는 경향
  • 저수준 정책 : 입력과 출력에 가까이 위치한 정책
    • 더 빈번하게 변경됨
    • 보다 긴급성을 요함
    • 덜 중요한 이유로 변경되는 경향

⭐️모든 소스 코드 의존성의 방향이 고수준 정책을 향할 수 있도록 정책을 분리했다면 변경의 영향도를 줄일 수 있다.

20장. 업무 규칙

  • 업무규칙 : 사업적으로 수익을 얻거나 비용을 줄일 수 있는 규칙 또는 절차
  • 핵심 업무 규칙(Critical Business Rule) : 사업 자체에 핵심적이며, 규칙을 자동화하는 시스템이 없더라도 존재하는 업무 규칙
  • 핵심 업무 데이터(Critical Business Data) : 핵심 업무 규칙에 필요한 데이터
  • 엔티티(Entity) : 핵심 규칙과 핵심 데이터 결합으로 인해 추출되는 객체

엔티티


컴퓨터 시스템 내부의 객체. 핵심 업무 데이터를 기반으로 동작하는 일련의 조그만 핵심 업무 규칙을 구체화한다.

엔티티 = 핵심 업무 데이터 + 핵심 업무 규칙

엔티티는 순전히 업무에 대한 것이며, 이외의 것은 없음.

유스 케이스


  • 자동화된 시스템이 사용되는 방법을 설명
  • 사용자가 제공해야 하는 입력, 사용자에게 보여줄 출력, 그리고 해당 출력을 생성하기 위한 처리 단계를 기술
  • 애플리케이션에 특화된 업무 규칙을 설명
  • 엔티티 내부의 핵심 업무 규칙을 어떻게, 그리고 언제 호출할 지를 명시하는 규칙을 담는다. = 엔티티의 행위에 대해 유스케이스가 제어
  • 유스케이스는 사용자 인터페이스를 기술하지않는다
  • 유스케이스는 객체
    • 유스케이스는 애플리케이션에 특화된 업무 규칙을 구현하는 하나 이상의 함수를 제공
    • 입력 데이터, 출력 데이터, 엔티티에 대한 참조 데이터 등의 데이터 요소 포함
  • 유스케이스는 엔티티에 의존. 반면 엔티티는 유스케이스에 의존하지 않음.
<기능 제목>
입력 : 
출력:
기본과정
  1. 
  2. 
  3. 
  4.

위는 유스케이스 작성 템플릿.

21장. 소리치는 아키텍처

소프트웨어 아키텍처는 시스템의 유스케이스를 지원하는 구조.

아키텍처는 프레임워크에 대한 것이 아니다. 아키텍처를 프레임워크 중심으로 만들어버리면 유스케이스가 중심이 되는 아키텍처는 절대 나올 수 없다.

아키텍처의 목적


좋은 아키텍처는 유스케이스를 중심에 둔다. 또한 좋은 아키텍처는 프레임워크, 데이터베이스, 웹 서버, 여타 개발 환경 문제나 도구에 대해서는 결정을 미룰 수 있도록 만든다.

22장. 클린 아키텍처

아키텍처의 목표는 관심사의 분리다.

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

22_1

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

엔티티

  • 전사적인 핵심 업무 규칙을 캡슐화
  • 메스드를 가진 캑체 또는 일련의 데이터 구조와 함수의 집합
  • 외부의 무언가가 변경되더라도 엔티티가 변경될 가능성은 낮음

유스케이스

  • 애플리케이션에 특화된 업무 규칙을 포함
  • 엔티티로 들어오고 나가는 데이터 흐름을 조정
    • 엔티티가 자신의 핵심 업무 규칙을 사용해 유스케이스의 목적을 달성하도록 함

인터페이스 어댑터

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

프레임워크와 드라이버

  • 데이터베이스나 웹 프레임워크 같은 프레임워크나 도구들로 구성
  • 모든 세부사항이 위치하는 곳

각 경계를 횡단하는 방법

의존성 역전 원칙 적용

  • 제어흐름과 의존성의 방향이 명백히 반대
  • 제어흐름과 반대로 바깥쪽 원의 코드가 안쪽 원의 코드를 호출하게 만듦

경계를 횡단할 때 데이터는 데이터 전송 객체(Data Transfer Object, DTO)를 사용한다. 엔티티나 데이터베이스의 행 전체 데이터를 전달하는 것은 의존성 규칙을 위배하는 행위기 때문에 하지 않는다!

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

프레젠터 (Presenter)?

  • OutputData를 ViewModel과 같이 화면에 출력할 수 있는 형식으로 재구성

프레젠터는 험블 객체 패턴을 따른 형태이다.

험블 객체 패턴


  • 디자인 패턴으로, 테스트하기 어려운 행위테스트하기 쉬운 행위를 단위 테스트 작성자가 분리하기 쉽게 하는 방법
  • 행위들을 두 개의 모듈 또는 클래스로 나눈다. 그리고 테스트가 어려운 행위를 모두 험블 객체로 옮김. 나머지 모듈에는 테스트하기 쉬운 행위를 모두 옮김
  • 험블 객체 패턴 사용 시 두 부류의 행위를 분리해 프레젠터와 뷰라는 서로 다른 클래스로 만들 수 있음

프레젠터와 뷰


💡프레젠터

  • 테스트 하기 쉬운 객체
  • 애플리케이션으로부터 데이터를 받아 화면에 표현할 수 있는 포맷으로 만듦

💡뷰

  • 험블 객체
  • 테스트 하기 어려움
  • 데이터를 GUI로 이동시킴
  • 데이터를 직접 처리하지 않음
  • 뷰 모델의 데이터를 화면으로 로드할 뿐

데이터베이스 게이트웨이


  • 유스케이스 인터랙터와 데이터베이스 사이에 위치함.
  • 다형적 인터페이스
  • 애플리케이션이 데이터베이스에 수행하는 CRUD 작업과 관련된 모든 메서드 포함
  • 인터페이스의 구현체는 데이터베이스 계층에 위치
    • 구현체는 험블 객체

데이터 매퍼


  • 객체는 데이터 구조가 아님
  • 객체 사용자는 객체에서 public 메서드만 볼 수 있음 → 즉, 사용자 관점에서 볼 때 객체단순히 오퍼레이션의 집합
  • 데이터 구조 : 함축된 행위를 가지지 않는 public 데이터 변수의 집합
  • ORM 시스템은 데이터베이스 계층. 게이트웨이 인터페이스와 데이터베이스 사이에서 일종의 또 다른 험블 객체 경계를 형성

24장. 부분적 경계

경계를 완벽하게 만드는 데 비용이 많이 든다. (eg. 데이터 구조, 컴포넌트 격리를 위한 의존성 관리 등)

→ 그렇기 때문에 부분적 경계로 구현

1. 마지막 단계 건너뛰기


독립적으로 컴파일하고 배포할 수 있는 컴포넌트를 만들기 위한 작업은 모두 수행 후, 단일 컴포넌트에 그대로 모아만 두는 것.

  • 완벽한 경계를 만들 때 만큼의 코드량과 사전 설계 필요
  • 다수의 컴포넌트 관리를 위한 작업 불필요
  • 시간이 지나면서
    • 별도로 분리한 컴포넌트가 재사용될 가능성이 없을 수 있음
    • 컴포넌트 간의 구분이 희미해짐

2. 일차원 경계


전략 패턴을 활용한 쌍방향 Boundary 인터페이스 사용.

💡전략 패턴 ?

알고리즘 군을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만든다

3. 퍼사드


  • 경계는 Facade 클래스로만 간단히 정의.

  • Facade 클래스에는 모든 서비스 클래스를 메서드 형태로 정의하고, 서비스 호출일 발생하면 해당 서비스 클래스로 호출을 전달. 클라이언트는 이들 서비스 클래스에 직접 접근 불가
  • 클라이언트가 모든 서비스 클래스에 대한 추이 종속성을 가지게 되는 문제 발생 : 서비스 클래스 중 하나 소스 코드 변경 시 클라이언트도 재컴파일

💡퍼사드 패턴 ?

어떤 서브시스템의 일련의 인터페이스에 대한 통합된 인터페이스를 제공한다. 퍼사드에서 고수준 인터페이스를 정의하기 때문에 서브시스템을 더 쉽게 사용할 수 있음.

25장. 계층과 경계

추상화가 필요하리라고 미리 예측해서는 안 된다. (= YAGNI : You Aint’ Gonna Neeed It)

현명하게 추측해야 한다. 아키텍처의 경계를 어디에 둘 것이며, 부분적으로 구현할 경계와 무시할 경계는 무엇인지를 결정해야 한다. 이는 일회성 결정이 아니다. 시스템이 발전함에 따라 주의를 기울여야 한다.

26장. Main 컴포넌트

Main

  • 모든 시스템에는 최소 하나의 컴포넌트가 존재하고, 이 컴포넌트가 나머지 컴포넌트를 생성, 조정, 관리한다.
  • 궁극적인 세부사항으로, 가장 낮은 수준의 정책
  • 시스템의 초기 진입점
  • 가장 지저분한 컴포넌트
  • 고수준의 시스템을 위한 모든 것을 로드한 후, 제어권을 고수준의 시스템에게 넘긴다.

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

서비스 지향 아키텍처, 마이크로서비스 아키텍처 는 최근에 큰 인기를 끌고 있다.

서비스 아키텍처?

서비스는 프로세스나 플랫폼 경계를 가로지르는 함수 호출에 지나지 않는다. 아키텍처적으로 중요한 서비스도 있지만, 중요하지 않은 서비스도 존재한다.

서비스의 이점?

  1. 결합 분리의 오류
    • 서비스 사이의 결합이 확실이 분리된다 → 꼭 그런 것만은 아님
    • 프로세스 내의 또는 네트워크 상의 공유 자원 때문에 결합될 가능성이 여전히 존재
    • 서로 공유하는 데이터에 의해 서비스는 강력하게 결합되어 버림
  2. 개발 및 배포 독립성의 오류
    • 전담팀에서 각 서비스를 작성하고, 유지보수하며, 운영하는 책임을 질 수 있다.
    • 대규모 엔터프라이즈 시스템은 서비스 기반 시스템 외에도, 모노리틱 시스템이나 컴포넌트 기반 시스템으로도 구축할 수 있다. 즉, 서비스는 확장 가능한 시스템을 구축하는 유일한 선택지가 아니다
    • 서비스라고 해서 항상 독립적으로 개발하고, 배포하며, 운영할 수 있는 것은 아니다

횡단 관심사 문제

  • 기능을 추가하려면 서비스 전부를 변경해야 하는 경우가 존재한다. 서비스들이 모두 결합되어 있어 독립적으로 개발하고, 배포하거나, 유지될 수 없다.
  • 다형적으로 확장할 ㅅ ㅜ있는 클래스 집합을 생성해 새로운 기능을 처리하도록 함 : 템플릿 메서드전략 패턴 등을 이용

서비스는 시스템의 확장성과 개발 가능성 측면에서 유용하지만, 그 자체로는 아키텍처적으로 그리 중요한 요소는 아님. 시스템의 아키텍처는 시스템 내부에 그어진 경계와 경계를 넘나드는 의존성에 의해 정의된다.

28장. 테스트 경계

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

깨지기 쉬운 테스트 문제

  • 시스템의 공통 컴포넌트가 변경되면 수백, 심지어 수천 개의 테스트가 망가진다
  • 해결방법
    • 테스트를 고려한 설계 필수
    • 변동성이 있는 것에 의존하지 말라

읽고나서


저수준 세부사항에서 고수준의 추상화를 향하도록 배치 된다.

위의 문장이 가장 핵심으로 받아들여지는 문장이었다. 의존성 규칙. 의존성 역전으로 인한 컴포넌트 간의 독립성 및 의존성 격리 확보가 중요하다는 것을 알 수 있었다. 실제 프로젝트에 투입되어 설계를 진행하다 보면 많은 부분에 있어서 어려움을 겪는다. 물론 가장 어려운 부분은 핵심 업무 규칙을 파악하는 것이다. 그래서 많은 기업에서 업무 도메인 경험을 중시하는 것일 것이다. 업무 규칙과 더불어서 어려운 점은 어떻게 소프트웨어를 유연하고 확장 가능하도록 만들 것인가 이다. 저자인 엉클 밥은 이러한 고민들에 대한 해결책들을 제시하고 다양한 방향의 해결책들을 제시하고 있다.

아직 지식도, 경험도 부족한 주니어 개발자로서 이 책의 10퍼센트도 이해하지 못했다고 생각한다. 하지만 스프링 프레임워크의 의존성 역전이 왜 나온 것이며, 왜 중요한 것인지가 잘 와닿지 않았는데, 이 책을 통해서 조금이나마 그 중요성을 깨달을 수 있었다. 이 책 안의 원칙들을 모두 지키는 것이 이상적일 것이다. 하지만 현실은 수 많은 사람들의 이해관계, 예산, 일정 등의 많은 외부 요소들이 소프트웨어의 바깥에 존재한다. 따라서 아키텍트와 개발자 모두 이러한 요소를 고려한 최선의 아키텍처를 제시할 수 있어야 할 것이다. <클린 아키텍처>는 그러한 판단의 근거가 되는 훌륭한 책이라 생각된다.

References

  • Clean Architecture, Rober C. Martin
  • Head First Design Pattern