넘치게 채우기

5-11. 계층과 경계 본문

개발/Clean Architecture

5-11. 계층과 경계

riveroverflow 2023. 12. 19. 21:27
728x90
반응형

시스템이 세 가지 컴포넌트(UI, 업무 규칙, 데이터베이스)로만 구성된다고 생각하기 쉽다.

몇몇 단순한 시스템에서는 이 정도로 충분하다.

하지만 대다수는 이보다 훨씬 많다.

움퍼스 사냥 게임

1972년 발매된 움퍼스 사냥을 생각해보자.

텍스트를 기반으로 하는 이 게임은 Go EastShoot West와 같은 매우 단순한 명령어를 입력한다.

플레이어는 명령어를 입력하며, 컴퓨터는 플레이어가 보고, 냄새 맡고, 듣고, 경험할 것들로 응답한다.

 

텍스트 기반 UI는 그대로 유지하되, 게임 규칙과 UI를 분리해서 우리 제품을 여러 시장에서 다양한 언어로 발매할 수 있게 만든다고 해보자.

게임 규칙은 언어 독립적인 API를 사용해서 UI 컴포넌트와 통신할 것이고, UI는 API를 사람이 이해할 수 있는 언어로 변환할 것이다.

그림 25.1처럼 소스 코드 의존성을 적절히 관리하면, UI 컴포넌트가 어떤 언어를 사용하더라도 게임 규칙을 재사용할 수 있다.

게임 규칙은 어떤 종류의 인간 언어가 사용되는지 알지도 못할 뿐더러 신경 쓸 이유도 없다.

 

그림 25.1: UI 컴포넌트가 어떤 언어를 사용하더라도 게임 규칙을 재사용할 수 있다.

 

 

또한 게임의 상태를 영속적인 저장소에 유지한다고 가정해보자. 그게 플래시 메모리나 클라우드, 혹은 단순히 RAM일 수 있다. 어떤 경우에도 우리는 게임 규칙이 이러한 세부사항을 알지 않기를 바란다. 따라서 이번에도 역시 API를 생성하여 게임 규칙이 데이터 저장소 컴포넌트와 통신할 때 사용도록 만든다.

우리는 게임 규칙이 다양한 종류의 데이터 저장소에 대해 알지 않기를 원한다. 따라서 그림 25.2에서 보듯이, 의존성 규칙을 준수할 수 있도록 의존성이 적절한 방향을 가리키게 만들어야 한다.

 

그림 25.2 의존성 규칙 준수하기

 

 

클린 아키텍처?

그림 25.3 개선된 다이어그램

 

그림 25.3의 다이어그램은 더 복잡해졌지만, 놀라울 것은 없다.

점선으로 된 테두리는 API를 정의하는 추상 컴포넌트를 가리키며, 해당 API는 위나 아래의 컴포넌트가 구현한다.

GameRulesGameRules가 정의하고 Language가 구현하는 API를 이용해 Language와 통신한다. 마찬가지로 LanguageLanguage가 정의하고 TextDelivery가 구현하는 API를 이용해 TextDelivery와 통신한다.

API는 구현하는 쪽이 아닌 사용하는 쪽에 정의되고 소속된다.

GameRules를 들여다보면 GameRules 내부 코드에서 사용하고 Language 내부 코드에서 구현하는 다형적 Boundary 인터페이스를 발견할 수 있다.

 

또한 Language에서 사용하고 GameRules내부 코드에서 구현하는 다형적 Boundary인터페이스도 발견할 수 있다.

Language를 들여다 봐도 동일한 구조를 발견할 수 있다. 즉, TextDelivery내부의 코드에서 구현한는 다형적 Boundary 인터페이스와, TextDelivery에서 사용하고 Language가 구현하는 다형적 Boundary 인터페이스를 발견할 수 있을 것이다.

이 모든 경우에 해당 Boundary 인터페이스가 정의하는 API는 의존성 흐름의 상위에 위치한 컴포넌트에 속한다.

English, SMS, CloudData와 같은 변형들은 추상 API 컴포넌트가 정의하는 다형적 인터페이스를 통해 제공되고, 실제로 서비스하는 구체 컴포넌트가 해당 인터페이스를 구현한다. 예를 들어 Language가 정의하는 다형적 인터페이스는 EnglishSpanish가 구현할 것이다.

 

이러한 변형들을 모두 제거하고 순전히 API 컴포넌트만 집중하면 다이어그램을 단순화할 수 있다. 그림 25.4가 그 결과다.

 

그림 25.4 단순화된 다이어그램

 

 

그림 25.4의 다이어그램은 모든 화살표가 위를 향하도록 맞춰졌다는 점에 주목하자. 그 결과 GameRules는 최상위에 놓인다. GameRules는 최상위 수준의 정책을 가지는 컴포넌트이므로 이치에 맞는 배치이다.

정보가 흐르는 방향을 생각해 보자. 모든 입력은 사용자로부터 전달받아 좌측 하단의 TextDelivery 컴포넌트로 전달된다. 이 정보는 Language 컴포넌트를 거쳐서 위로 올라가며, GameRules에 적합한 명령어로 변역된다.

GameRules는 사용자 입력을 처리하고, 우측 하단의 DataStorage로 적절한 데이터를 내려 보낸다.

 

그런 후 GameRulesLanguage로 출력을 되돌려 보내고, Language는 API를 다시 적절한 언어로 번역한 후 번역된 언어를 TextDelivery를 통해 사용자에게 전달한다.

이 구성은 데이터 흐름을 두 개의 흐름으로 효과적으로 분리한다. 왼쪽의 흐름은 사용자와의 통신에 관여하며, 오른쪽의 흐름은 데이터 영속성에 관여한다. 두 흐름은 상단의 GameRules에서 서로 만나며, GameRules는 두 후름이 모두 거치게 되는 데이터에 대한 최종적인 처리기가 된다.

 

흐름 횡단하기

이 예제처럼 데이터 흐름은 항상 두 가지일까? 절대로 아니다. 움퍼스 사냥꾼 게임을 멀티플레이로 만든다고 해보자.

이 경우 그림 25.5에서 보듯이 네트워크 컴포넌트를 추가해야 한다.

이 구성은 데이터 흐름을 세 개의 흐름으로 분리하며, 이들 흐름은 모두 GameRules가 제어한다.

 

그림 25.5 Network 컴포넌트 추가하기

 

따라서 시스템이 복잡해질수록 컴포넌트 구조는 더 많은 흐름으로 분리될 것 이다.

 

흐름 분리하기

게임의 GameRules 컴포넌트를 생각해 보자. 게임 규칙 중 일부는 지도와 관련된 메커니즘을 처리한다. 이 규칙들은 동굴이 서로 어떻게 연결될지, 각 동굴에 어떤 문체가 위치할지 등을 알고 있다. 또한 플레이어가 동굴에서 동굴로 이동하는 방법이나, 플레이어가 반드시 처리해야 하는 사건을 결정하는 방법도 알고 있다.

 

하지만 이보다 더 높은 수준에는 또 다른 정책 집합이 존재한다. 즉, 플레이어의 생명력, 그리고 특정 사건을 해결하는 비용과 얻게 될 소득 등을 알고 있는 그러한 정책이다. 이러한 정책은 플레이어의 생명력이 지속적으로 줄어들게 하거나, 식량을 발견하면 생명력이 늘어나도록 한다. 저수준 메커니즘과 관련된 정책에서는 이러한 고수준 정책에게 FoundFood(식량 발견)이나 FellInFit(구덩이에 빠짐)과 같은 사건이 발생했음을 알린다. 그러면 고수준 정책에서는 플레이어의 상태를 관리한다. (그림 25.6 참고). 그리고 게임이 끝났을 때 플레이어의 승리 여부도 해당 정책에서 결정한다.

 

그림 25.6 고수준의 정책은 플레이어를 관리한다.

 

 

대규모의 플레이어가 동시에 플레이할 수 있는 버전의 움퍼스 사냥 게임이 있다고 가정해보자.

MoveManagement는 플레이어의 컴퓨터에서 직접 처리되지만 PlayerManageMent는 서버에서 처리된다.

PlayerManagement는 접속된 모든 MoveManagement 컴포넌트에 마이크로서비스 API를 제공한다.

 

그림 25.7의 다이어그램은 이러한 시나리오를 다소 간략화해 묘사한다.

Network 요소는 묘사된 것보다 훨씬 더 복잡하지만, 무엇을 말하고 싶은지는 아마 쉽게 알 수 있을 것이다.

MoveManagementPlayerManagement 사이에는 완벽한 형태의 아키텍처 경계가 존재한다.

 

그림 25.7 마이크로서비스 API 추가하기

결론

경계는 어디에나 존재한다. 아키텍트로서 우리는 아키텍처 경계가 언제 필요한지를 신중하게 파악해내야 한다.

또한 우리는 이러한 경계를 제대로 구현하려면 비용이 많이 든다는 사실도 인지하고 있어야 한다.

이와 동시에 이러한 경계가 무시되면 더 비용이 크다는 것도 알아야 한다.

 

시스템이 발전함에 따라 주의를 기울여야 한다. 경계가 필요할 수도 있는 부분에 주목하고, 경계가 존재하지 않아 생기는 마찰의 어렴풋한 첫 조짐을 신중하게 관찰해야 한다.

첫 조짐이 보이는 시점이 되면, 해당 경계를 구현하는 비용과 무시할 때 감수할 비용을 가늠해 본다. 그리고 결정된 사항을 자주 검토한다. 우리의 목표는 경계의 구현 비용이 그걸 무시해서 생기는 비용보다 적어지는 그 변곡점에서 경계를 구현하는 것이다.

728x90
반응형

'개발 > Clean Architecture' 카테고리의 다른 글

5-13. '크고 작은 모든' 서비스들  (0) 2023.12.21
5-12. 메인(Main) 컴포넌트  (0) 2023.12.20
5-10. 부분적 경계  (0) 2023.12.18
5-9. 프레젠터와 험블 객체  (0) 2023.12.17
5-8. 클린 아키텍처.  (0) 2023.12.16