소개
클린아키텍처: 소프트웨어 구조와 설계의 원칙 책을 읽고 정리하며 소감을 적는 포스트입니다.
계층과 경계
시스템이 세 가지 컴포넌트(UI, 업무 규칙, 데이터베이스)로만 구성된다고 생각하기 쉽다. 하지만 대다수의 시스템에서 컴포넌트 개수는 이보다 훨씬 많다.
움퍼스 사냥 게임
1972년에 발매된 인기있는 모험게임인 움퍼스 사냥(Hunt thw Wumpus)는 텍스트를 기반으로 하는 GO EAST와 SHOOT WEST와 같은 매우 단순한 명령어를 사용한다.
여기서 텍스트 기반 UI는 그대로 유지하되, 게임 규칙과 UI를 분리해서 우리 제품을 여러 시장에서 다양한 언어로 발매할 수 있게 만든다고 가정해 보자. 게임 규칙은 언어 독립적인 API를 사용해서 UI 컴포넌트와 통신할 것이고, UI는 API를 사람이 이해할 수 있는 언어로 변환할 것이다.
아래의 그림과 같이 의존성을 적절히 관리하면 UI 컴포넌트가 어떤 언어를 사용해도 게임 규칙을 재사용 할 수 있다.
UI 컴포넌트가 어떤 언어를 사용하더라도 게임 규칙을 재사용할 수 있다.
아래의 그림과 같이 의존성 규칙을 준수하여 게임 규칙이 데이터 저장소 컴포넌트와 통신 할 때 우리는 게임 규칙이 다양한 종류의 데이터 저장소에 대해 알지 않기를 원한다.
의존성 규칙 준수하기
클린 아키텍처?
위 예제와 같은 단순한 시스템은 클린 아키텍처 접근법을 적용해서 유스케이스, 경계, 엔티티 그리고 관련된 데이터 구조를 모두 만드는 일도 쉬운 일이다. 그런데 정말 아키텍처 경계를 모두 발견한 것인가??
예를 들어 UI에서 언어가 유일한 변경점은 아니다. 텍스트를 주고 받는 메커니즘을 다양하게 만들고 싶을 수도 있다. 예를 들어 셸(shell)창을 사용하거나 채팅 프로그램으로 할 수도 있다.
다양한 가능성이 존재하므로 변경의 축에 의해 정의되는 아키텍처 경계가 잠재되어 있을 수 있다.
개선된 다이어그램
점선으로 된 테두리는 API를 정의하는 추상 컴포넌트를 가리키며, 해당 API는 추상 컴포넌트 위나 아래의 컴포넌트가 구현된다.
- TextDelivery : SMS, Console
- Language : English, Spanish
- Data Storage : Cloud Data, Flash Data
위 경우에 해당하는 Boundary 인터페이스가 정의하는 API는 의존성 흐름의 상위에 위치한 컴포넌트에 속한다.
이러한 변형들을 모두 제거하고 순전히 API 컴포넌트만 집중하면 다이어그램을 단순화 할 수 있다.
단순화된 다이어그램
위 다이어그램은 모든 화살표가 위를 향하도록 맞춰졌다는 점에 주목해야 한다.
모든 입력은 사용자로부터 전달 받아 TextDelivery를 통해 Language를 거쳐 GameRules에 적합한 명령어로 변역된다.
GrameRules는 사용자 입력을 처리하고, 우측 하단의 DataStorage로 데이터를 내려 보낸다.
이 구성은 데이터 흐름을 두 개의 흐름을 효과적으로 분리한다.
흐름 횡단하기
위 예제처럼 데이터 흐름이 항상 두 가지 일까? 절대아니다. 움퍼스 사냥 게임을 네트워크상에서 여러 사람이 함께 플레이할 수 있게 만든다고 해보자
아래와 같이 네트워크(NETWORK) 컴포넌트를 추가해야 한다.
Network 컴포넌트 추가하기
위 흐름은 GameRules가 모두 제어한다. 따라서 시스템이 복잡해질수록 컴포넌트 구조는 더 많은 흐름으로 분리될 것이다.
흐름 분리하기
이쯤 되면 모든 흐름이 결국에는 상단의 단일 컴포넌트에서 서로 만난다고 생각할 수 있다. 하지만 현실을 훨씬 복잡하다.
움퍼스 사냥 게임의 GameRules 컴포넌트를 보면 게임 규칙 중일부는 지도와 관련된 메커니즘을 처리한다. 동굴의 연결, 물체의 위치 등을 알고 있다.
하지만 이보다 더 높은 수준에서 또 다른 정책 집합이 존재한다. 즉, 플레이어 생명령, 사건 해결 비용, 소등 등 이다.
저수준의 정책은 고수준 정책에게 FellInFit(구덩이에빠짐)과 같은 사건이 발생했음을 알린다.
고수준의 정책은 플레이어를 관리한다.
이것이 아키텍처 경계일까? MoveManagement와 PalyerManagement를 분리해야할까? 이 예제를 더 흥미롭게하는 마이크로서비스까지 추가해 보자.
대규모 플레이어가 동시에 플레이 할 수 있는 버전은 움퍼스 사냥 게임이 있다고 가정해보자. MoveManagement는 플레이어의 컴퓨터에서 처리되지만 PlayerManagement는 서버에서 처리된다.
PlayerManagement는 접속된 모든 MoveManagement 컴포넌트에 마이크로서비스 API를 제공한다.
마이크로서비스 API 추가하기
MoveManagement와 PlayerManagement 사이에는 완벽한 형테의 아키텍처 경계가 존재한다.
결론
아키텍트로서 우리는 아키텍처 경계가 언제 필요한지를 신중하게 파악해내야 한다. 또한 우리는 이러한 경계를 제대로 구현하려면 비용이 많이 든다는 사실도 인지하고 있어야 한다.
그러면 우리는 어떻게 해야하나? 매우 똑똑한 사람들이 수년간 말해왔듯이, 우상화가 필요하리라고 미리 예측해서느 안 된다. 이것이 바로 YAGNU(You Aren’t Going to Need It)이 말하는 철학이다. 오버 엔지니어링(over engineering)이 언더 엔지니어링(under engineering)보다 나쁠 때가 훨씬 많기 때문이다.
우리는 비용을 산정하고 어디에 아키텍처 경계를 둬야 할지, 그리고 완벽하게 구현할 경계는 무엇인지와 부분적으로 구현할 경계와 무시할 경계는 무엇인지를 결정해야 한다.
하지만 이는 일회성 결정이 아니다. 프로젝트 초반에는 구현할 경계가 무엇인지와 무시할 경계가 무엇인지를 쉽게 결정할 수 없다. 대신 경계가 필요할 수도 있는 부분에 주목하고, 경계가 존재하지 않아 생기는 마찰의 어렴풋한 첫 조짐을 신중하게 관차랳야 한다.