클린 아키텍처 (1) – 오버뷰, 패러다임, 설계 원칙 SOLID

  • by

로버트 마틴 형제의 깨끗한 아키텍처 정리.

경험이 부족했던 주니어 시절에 완독했지만, 또 손에 들고 읽어 보면, 더 보이는 것이 많아, 놓치고 싶지 않은 마음에 기록으로서 남겨 두는 컴퓨터 다툼들의 명도서.


<オーバービュー>

설계와 아키텍처란?

  • 아키텍처(고수준)에서 설계(저수준)로 향하는 의사결정의 연속성만 있을 뿐. 두 개념은 경계도 없고 차이도 없다.

  • 소프트 아키텍처의 품질을 진지하게 생각하자. 빨리 가는 유일한 방법은 제대로 가는 것입니다.

  • 코드의 정리는 나중에 하면 된다.

    일단 시장 발매가 먼저다!
    라는 마인드는 현실에서는 제대로 동작할 수 없다.

  • 비용을 최소화하고 생산성을 극대화할 수 있는 설계와 아키텍처를 갖춘 시스템을 구축하십시오.

두 가지 가치

  • 모든 sw 시스템은 2가지 가치를 제공하고 sw 개발자는 2가지 가치를 반드시 높게 유지해야 합니다.

    • 행위(behavior)– 구슬 요구 사항을 반영하기 위해 코드를 작성하고 디버깅을 수행하여 기계가 적절한 조치를 취하도록합니다.

      • 즉, 시스템이 작동하도록 한다는 관점. 긴급하지만 중요하지 않다… .
    • 구조(structure)– sw는 부드러움을 갖도록 만들어졌습니다 (software). 즉, 요건의 변화에 ​​수반하는 머신의 행위의 변화를 간단하게 포함시킬 수 있도록 변경하기 쉬운 아키텍처를 의미. 변경해도 범위에 어려움이 비례해야 하며 형태와는 무관해야 한다.

      • 즉, 시스템이 변화에 유연해진다는 관점. 긴급하지는 않지만 중요…
  • 올바른 SW 개발팀은 부끄러움을 완화하고 시스템 아키텍처를 위해 다른 이해관계자와 동등하게 논의해야 한다.

    이것이 책무다.

  • SA는 시스템이 제공하는 특성과 기능보다 시스템 구조에 중점을 둡니다.

    올바른 가치를 위해 싸우자.

<プログラミングパラダイム>

구조 프로그래밍

초기 프로그래밍의 세계관에서 이미 모든 논리 흐름은 순차/분기/반복의 세 가지 구조만으로 표현할 수 있음이 입증되었습니다.

기능을 계층적인 구조로 브레이크다운해 나가면, 모듈과 그 모듈을 상세하게 기능적으로 분해할 수 있다는 사실도 도출되었다.

증명이란? 수학은 증명할 수 있는 묘사가 진실하다는 것을 증명하고 과학은 증명할 수 있는 묘사가 가짜다는 것을 증명하는 과정이다.

테스트는 버그가 있음을 나타내며 버그가 없음을 나타낼 수 없습니다.

즉, 프로그램의 실수를 증명할 수는 있지만 프로그램의 올바른 것을 증명하는 것은 불가능합니다.

구조적 프로그래밍은 프로그램을 증명할 수 있는 상세 기능 세트로 재귀적으로 분해하는 것을 강제하고, 테스트에 의해 증명 가능한 상세 기능이 거짓임을 입증하려고 시도합니다.

따라서 프로그래밍은 과학입니다.

결론으로 SA는 모듈, 컴포넌트 및 서비스를 쉽게 반증(테스트)할 수 있도록 기능적 분해를 최상의 실천법으로 간주해야 한다.

.

객체 지향 프로그래밍

캡슐화 :실은 c→c++→c#로 발전해 왔고, 보다 완전한 캡슐화로부터 멀어졌다.

따라서 OOP는 언어적으로 캡슐화를 강제하지 않으므로 프로그래머가 제대로 작동하여 캡슐화된 데이터를 우회하여 사용하지 않는다는 믿음으로 작동합니다.

상속 : OOP로부터의 상속은, 데이터 구조에 가면의 쓰는 것을 매우 편하게 제공할 수 있다는 점에서는 의미를 줄 수 있다.

다만, 계승도 c에서도 공연하게 사용되고 있던 방식이므로(include를 통해..), 계승이라는 개념 자체가 OOP를 설명하는 주된 기준이 될 수 없다.

다형 : 엄밀히 말하면 c의 포인터가 바로 OOP의 다형성의 근원이 되는 개념이다.

다만, OOP의 등장으로 포인터를 다 사용하지 않아도 매우 간편하게 강력한 생산성을 확보할 수 있게 되었다.

즉, 언제 어디서나 플러그인 아키텍처를 쓸 수 있게 된 것이다.

의존성 역전 : 전형적인 호출 트리에서 소스 코드 의존성의 방향은 항상 하이 레벨에서 로우 레벨 모듈로의 제어 흐름을 따르지만, 여기에 인터페이스 (런타임에 올라가지 않음)가 끼워지면 의존성 역전이 발생합니다.

이것은 프로그래머의 설계에 따라 원하는 방향으로 의존성을 설정할 수 있음을 의미합니다.

이것은 곧 업무 규칙 UI나 DB에 의존하게 되고, 소스 코드는 업무 규칙에 유연하게 하는 것을 가능하게 하고, 이것은 곧바로 배포 독립성, 개발 독립성으로 이어지는 주요한 키가 된다.


제어 흐름과 반대 소스 코드 의존성 반전

결론으로서 OOP란?

즉, SA의 관점에서 OOP는 다형성을 이용하여 시스템 전체의 소스 코드 의존성에 대한 절대적인 제어 권한을 획득할 수 있는 개발 패러다임을 의미한다.

플러그인 아키텍처가 구성될 수 있고, 고레벨 정책을 포함하는 모듈은 저레벨 세부사항을 포함하는 모듈에 대한 독립성을 보장할 수 있다.

함수형 프로그래밍

함수형 프로그래밍은 변수 할당에 부과되는 규율로 시작합니다.

java는 가변 변수를 기반으로 작동합니다.

실제로 변수의 가변성은 충돌, 교착 상태 및 동시 업데이트 문제를 일으키므로 동시성 제어에도 문제가 발생할 수 있습니다.

응용 프로그램을 올바르게 구조화하려면 변수를 변경하는 가변 컴포넌트와 변경하지 않는 불변 컴포넌트를 엄격하게 분리하고 가능한 한 많은 처리를 불변 컴포넌트로 이동해야 합니다.

스토리지 공간을 무한대에 가깝게 만들 수 있는 시대에는 상태를 저장하는 대신 트랜잭션 자체를 저장할 수 있습니다.

즉, 어플리케이션이 CRUD가 아닌 CR만을 실행시키는 경우, 종국에는 어플리케이션을 완전한 불변성을 갖도록 할 수 있다.

* 이벤트 소싱.

결론으로서, 프로그래밍의 3개의 패러다임을 참조해 보면, 툴은 바뀌고, 하드웨어도 바뀌었지만 SW코어는 여전히 그대로.

컴퓨터 프로그램은 순차/분기/반복/참조만으로 구성되며, 그 이상도 이하도 아니다.

<設計原則 - SOLID>

좋은 벽돌은 곧 깨끗한 코드입니다.

이 좋은 벽돌로 좋은 구조를 만드는 원칙은 유명한 SOLID입니다.

SOLID는 함수와 데이터 구조를 클래스에 배치하는 방법과 이러한 클래스를 서로 결합하는 방법을 설명합니다.

키워드는 다음과 같습니다.

  • 변경 유연
  • 이해하기 쉬운
  • 많은 sw 시스템에서 사용할 수 있는 컴포넌트의 기반이 됩니다.

SRP 단일 책임 원칙(Sing Responsibility Principle)

컨웨이 법칙: sw 시스템이 가질 수 있는 최적의 구조는 시스템을 만드는 조직의 사회적 구조에 큰 영향을 받는다.

따라서 각 소프트웨어 모듈은 하나의 액터에 대해서만 책임을 져야 합니다.

모듈은 협의의 의미에서 소스 파일을 의미하지만 실제로는 함수와 데이터 구조로 구성된 집합 집합입니다.

바로 이 단일 액터를 담당하는 코드를 함께 묶는 힘이 바로 응집성이다.

이를 위반하는 징후는 다음과 같습니다.

징후 1: 우발적인 중복


SRP에 제대로 위반

하나의 클래스 안에 있는 3가지 방법은 각각 매우 다른 3가지 유형의 액터를 담당하기 위해 SRP를 위반한다.

이 조인은 CFO 팀이 결정한 조치가 COO 팀에 의존하는 무언가에 영향을 줄 수 있습니다.

예를 들어, calculatePay()와 reportHours()가 과도한 근무를 제외한 정상적인 근무 시간을 계산하는 알고리즘 regularHours()를 공유하는 경우 두 메서드 모두 하나의 알고리즘을 사용하므로 재해가 발생할 수 있습니다.

있습니다.

이 문제는 다른 액터가 의존하는 코드를 너무 가깝게 하기 때문에 발생합니다.

그러므로 SRP는 서로 다른 액터가 의존하는 코드를 서로 분리한다고 말한다.

징후 2: 병합

소스 코드가 다양하고 많은 메소드를 포함한다고 하면, 운용중에 각 팀에 속하는 개발자의 개발 코드를 하나의 형상으로 병합하는 것이 비일비재로 발생한다.

위의 예에서도 CFO 팀의 DBA가 스키마를 변경하고 COO 팀 개발자가 다른 요구 사항을 구현하기 위해 일부 형식을 변경하면 클래트 파일의 소스 코드에서 병합이 발생하고 즉시 에 risk를 일으킵니다.

이 문제에서 벗어나는 방법은 하나다.

다른 액터를 뒷받침하는 코드를 서로 분리하는 것이다.

해결책

모두가 메소드를 이동하는 다른 클래스로 이동해야합니다.

가장 빠른 방법은 데이터와 메서드를 분리하는 방법이며, 어떤 메서드도 없는 data class 로서 EmployeeData 를 선언해, 3 개의 클래스가 그것을 공유하게 하는 방법입니다.


데이터 영역과 방법 영역을 분리하여 SRP 위반 해결

그러나 이것은 개발자가 세 가지 클래스를 인스턴스화하고 추적해야 하는 단점이 있기 때문에 이를 해결하기 위해 외관 패턴을 작성할 수도 있습니다.


외관 패턴으로 SRP 위반 해결

Facade에 코드는 거의 없지만, 이 클래스는 3개의 클래스의 오브젝트를 생성해, 요구된 메소드를 가지는 오브젝트에 위임하는 것을 담당합니다.

또는 가장 중요한 메서드는 기존 Employee 클래스에 employeData와 함께 유지되지만, 그다지 중요하지 않은 reportHours(), saveEmployee() 등의 메서드는 다른 클래스로 분할되어 부분 파사드 역할을 합니다.

결론

SRP는 메소드와 클래스 레벨의 원칙이지만, 이것보다 상위 레벨에서도이 아이디어는 적용됩니다.

이 원칙이 컴포넌트 레벨에서 적용되면 공통 폐쇄 원칙이 되며 아키텍처 레벨에서 아키텍처 경계를 작성하는 데 필요한 변경의 축이 될 수 있습니다.

OCP 오픈 – 폐쇄 원칙 (Open-Closed Principle)

시스템 동작을 변경하려면 기존 코드를 변경하는 대신 새 코드를 추가하는 방식으로 구현해야 합니다.

즉, sw 객체는 확장을 위해 열려 있어야 하고 변경은 닫아야 합니다.

즉, 행위는 확장 가능해야 하지만 이 대리인을 변경해서는 안 된다는 의미다.

한 번 OCP 원칙의 궁극적인 목표는 변경되는 코드의 양을 가능한 한 최소화하는 것입니다.

필요
.

예를 들어 재무 재표를 웹 페이지로 표시하는 시스템을 고려하면 두 가지 책임으로 나눌 수 있습니다.

  • 보고서 데이터를 계산할 책임
  • 이 데이터를 웹으로 표시하거나 종이로 인쇄하기에 적합한 형태로 표현할 책임

이와 같이 책임을 나누면, 어느 책임이든 변경이 발생해도, 다른 것은 변경되지 않게 소스 코드 의존성도 확실히 정리할 필요가 있습니다.

마지막으로, 새롭게 조직된 구조는 행위가 확대될 때 변경이 발생하지 않도록 보장합니다.

해야 한다.

이것을 종합하면, 처리 과정이 클래스 단위로 분할되고, 이것은 곧바로 이하와 같은 컴퍼넌트 단위로 구분된 도면이 나오게 된다.


구조화된 아키텍처

  • : 인터페이스 /// : 데이터 구조 /// 열린 화살표: 사용 관계 /// 닫힌 화살표: 구현 or 상속
  • 화살표가 A 클래스에서 B 클래스로 향하면 A에서는 B를 호출하지만 B는 A를 전혀 호출 할 수 없다는 것을 의미합니다.

  • 컴포넌트의 경계를 의미하는 이중선은 화살표와 한 방향으로만 교차합니다.

    즉, 구성 요소 관계도 단방향입니다.

마지막 줄에는 특히 의미가 있습니다.

A 컴포넌트에서 발생한 변경으로부터 B 컴포넌트를 보호하려면 A가 B에 의존해야 합니다.

presenters에서 발생한 변경으로부터 controller를 보호하고, view에서 발생한 변경으로부터 preserer를 보호, interactor는 다른 모든 것으로부터 발생한 변경으로부터 보호하는 것이다.

따라서 Interactor는 OCP에 가장 적합한 위치에 있으며, 해당 구성 요소에서 발생한 변경 사항도 interactor에 영향을 미치지 않습니다.

  • 이 인터랙터는 바로 작업 규칙을 포함하고 앱에서 가장 높은 수준의 정책을 포함합니다.

    따라서 가장 최상의 보호를 받는다.

  • view는 최소 수준의 개념으로 거의 보호되지 않습니다.

    즉, 자주 변경이 발생할 수 있습니다.

  • presentar는 view보다 높고 controller나 interactor보다 낮은 레벨에 위치한다.

이것이 바로 아키텍처 수준에서 OCP가 작동하는 방식이며, SA는 기능이 어떻게, 왜, 언제 발생하는지에 따라 기능을 분리하고, 분리된 기능을 컴포넌트 계층으로 조직화해야 한다.

그 외에도 OCP를 활용하여 컴포넌트 계층화를 진행하면서 적절하게 인터페이스를 배치하고, interactor가 직접 데이터베이스 컴포넌트에 붙지 않도록 의존성을 역전시키는 방향성 제어(FinalcialDataGateway),

controller가 interactor의 내부에 대해서 많이 모르게 하기 위한 정보는 닉(FinancialReportRequester)이 생각된다.

LSP 리스코프 치환 원칙 (Liskov Substitution Principle)

서브타입에 대한 원칙적으로 상호 교환 가능한 구성요소를 사용하여 sw 시스템을 작성할 수 있으려면, 이들 구성요소는 서로 교환 가능해야 합니다.

OOP 초기에는 LSP가 단순히 “상속”을 사용하도록 안내하는 방법이라고 생각했지만 시간이 지남에 따라 인터페이스 및 구현에도 적용되는보다 광범위한 sw 설계 원칙으로 변모했습니다.

네. 즉, 이것은 인터페이스 1개와 N개의 클래스, REST API 1개와 N개의 응답 서비스 집단 등으로 확장하여 생각할 수 있다.

올바른 예


Billing 응용 프로그램에서 calcFee()를 호출하고 License에는 PersonalLicense와 BusinessLicense라는 두 가지 하위 유형이 있습니다.

이 두 하위 유형은 서로 다른 알고리즘으로 라이센스 비용을 계산합니다.

이 디자인은 Billing 응용 프로그램의 행위가 License 하위 유형에서 무엇을 사용하는지에 전혀 의존하지 않습니다.

이 예는 모든 하위 유형이 라이센스 유형을 대체할 수 있는 경우입니다.

위반 사례


위의 예에서 Squere는 Rectangle 하위 유형에 적합하지 않습니다.

Rectangle의 높이와 너비는 서로 독립적으로 변경할 수 있지만 Square의 높이와 너비는 반드시 함께 변경되기 때문입니다.

따라서 User가 Rectangle과 대화 할 때 오류가 발생하지 않도록하려면 if 문에서 Rectangle이 실제로 Square인지 여부를 확인하는 메커니즘을 User에 추가하는 방법이 있습니다.

의 행위가 사용하는 타입에 의존하기 때문에, LSP 지킬 수 없게 된다.

택시 플랫폼의 예에서도 중계 플랫폼의 app caller는 하나이지만, N개의 택시 사업자를 연계한다고 쳤을 때, N개의 택시 사업자 서비스를 서브타입으로 치환할 수 있는 구조로 처리해야 한다 .택시 사업자별로 사양대로 개발하지 않는 케이스가 있다고 해도, db로 패턴화를 해 두도록 들쭉날쭉한 정보를 모두 분리해 버리고 공통 요소만을 코드에 남겨 LSP를 달성해야 한다.

LSP는 아키텍처 레벨까지 확장할 수 있으며 반드시 확장해야 한다.

대체 가능성을 위반하면 시스템 아키텍처가 오염되어 상당한 양의 별도의 메커니즘을 추가해야하기 때문입니다.

ISP 인터페이스 분리 원칙(Interface Segregation Principle)

SW 설계자는 사용하지 않는 것에 의존해서는 안됩니다.

여러 사용자가 OPS 클래스의 작업 op1, op2, op3 을 각각 사용하는 다음 예제를 보면 User1의 코드는 자신이 전혀 사용하지 않는 op2, op3 메서드에 의존하게 되며 이러한 종속성으로 인해 OPS 클래스 내 op2가 변경된 경우에도 User1도 컴파일 후 새로 배포해야 합니다.


사용자 클래스가 메서드에 의존합니다!

그러나 이것을 아래와 같이 오퍼레이션을 인터페이스 단위로 분리해 배치하면 해결할 수 있지만 이것이 ISP의 기본 사상이다.


사용성 기준의 메소드별로 인터페이스를 설치하고 분리

일반적으로 필요 이상으로 많은 것을 포함하는 모듈에 의존하는 것은 유해합니다.

소스 코드 종속성의 경우 불필요한 재컴파일과 재배포를 강제하고 코드 레벨보다 높은 레벨의 아키텍처 레벨에서도 동일한 조인도가 발생합니다.

DIP 의존성 역전 원칙(Dependency Inversion Principle)

높은 수준의 정책을 구현하는 코드는 낮은 수준의 세부 사항을 구현하는 코드에 절대 의존해서는 안되며 세부 사항은 정책에 의존해야합니다.

DIP에서 말하는 유연성이 극대화된 시스템이란, 소스 코드 의존성이 추상(abstract)에 의존해, 구체(concretion)에 의존하지 않는 시스템을 의미한다.

이것을 간단히 말하면 using, import, include와 같은 구문은 인터페이스나 추상 클래스와 같은 추상 선언만을 참조해야 하고, 구체적인 imple을 참조해서는 안 된다는 이야기다.

왜냐하면 구 클래스는 매우 변동성이 큰 요소이기 때문이다.

SA라면 인터페이스의 변동성을 낮추기 위해 노력하고 인터페이스를 변경하지 않아도 구현체에 기능을 추가할 수 있는 구조를 쌓도록 노력해야 한다.

이는 다음의 3가지 코딩 실천법으로 정리되어 있다.

  • 변동성이 큰 구 클래스를 참조하지 마십시오.
  • 변동성의 큰 구체적인 종류에서 파생하지 말라.
  • 구 함수를 재정의하지 마십시오.

추상 공장 패턴

그러나 실제로는 대부분의 언어로 객체를 생성하기 위해서는 그 객체를 구체적으로 정의한 코드에 대해서 소스 코드 의존성이 발생할 수밖에 없다.

이 불가피한 종속성을 처리할 때 추상 팩토리 패턴을 사용할 수 있습니다.


아키텍처 경계/의존성 역전 적용

곡선은 아키텍처 경계를 의미하며, 상부의 고레벨 영역(추상)과 하부의 저레벨 영역(구체적인)을 분리한다.

전체 소스 코드 의존성은 대응하는 곡선과 교차할 때 모두 한 방향, 즉 추상 방향으로 향한다.

그러나 실제 컴퓨팅의 제어 흐름은 위에서 설명한 “의존성 역전”정리와 같이이 아키텍처의 소스 코드 의존성과 반대 방향으로 곡선을 가로 지릅니다.

종합하면 소스 코드 의존성은 제어 흐름과 반대 방향으로 역전되어 효율적인 머티리얼 컴포넌트와 추상 컴포넌트를 분리합니다.

. 이 규칙을 종속성 규칙이라고합니다.

  • Application은 Service 인터페이스를 통해 ConcreteImpl을 사용하지만 이를 사용하려면 ConcreteImpl의 인스턴스를 만들어야 합니다.

  • Application 는, ConcreteImpl 에 대해서 직접 소스 코드 의존성을 작성하지 않고 , 이 목적을 달성하기 위해서 ServiceFactory 로부터 파생한 ServiceFactory 인터페이스의 makeSvc() 를 직접 호출합니다.

  • 이 makeSvc() 메소드는 ServiceFactory 인터페이스로부터 파생한 ServiceFactoryImple 로 구현되어,
  • ServiceFactoryImpl 구현체가 ConcreteImpl 의 인스턴스를 생성한 후, Service 형에 돌려주는 형태이다.