‘클린 아키텍처’ 기술 서적에 대해 학습했던 내용을 정리하기 위한 목적의 TIL 포스팅입니다.🙆♂️
- 시스템은 보통 세 가지 컴포넌트(UI, 업무 규칙, 데이텅베이스)로만 구성된다 생각하기 쉽지만, 하지만 대다수 시스템은 이보다 훨씬 많다.
움퍼스 사냥 게임
- 위 이미지처럼 소스 코드 의존성을 관리하면, UI 컴포넌트가 어떤 언어를 사용하더라도 게임 규칙을 재사용할 수 있다.
- 게임 규칙은 어떤 종류의 인간 언어가 사용되는지 알지도 못하며 신경 쓸 이유도 없다.
- 또한 게임 규칙은 데이터 저장소와 통신할때 마찬가지로 의존성 관리가 되어 게임 규칙을 의존하는 형태가 되어야 한다.
클린 아키텍처?
- 위 예제의 맥락이라면 클린 아키텍처 접근법을 적용해서 유스케이스, 경계, 엔티티, 그리고 관련된 데이터 구조를 모두 만드는 일도 쉬운 일이다. 그런데 중요한 아키텍처 경계를 정말 ㄷ모두 발견한 걸까?
- 예를 들어 UI에서 언어가 유일한 변경의 축은 아니기 때문에 변경의 축에 의해 정의 되는 잠재된 아키텍처 경계가 있을 수 있다. 텍스트를 주고받는 메커니즘, 데이터 저장 매커니즘등을 다양하고 만들고 싶을수도 있다..
- 이 모든 경우에 해당 Boundary 인터페이스가 정의하는 API는 의존성 흐름의 상위에 위치한 컴포넌트에 속한다.
- 예를 들어, GameRules 를 들여다 보면 GameRules 내부 코드에서 사용하고 Language 내부 코드에서 구현하는 다형적 Boundary 인터페이스를 발견할 수 있다.
- English, SMS, CloudData 와 같은 변형들은 추상 API 컴포넌트가 정의하는 다형적 인터페이스를 통해 제공되고, 실제 서비스하는 구체 컴포넌트가 해당 인터페이스를 구현한다.
- 예를 들어, Language 가 정의하는 다형적 인터페이스는 English 나 Spanish 가 구현할 것이다.
- 변형들을 모두 제거하고 순전한 API컴포넌트만 집중하면 위 다이어그램과 같이 단순화할 수 있다.
- 모든 화살표가 위를 향하도록 맞춰졌다는 점에 주목하자. 그 결과 GameRules 는 최상위 수준의 정책을 가지는 컴포넌트이므로 이치에 맞는 배치다.
- 정보가 흐르는 방향을 생각해 보자.
- 모든 입력은 사용자로부터 전달받아 좌측 하단의 TextDelivery 컴포넌트로 전달된다.
- 이 정보는 Language 컴포넌트를 거쳐 위로 올라가며, GameRules 에 적합한 명령어로 번역된다.
- GamesRules 는 사용자 입력을 처리 후, 우측 하단의 DataStorage 로 적절한 데이터를 내보낸다.
- 그런후 GameRules 는 Language 로 출력을 되돌려 보내고, Language 는 API를 다시 적절한 언어로 번역한 후 번역된 언어를 TextDelivery 를 통해 사용자에게 전달한다.
- 이 구성은 데이터 흐름을 두 개로 효과적으로 분리한다.
- 왼쪽 흐름은 사용자와의 통신에 관여하며, 오른쪽 흐름은 데이터 영속성에 관여한다.
- 두 흐름은 상단의 GameRules 에서 서로 만나며, GameRules는 두 흐름이 모두 거치게 되는 데이터에 대한 최종 처리기가 된다.
흐름 횡단하기
- 데이터 흐름은 시스템이 복잡해질수록 컴포넌트 구조가 더 많은 흐름으로 분리될 것이다.
- 예를 들어, 움퍼스 사냥 게임을 네트워크 상에서 여러사람이 함께 플레이할 수 있게 만든다 할 때 네트워크 컴포넌트를 추가해야 한다.
- 이때 데이터 흐름은 세 개로 분리되며, 이들 흐름은 GameRules 가 제어한다.
흐름 분리하기
- 이쯤 되면 모든 흐름이 결국엔 상단의 단일 컴포넌트에서 서로 만난다고 생각할 수 있지만, 현실은 훨씬 복잡하다.
- 움퍼스 게임에서 게임 규칙중 일부인 지도 관련 메커니즘을 처리하는
MoveManagement
를 추가한 경우 지도규칙에 의해 플레이어의 지도 관련 사건을 처리한다. 게임중 구덩이에 빠지면
MoveManagement
은 이를 판단한 후 고수준인 PlayerManagement정책에게 알려 플레이어의 승리 여부 상태를 결정해준다.- 여기서 좀 더 흥미롭게 마이크로서비스까지 추가해보자.
(p.242 그림 25.7 참고)
- 대규모 플레이어가 동시에 플레이할 수 있는 버전의 움퍼스 사냥 게임이 있다고 가정해보자.
- MoveManagement 는 플레이어 컴퓨터에서 직접 처리되지만 PlayerManagement 는 서버에서 처리된다.
- PlayerManagement 는 접속된 모든 MoveManagement 컴포넌트에 마이크로서비스 API를 제공한다.
- 이러할 때 MoveManagement 와 PlayerManagement는 사이에는 아키텍처 경계가 생기게 된다.
결론
- 위 움퍼스 사냥 게임 예제는 아키텍처 경계가 어디에나 존재한다는 사실을 보여준다.
- 아키텍트로서 아키텍처 경계가 언제 필요한지를 신중하게 파악해내야 한다.
- 경계를 제대로 구현하려면 비용이 많이든다는 사리과 경계가 무시되었을때 나중에 추가하는 비요이 크다는 사실도 알아야 한다.
- 아키텍트는 추상화가 필요하리라고 미리 예측해선 안된다.
- 이것이 바로 YAGNI 가 말하는 철학이다.
- 오버 엔지니링이 언더 엔지니어링보다 나쁠떄가 훨씬 많기 떄문이다.
- 다른 한편으론 어떤 아키텍처 경계도 존재하지 않는 상황에서 경계가 정말로 필요하다는 사실을 발견 후, 그제서야 경계를 추가하려면 비용이 많이 들고 큰 위험을 감수해야 한다.
- 프로젝트 초반에는 구현할 경계가 무엇인지와 무시할 경계가 무엇인지 쉽게 결정할 수 없다.
- 대신 지켜봐야 하고, 시스템이 발전함에 따라 주의를 기울여야 한다.
- 경계가 필요할 수도 있는 부분에 주목하고, 경계가 존재하지 않아 마찰의 어렴풋한 첫 조짐을 신중하게 관찰해야 한다.
- 첫 조짐이 보이는 시점이 되면, 해당 경계를 구현하는 비용과 무시할 때 감수할 비용을 가늠한다. 그리고 결정된 사항을 자주 검토한다.
- 우리의 목표는 경계의 구현 비용이 그걸 무시해서 생기는 비용보다 적어지는 바로 그 변곡점에서 경계를 구현하는 것이다.(어려울 것 같다..)
- 목표를 달성하려면 빈틈없이 지켜봐야 한다..