클린 아키텍처 (Chapter 1~11) - Robert C. Martin


소개

1. 설계와 아키텍처란?

컴퓨터 프로그래밍을 하는 관행을 정말 유심히 관찰해 보면 지난 50년 동안 변한 게 거의 없다는 사실을 깨달을 것이다. 언어는 조금 발전했다.
도구는 환상적으로 좋아졌다. 하지만 컴퓨터 프로그래밍을 이루는 기본 구성요소는 조금도 바뀌지 않았다.

빨리 가는 유일한 방법은 제대로 가는 것이다.

어떤 경우라도 개발 조직이 할 수 있는 최고의 선택지는 조직에 스며든 과신을 인지하여 방지하고, 소프트웨어 아키텍처의 품질을 심각하게 고민하기 시작하는 것이다.
소프트웨어 아키텍처를 심각하게 고려할 수 있으려면 좋은 소프트웨어 아키텍처가 무엇인지 이해해야 한다.
비용은 최소화하고 생산성은 최대화할 수 있는 설계와 아키텍처를 가진 시스템을 만들려면, 이러한 결과로 이끌어줄 시스템 아키텍처가 지닌 속성을 알고 있어야 한다.



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

소프트웨어의 첫 번째 가치는 행위(behavior)다. 소프트웨어의 두 번째 가치는 ‘소프트웨어(Software)’라는 단어와 관련이 있다. 부드러운(Soft)라는 바로 이 단어에 두 번째 가치가 존재한다.
소프트웨어는 반드시 부드러워야 한다. 다시 말해 이는 변경하기 쉬워야 한다.

소프트웨어의 첫 번째 가치인 행위는 긴급하지만 매번 높은 중요도를 가지는 것은 아니다. 소프트웨어의 두 번째 가치인 아키텍처는 중요하지만 즉각적인 긴급성을 필요로 하는 경우는 절대 없다.
우선순위는 1: 긴급하고 중요한, 2: 긴급하진 않지만 중요한, 3: 긴급하지만 중요하지 않은, 4: 긴급하지도 중요하지도 않은
아키텍처, 즉 중요한 일은 이 항목의 가장 높은 두 순위를 차지하는 반면, 행위는 첫 번째와 세 번째에 위차한다는 점을 주목하자.
업무 관리자와 개발자가 흔하게 저지르는 실수는 세 번째에 위치한 항목을 첫 번째로 격상시켜버리는 일이다.

아키텍처를 위해 투쟁하라. 이러한 도전은 당신이 소프트웨어 아키텍트라면 두 배로 중요해진다. 맡은 업무 자체를 봐도 소프트웨어 아키텍트는 시스템이 제공하는 특성이나 기능보다는 시스템의 구조에 더 중점을 둔다.
아키텍트는 이러한 특성과 기능을 개발하기 쉽고, 간편하게 수정할 수 있으며, 확장하기 쉬운 아키텍처를 만들어야 한다.
아키텍처가 후순위가 되면 시스템을 개발하는 비용이 더 많이 들고, 일부 또는 전체 시스템에 변경을 가하는 일이 현실적으로 불가능해진다. 이러한 상황이 발생하도록 용납했다면, 이는 결국 소프트웨어 개발팀이 스스로 옳다고 믿는 가치를 위해 충분히 투쟁하지 않았다는 뜻이다.



프로그래밍 패러다임

3. 패러다임 개요

  • 구조적 프로그래밍
    데이크스트라는 무분별한 점프(goto 문장)는 프로그램 구조에 해롭다는 사실을 제시했다.
    구조적 프로그래밍은 제어흐름의 직접적인 전환에 대해 규칙을 부과한다.

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

  • 함수형 프로그래밍
    함수형 프로그래밍은 할당문에 대해 규칙을 부과한다.

세 가지 패러다임 각각은 우리에게서 goto문, 함수 포인터, 할당문을 앗아간다. 우리에게서 가져갈 수 있는게 더 남아 있는가? 아마 없을 것이다.
따라서 프로그래밍 패러다임은 앞으로도 딱 세 가지밖에 없을 것이다.



4. 구조적 프로그래밍

구조적 프로그래밍은 프로그램을 증명 가능한 세부 기능 집합으로 재귀적으로 분해할 것을 강요한다. 그러고 나서 테스트를 통해 증명 가능한 세부 기능들이 거짓인지를 증명하려고 시도한다.
이처럼 거짓임을 증명하려는 테스트가 실패한다면, 이 기능들은 목표에 부합할 만큼은 충분히 참이라고 여기게 된다.
(프로그램이 잘못되었음을 테스트를 통해 증명할 수는 있지만, 프로그램이 맞다고 증명할 수는 없다. 테스트에 충분한 노력을 들였다면 테스트가 보장할 수 있는 것은 프로그램이 목표에 부합할 만큼은 충분히 참이라고 여길 수 있게 해주는 것이 전부다.)



5. 객체지향 프로그래밍

좋은 아키텍처를 만드는 일은 객체 지향(Object-Oriented) 설계 원칙을 이해하고 응용하는 데서 출발한다. 그렇다면 대채 OO란 무엇인가?

OO의 본질을 설명하기 위해 세 가지 주문에 기대는 부류도 있는데, 캡슐화(Encapsulation), 상속(Inheritance), 다형성(Polymorphism)이 바로 그 주문이다.
이들은 OO가 이 세 가지 개념을 적절하게 조합한 것이거나, 또는 OO언어는 최소한 세 가지 요소를 반드시 지원해야 한다고 말한다.

OO란 무엇인가? 소프트웨어 아키텍트 관점에서 정답은 명백하다.
OO란 다형성을 이용하여 전체 시스템의 모든 소스 코드 의존성에 대한 절대적인 제어 권한을 획득할 수 있는 능력이다.

OO를 사용하면 아키텍트는 플러그인 아키텍처를 구성할 수 있고, 이를 통해 고수준의 정책을 포함하는 모듈은 저수준의 세부사항을 포함하는 모듈에 대해 독립성을 보장할 수 있다.
저수준의 세부사항은 중요도가 낮은 플러그인 모듈로 만들 수 있고, 고수준의 정책을 포함하는 모듈과는 독립적으로 개발하고 배포할 수 있다.

clean_archit001


clean_archit002
제어의 역전 (Dependency Inversion)



6. 함수형 프로그래밍

25까지의 정수의 제곱을 출력하는 간단한 문제

Java

public class Squint {
  public static void main(String args[]){
      for(int i=0; i<25; i++){
          System.out.println(i*i);
      }
  }
}

Clojure(함수형 언어)

(println (take 25 (map (fn [x] (* x x)) (range))))

자바 프로그램은 가변 변수(mutable variable)를 사용하는데, 가변 변수는 프로그램 실행 중에 상태가 변할 수 있다.
클로저 프로그램에서는 이러한 가변 변수가 전혀 없다. 클로저에서는 x와 같은 변수가 한 번 초기화되면 절대로 변하지 않는다.
함수형 언어에서 변수는 변경되지 않는다.

아키텍처를 고려할 때 이러한 내용이 왜 중요한가? 아키텍트는 왜 변수의 가변성을 염려하는가? 터무니없게도 대답은 단순하다.
경합(race), 조건, 교착상태(deadlock), 동시 업데이트(concurrent update)문제가 모두 가변 변수로 인해 발생하기 때문이다.
만약 어떠한 변수도 갱신되지 않는다면 경합 조건이나 동시 업데이트 문제가 일어나지 않는다. 락(lock)이 가변적이지 않다면 교착상태도 일어나지 않는다.

아키텍트라면 동시성(concurrency)문제에 지대한 관심을 가져야만 한다. 우리는 스레드와 프로세스가 여러 개인 상황에서도 설계한 시스템이 여전히 강건하기를 바란다.
그렇다면 이제 불변성이 정말로 실현 가능한지를 스스로에게 반드시 물어봐야 한다.

불변성과 관련하여 가장 주요한 타협 중 하나는 애플리케이션, 또는 애플리케이션 내부의 서비스를 가변 컴포턴트와 불변 컴포넌트로 분리하는 일이다.
불변 컴포넌트에서는 순수하게 함수형 방식으로만 작업이 처리되며, 어떤 가변 변수도 사용되지 않는다.
불변 컴포넌트는 변수의 상태를 변경할 수 있는, 즉 순수 함수형 컴포넌트가 아닌 하나 이상의 다른 컴퐅넌트와 서로 통신한다.
상태 변경은 컴포넌트를 갖가지 동시성 문제에 노출하는 꼴이므로, 흔히 트랜잭션 메모리(transactional memory)와 같은 실천법을 사용하여 동시 업데이트와 경합 조건 문제로부터 가변 변수를 보호한다.

clean_archit003

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

이벤트 소싱
이벤트 소싱은 상태가 아닌 트랜잭션을 저장하자는 전략이다. 상태가 필요해지면 단순히 상태의 시작점부터 모든 트랜잭션을 처리한다.

Ex) 계좌 잔고를 관리하는 은행 애플리케이션에서는 입금 트랜잭션과 출금 트랜잭션이 실행되면 잔고를 변경해야 한다. 
이제 계좌 잔고를 변경하는 대신 트랜잭션 자체를 저장한다고 상상해보자. 
누군가 잔고 조회를 요청할 때마다 계좌 개설 시점부터 발생한 모든 트랜잭션을 단순히 더한다. 
이 전략에서는 가변 변수가 하나도 필요 없다. 당연하게도 이러한 접근법은 터무니 없다. 시간이 지날수록 트랜잭션 수는 끝없이 증가하고, 잔고 계산에 필요한 컴퓨팅 자원은 걷잡을 수 없이 커진다.  
따라서 이 전략이 영원히 실현 가능하려면 무한한 저장 공간과 무한한 처리 능력이 필요하다.  

더 중요한 점은 데이터 저장소에서 삭제되거나 변경되는 것이 하나도 없다는 사실이다. 결과적으로 애플리케이션은 CRUD가 아니라 그저 CR만 수행한다.
또한 데이터 저장소에서 변경과 삭제가 전혀 발생하지 않으므로 동시 업데이트 문제 또한 일어나지 않는다.
저장 공간과 처리 능력이 충분하면 애플리케이션이 완전한 불변성을 갖도록 만들 수 있고, 따라서 완전한 함수형으로 만들 수 있다.

결론
구조적 프로그래밍은 제어흐름의 직접적인 전환에 부과되는 규율이다.

객체 지향 프로그래밍은 제어흐름의 간접적인 전환에 부과되는 규율이다.

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

이들 세 패러다임 모두 우리에게서 무언가를 앗아갔다. 각 패러다임은 우리가 코드를 작성하는 방식의 형태를 한정시킨다.
어떤 패러다임도 우리의 권한이나 능력에 무언가를 보태지 않는다. 지난 반세기 동안 우리가 배운 것은 해서는 안 되는 것에 대해서다.
이 사실을 깨닫는다면 우리는 환영받지 못할 사실, 즉 소프트웨어는 급격히 발전하는 기술이 아니라는 진실과 마주하게 된다.

앨런 튜링이 최초의 코드를 작성할 때 사용한 소프트웨어규칙과 지금의 소프트웨어 규칙은 조금도 다르지 않다.
도구는 달라졌고 하드웨어도 변했지만, 소프트웨어의 핵심은 여전히 그대로다.
소프트웨어, 즉 컴퓨터 프로그램은 순차(sequence), 분기(selection), 반복(iteration), 참조(indirection)로 구성된다. 그 이상도 이하도 아니다.



설계 원칙

좋은 소프트웨어 시스템은 깔끔한 코드(clean code)로 부터 시작한다. 좋은 벽돌을 사용하지 않으면 빌딩의 아키텍처가 좋고 나쁨은 그리 큰 의미가 없는 것과 같다.
반대로 좋은 벽돌을 사용하더라도 빌딩의 아키텍처를 엉망으로 만들 수 있다. 그래서 좋은 벽돌로 좋은 아키텍처를 정의하는 원칙이 필요한데, 그게 바로 SOLID다.

SOLID원칙의 목적은 중간 수준의 소프트웨어 구조가 아래와 같도록 만드는 데 있다.

  • 변경에 유연하다.
  • 이해하기 쉽다.
  • 많은 소프트웨어 시스템에 사용될 수 있는 컴포넌트의 기반이 된다.

SRP: 단일 책임 원칙 (Single Responsibility Principle)
콘웨이(conway) 법칙에 따른 따름정리: 소프트웨어 시스템이 가질 수 있는 최적의 구조는 시스템을 만드는 조직의 사회적 구조에 커다란 영향을 받는다.
따라서 각 소프트웨어 모듈은 변경의 이유가 하나, 단 하나여야만 한다.

OCP: 개방-폐쇄 원칙 (Open-Closed Principle)
1980년대에 버트란트 마이어에 의해 유명해진 원칙이다.
기존 코드를 수정하기보다는 반드시 새로운 코드를 추가하는 방식으로 시스템의 행위를 변경할 수 있도록 설계해야만 소프트웨어 시스템을 쉽게 변경할 수 있다는 것이 이 원칙의 요지이다.

LSP: 리스코프 치환 법칙 (Liskov Substitution Principle)
1988년 바바라 리스코프가 정의한, 하위 타입(subtype)에 관한 유명한 원칙이다.
요약하면, 상호 대체 가능한 구성요소를 이용해 소프트웨어 시스템을 만들 수 있으려면, 이들 구성요소는 반드시 서로 치환 가능해야 한다는 계약을 반드시 지켜야 한다.

ISP: 인터페이스 분리 원칙 (Interface Segregation Principle)
이 원칙에 따르면 소프트웨어 설계자는 사용하지 않은 것에 의존하지 않아야 한다.

DIP: 의존성 역전 원칙 (Dependency Inversion Principle)
고수준 정책을 구현하는 코드는 저수준 세부사항을 구현하는 코드에 절대로 의존해서는 안 된다. 대신 세부사항이 정책에 의존행야 한다.



7. SRP: 단일 책임 원칙

단인 모듈은 변경의 이유가 하나, 오직 하나뿐이어야 한다.
하나의 모듈은 하나의, 오직 하나의 사용자 또는 이해관계자에 대해서만 책임져야 한다.

징후 1: 우발적 중복
이 클래스는 세 가지 메서드 calculatePay(), reportHours(), save()를 가진다.
clean_archit004

이 클래스는 SRP를 위반하는데, 이들 세 가지 메서드가 서로 매우 다른 세 명의 액터를 책임지기 때문이다.

  • calculatePay() 메서드는 회계팀에서 기능을 정의하며, CFO 보고를 위해 사용한다.
  • reportHours() 메서드는 인사팀에서 기능을 정의하고 사용하며, COO 보고를 위해 사용한다.
  • save() 메서드는 데이터베이스 관리자가 기능을 정의하고, CTO 보고를 위해 사용한다.

개발자가 이 세 메서드를 Employee라는 단일 클래스에 배치하여 세 액터가 서로 결합되어 버렸다. 이 결합으로 인해 CFO 팀에서 결정한 조치가 coo팀이 의존하는 무언가에 영향을 줄 수 있다.

징후 2: 병합
최근 도구는 굉장히 뛰어나지만, 어떤 도구도 병합이 발생하는 모든 경우를 해결할 수는 없다. 결국 병합에는 항상 위험이 뒤따르게 된다.
이 문제를 벗어나는 방법은 서로 다른 액터를 뒷받침하는 코드를 서로 분리하는 것이다.

해결책
가장 확실한 해결책은 데이터와 메서드를 분리하는 방식일 것이다.

clean_archit005

아무런 메서드가 없는 간단한 데이터 구조인 EmployeeData 클래스를 만들어, 세 개의 클래스가 공유하도록 한다.
각 클래스는 자신의 메서드에 반드시 필요한 소스 코드만을 포함한다. 세 클래스는 서로의 존재를 몰라야 한다. 따라서 ‘우연한 중복’을 피할 수 있다.


clean_archit006

반면 위의 해결책은 개발자가 세 가지 클래스를 인스턴스화하고 추적해야 한다는 게 단점이다. 이러한 난관에서 빠져나올 때 흔히 쓰는 기법으로 파사드(Facade) 패턴이 있다.
EmployeeFacade에 코드는 거의 없다. 이 클래스는 세 클래스의 객체를 생성하고, 요청된 메서드를 가지는 객체로 위임하는 일을 책임진다.


clean_archit007

어떤 개발자는 가장 중요한 업무 규칙을 데이터와 가깝게 배치하는 방식을 선호한다. 이 경우라면 기존의 Employee 클래스에 그대로 유지하되, Employee 클래스를 덜 중요한 나머지 메서드들에 대한 Facade로 사용할 수도 있다.

단일 책임 원칙(SRP)는 메서드와 클래스 수준의 원칙이다.



8. OCP: 개방-폐쇄 원칙

소프트웨어 개체(artifact)는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다. 다시 말해 소프트웨어 개체의 행위는 확장할 수 있어야 하지만, 이때 개체를 변경해서는 안 된다.

clean_archit008

화살표가 열려 있다면 사용(using)관계이며, 닫혀 있다면 구현(implement)관계 또는 상속(inheritance)관계다. 여기에서 주목할 점은 모든 의존성이 소스 코드 의존성을 나타낸다는 사실이다. 여기에서 주목해야할 또 다른 점은 이중선은 화살표와 오직 한 방향으로만 교차한다는 사실이다.



clean_archit009

모든 컴포넌트 관계는 단 방향으로 이루어진다는 뜻이다. 이들 화살표는 변경으로부터 보호하려는 컴포넌트를 향하도록 그려진다.
A 컴포넌트에서 발생한 변경으로부터 B 컴포넌트를 보호하려면 반드시 A 컴포넌트가 B 컴포넌트에 의존해야 한다.
Interactor는 OCP를 가장 잘 준수할 수 있는 곳에 위치한다. Database, Controller, Presenter, View에서 발생한 어떤 변경도 Interactor에 영향을 주지 않는다.
왜 Interactor가 이처럼 특별한 위치를 차지해야만 하는가? 그 이유는 바로 Interactor가 업무 규칙을 포함하기 때문이다.
Interactor는 애플리케이션에서 가장 높은 수준의 정책을 포함한다.

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

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



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

여기에서 필요한 것은 당므과 같은 치환(substitution)원칙이다. S 타입의 객체 o1 각각에 대응하는 T 타입 객체 o2가 있고,
T 타입을 이용해서 정의한 모든 프로그램 P에서 o2의 자리에 o1을 치환하더라도 P의 행위가 변하지 않는다면, S는 T의 하위 타입이다.

clean_archit011
이 설계는 LSP를 준수하는데, Billing 애플리케이션의 행위가 License 하위 타입 중 무엇을 사용하는지에 전혀 의존하지 않기 때문이다. 이들 하위 타입은 모두 License 타입을 치환할 수 있다.


clean_archit012
LSP를 위반하는 전형적인 문제로는 정사각형/직사각형(square/rectangle) 문제가 있다.
이 예제에서 Square는 Rectangle의 하위 타입으로는 적합하지 않다. 이런 형태의 LSP 위반을 막기 위한 유일한 방법은 (if문 등을 이용해서) Rectangle이 실제로는 Square인지를 검사하는 메커니즘을 User에 추가하는 것이다.
하지만 이렇게 하면 User의 행위가 사용하는 타입에 의존하게 되므로, 결국 타입을 서로 치환할 수 없게 된다.

결론
LSP는 아키텍처 수준까지 확장할 수 있고, 반드시 확장해야만 한다. 치환 가능성을 조금이라도 위배하면 시스템 아키텍처가 오염되어 상당량의 별도 메커니즘을 추가해야 할 수 있기 때문이다.



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

인터페이스 분리 원칙(ISP)는 아래 그림에서 그 이름이 유래했다.

clean_archit013
User1은 오직 op1을, User2는 op2만을, User3는 op3만을 사용한다고 가정해 보자.
이 경우 User1에서는 op2, op3를 전혀 사용하지 않음에도 User1의 소스 코드는 이 두 메서드에 의존하게 된다.

clean_archit014
이러한 문제는 그림에서 보는 것처럼 오퍼레이션을 인터페이스 단위로 분리하여 해결할 수 있다.

결론
불필요한 짐을 실은 무언가에 의존하면 예상치도 못한 문제에 빠진다는 사실이다.
이 아이디어는 13장 “컴포넌트 응집도”에서 공통 재사용 원칙을 논할 때 더 자세히 다루겠다.



11. DIP: 의존성 역전 원칙

의존성 역전 원칙(DIP)에서 말하는 ‘유연성이 극대화된 시스템’이란 소스 코드 의존성이 추상(abstraction)에 의존하며 구체(concretion)에는 의존하지 않는 시스템이다.
자바와 같은 정적 타입 언어에서 이 말은 use, import, include 구문은 오직 인터페이스나 추상 클래스 같은 추상적인 선언만을 참조해야 한다는 뜻이다. 구체적인 대상에는 절대로 의존해서는 안 된다.
이 아이디어를 규칙으로 보기는 확실히 비현실적이다. 소프트웨어 시스템이라면 구체적인 많은 장치에 반드시 의존하기 때문이다.
예를 들어 자바에서 String은 구체 클래스이며, 이를 애써 추상 클래스로 만들려는 시도는 현실성이 없다. java.lang.String 구체 클래스에 대한 소스 코드 의존성은 벗어날 수 없고, 벗어나서도 안 된다.

이러한 이유로 DIP를 논할 때 운영체제나 플랫폼 같이 안정성이 보장된 환경에 대해서는 무시하는 편이다.
우리는 이들 환경에 대한 의존성은 용납하는데, 변경되지 않는다면 의존할 수 있다는 사실을 이미 알고 있기 때문이다.
우리가 의존하지 않도록 피하고자 하는 것은 바로 변동성이 큰(volatile) 구체적인 요소다. 그리고 이 구체적인 요소는 우리가 열심히 개발하는 중이라 자주 변경될 수밖에 없는 모듈들이다.

추상 인터페이스에 변경이 생기면 이를 구체화한 구현체들도 따라서 수정해야 한다.
반대로 구체적인 구현체에 변경이 생기더라도 그 구현체가 구현하는 인터페이스는 항상, 좀 더 정확히 말하면 대다수의 경우 변경될 필요가 없다. 따라서 인터페이스는 구현체보다 변동성이 낮다.

실제로 뛰어난 소프트웨어 설계자와 아키텍트라면 인터페이스의 변동성을 낮추기 위해 애쓴다.
인터페이스를 변경하지 않고도 구현체에 기능을 추가 할 수 있는 방법을 찾기 위해 노력한다. 이는 소프트웨어 설계의 기본이다.

  • 변동성이 큰 구체 클래스를 참조하지 말라
    대신 추상 인터페이스를 참조하라

  • 변동성이 큰 구체 클래스로부터 파생하지 말라

  • 구체 함수를 오버라이드 하지 말라

  • 구체적이며 변동성이 크다면 절대로 그 이름을 언급하지 말라


팩토리
이 규칙들을 준수하려면 변동성이 큰 구체적인 객체는 특별히 주의해서 생성해야 한다. 이러한 점은 조심하는 게 당연한데, 사실상 모든 언어에서 객체를 생성하려면 해당 객체를 구체적으로 정의한 코드에 대해 소스 코드 의존성이 발생하기 때문이다.
자바 등 대다수의 객체 지향 언어에서 이처럼 바람직하지 못한 의존성을 처리할 때 추상 팩토리를 사용하곤 한다.

clean_archit015

그림에서 추상 팩토리를 사용한 구조를 볼 수 있다. Application은 Service 인터페이스를 통해 ConcreteImpl을 사용하지만, Application에서는 어떤 식으로든 ConcreteImpl의 인스턴스를 생성해야 한다.
ConcreteImpl에 대해 소스 코드 의존성을 만들지 않으면서 이 목적을 이루기 위해 Application은 ServiceFactory 인터페이스의 makeSvc 메서드를 호출한다.
이 메서드는 ServiceFactory로부터 파생된 ServiceFactoryImpl에서 구현된다. 그리고 ServiceFactoryImpl 구현체가 ConcreteImpl의 인스턴스를 생성한 후 Service 타입으로 반환한다.

그림에서 곡선은 아키텍처 경계를 뜻한다. 이 곡선은 구체적인 것들로부터 추상적인 것들을 분리한다. 소스 코드 의존성은 해당 곡선과 교차할 때 모두 한 방향, 즉 추상적인 쪽으로 향한다.

곡선은 시스템을 두 가지 컴포넌트로 분리한다. 하나는 추상 컴포넌트이며, 다른 하나는 구체 컴포넌트다.
추상 컴포넌트는 애플리케이션의 모든 고수준 업무 규칙을 포함한다. 구체 컴포넌트는 업무 규칙을 다루기 위해 필요한 모든 세부사항을 포함한다.

결론
DIP는 아키텍처 다이어그램에서 가장 눈에 드러나는 원칙이 될 것이다. 위의 그림에서 곡선은 이후의 장에서는 아키텍처 경계가 될 것이다.
그리고 의존성은 이 곡선을 경계로, 더 추상적인 엔티티가 있는 쪽으로만 향한다. 추후 이 규칙은 의존성 규칙(Dependency Rule)이라 부를 것이다.





Reference

  • 클린 아키텍처 (로버트 C. 마틴)

Tag: [ book  clean-architecture  solid  srp  ocp  lsp  dip  ]