상세 컨텐츠

본문 제목

[Architecture] 5부 아키텍처 : 25장 계층과 경계

프로그래밍/Architecture

by jisooo 2024. 1. 17. 21:41

본문

 

 

#클린아키텍처 #cleanArchitecture #계층과경계

25장 계층과 경계에서는,

구체적인 예시 프로그램 "움퍼스 사냥 게임"과 함께

업무 규칙과 UI, 데이터 영속성 관련 세부사항 컴포넌트가

어떻게 통신하고 어떠한 방향으로 의존성을 지니며, 제어 흐름이 어떻게 흘러가는지

설명하고 있다.

5부에서 계속 설명한 컴포넌트 간 계층을 분리하고 경계를 긋는 일을

종합적인 예시를 들어 다시 한번 정리, 강조하는 내용이 담겨있다.

시스템이 보통 세 가지 컴포넌트 (UI, 업무 규칙, 데이터베이스)로만 구성된다고

생각하기 쉽지만, 대다수의 시스템에서 등장하는 컴포넌트의 개수는 이보다 훨씬 많고 복잡하다.

하지만 이처럼 복잡한 시스템에도 나타내는

계층 간의 분리와 경계의 원리는 모두 일맥상통한다.

복잡한 시스템에 대한 예로 사냥 게임 프로그램으로 예시를 들어보자.

움퍼스 사냥 게임

텍스트 기반으로 하는 이 움퍼스 사냥 게임은

매우 단순한 명령어를 사용한다.

플레이어가 텍스트로 명령어를 입력하며,

컴퓨터는 플레이어악 보고 냄새 맡고 듣고 경험한 것들로 응답한다.

텍스트 기반의 UI는 유지하고,

UI와 별개로 게임 규칙은 언어 독립적인 API를 사용해서 UI 컴포넌트와 통신할 것익,

UI는 API를 사람이 이해할 수 있는 언어로 변환할 것이다.

위 그림처럼 소스 코드 의존성을 적절히 관리하면,

UI 컴포넌트가 어떠한 언어를 사용하거나 추후 변경되더라도

게임 규칙을 재사용할 수 있다.

게임 규칙은 사실상 어떤 종류의 언어가 사용되는지 알지 못한 뿐만 아니라

굳이 신경 쓸 이유도 없다.

UI 컴포넌트 외에도,

우리는 게임의 상태를 영속적인 저장소에 유지한다고 가정해보자.

플래시 메모리나 클라욷, 혹은 단순 RAM일 수도 있다.

어떤 경우라도 우리는 게임 규칙이 이러한 세부사항을 알지 않기를 바란다.

따라서 이번에도 역시 API를 생성하여,

게임 규칙이 데이터 저장소 컴포넌트와 통신할 때 사용하도록 만든다.

우리는 게임 규칙이 다양한 종류의 데이터 저장소에 대해

알지 않기를 바란다. 즉, 게임 규칙 입장에서 데이터저장소나 UI 컴포넌트에 의존하게 해서는 안된다.

아래 그림에서 보듯이,

위와 같은 의존성 규칙을 준수할 수 있도록

의존성이 적절한 방향을 가리키게 만들어야 한다.

위 다이어그램에서도 우리는 5부에서 계속 강조해왔던 의존성 규칙을 발견할 수 있다.

업무 규칙은 세부사항에 의존해서는 안되고,

세부사항이 업무규칙에 의존한다.

각 세부사항의 의존성의 방향은

모두 업무 규칙인 Game Rules를 향해 화살표가 향해 있다.

클린 아키텍처

분명하게도 이 예제의 맥락이라면

클린 아키텍처 접근법을 적용해서

유스케이스, 경계, 엔티티, 그리고 관련된 데이터 구조를

모두 만드는 일도 쉬운 일이다.

그런데 위 다이어그램이 과연

중요한 아키텍처 경계를 모두 발견한 것일까?

UI에서 언어가 유일한 변경의 축은 아니다.

이 밖에도 텍스트를 주고받는 메커니즘을 다양하게 만들고 싶은 요구사항이 있을 수 있다.

예를 들어 일반적인 shell 창을 사용하고 싶을 때도 있고,

텍스트 메시지나 채팅 애플리케이션을 사용하기를 원할 수도 있다.

따라서 이 변경의 축에 의해 정의되는 아키텍처 경계가

잠재되어 있을 수 있다.

 

점선으로된 테두리에 쌓인 컴포넌트 Language, Text Delivery, Data Storage는

해당 기능에 관한 API를 정의하는 추상 컴포넌트를 가리키며,

해당 API는 추상 컴포넌트 위나 아래의 컴포넌트들이 구현한다.

Language 추상 컴포넌트는

English UI, Spanish UI가 구체 컴포넌트로써 API를 구현하고,

Text Delivery 추상 컴포넌트는

SMS, Console가 구체컴포넌트로써 API를 구현하고,

Data Storage 추상 컴포넌트는

Cloud Data, Flash Data가 구체 컴포넌트로써 API를 구현한다.

게임의 핵심 업무 규칙을 담은 GameRules 컴포넌트는

Language가 구현하는 API를 이용해 Language와 통신한다.

마찬가지로 Language는 컴포넌트는

Text Delivery가 구현하는 API를 이용해 TextDelivery와 통신한다.

즉 API는 구현하는 쪽이 아닌, 사용하는 쪽(GameRules)에서

추상 컴포넌트(인터페이스)로써 정의되고 소속된다.

GameRules를 들여다 보면,

GameRules 내부 코드에서 사용하고

Language 내부 코드에서 구현하는

다형적 Boundary 인터페이스가 정의되어있는 것을 발견할 수 있다.

마찬가지로

Language에서 사용하고 GameRules 내부 코드에서 구현하는

다형적 Boundary 인터페이스도 발견할 수 있다.

Language 내부 코드를 들여다봐도 동일한 구조를 발견할 수 있다.

TextDelivery 내부 코드에서 구현하는 다형적 Boundary 인터페이스와,

TextDelivery에서 사용하고 Language가 구현하는

다형적 Boundary 인터페이스를 발견할 수 있을 것이다.

이 모든 경우에 해당 Boundary 인터페이스가 정의하는 API는

의존성 흐름의 상위에 위치한 컴포넌트에 속한다.

English, SMS, CloudData와 같은 구체 컴포넌트들은

추상 API 컴포넌트가 정의하는 다형적 인터페이스를 통해 외부에 제공되고,

실제로 서비스하는 구체 컴폰전트가 해당 인터페이스를 구현한다.

예를 들어 Language가 정의하는 다형적 인터페이스는

English나 Spanish가 API를 구현할 것이다.

이러한 변형들을 모두 제거하고 순전히 API 컴포넌트만 집중하여

다이어그램을 단순화해보면 아래와 같다.

위 다이어그램에서는 모든 의존성 화살표가

위를 향하도록 맞춰졌다는 점에 주목해야 한다.

그 결과 GameRules 업무 규칙 컴포넌트는 최상위에 놓인다.

즉, GameRules는 최상위 수준의 정책을 가지는 컴포넌트이므로

이치에 맞는 배치이다.

흐름의 횡단 및 분리

계속 예시에서 설명한 움퍼스 게임 사냥 프로그램을

조금 더 복잡한 프로그램으로 만들어보자.

핵심 업무 규칙인 게임 규칙에도 저수준의 게임 규칙과 고수준의 게임 규칙이 존재할 수 있다.

예를 들어,

게임 규칙 중 일부는 게임의 맵과 관련된 메커니즘을 처리한다.

이 규칙들은 동굴이 서로 어떻게 연결되고,

각 동굴에 어떤 몬스터와 아이템이 위치할 지 등을 알고 있다.

또한 플레이어가 동굴에서 동굴로 이동하는 방법이나,

플레이어가 반드시 처리해야 할 이벤트들을 결정하는 방법도 알고 있다.

위와 같이 게임 맵을 다룬 게임 규칙 이외에

더 높은 고수준의 정책 집합이 존재한다.

즉 플레이어 자체의 생명력, 그리고 특정 이벤트를 해결했을 때 드는 비용과 얻게 될 경험치, 아이템들

등을 알고 있는 정책을 예시로 들 수 있다.

이러한 고수준 정책은 플레이어의 생명력이 지속적으로 줄어들게 하거나,

아이템을 발견하면 생명력이 늘어나도록 한다.

또한 특정 이벤트를 달성하면 경험치를 획득하여 구간 별 일정 경험치를 달성하면

플레이어의 레벨을 올려주는 등의 정책도 있을 수 있겠다.

저수준 메커니즘과 관련된 정책에서는

이러한 고수준 정책에게 "아이템 발견"이나 "구덩이에 빠짐"과 같은 이벤트가 발생했음을 알린다.

그러면 고수준 정책에서는 발생한 이벤트에 따라서 플레이어의 상태를 반영하고 관리한다.

그리고 게임이 끝났을 때의 플레이어의 상태와 승리 여부도 해당 정책에서 결정한다.

 

이렇게 업무 규칙 자체에서도 저수준과 고수준 정책을 컴포넌트로 분리시킬 수 있다.

그리고 여기서도

의존성의 방향은 모두 플레이어의 상태를 관리하는

고수준 정책인 "PlayerManagement"를 향해있는 것에 주목해야 한다.

좀 더 심화된 예시로,

대규모의 플레이어가 동시에 위 게임 프로그램을 플레이할 수 있다고 가정해보자.

MoveManagement는 플레이어의 각각 로컬 환경 컴퓨터에서 직접 처리되지만,

PlayerManagement는 게임 서버에서 처리된다.

PlayerManagement는 게임에 접속한 모든 MoveManagement 컴포넌트에

마이크로서비스 API를 제공한다.

위와 같은 상황에서도

MoveManagement와 PlayerManagement 사이에는

완벽한 형태의 아키텍처 경계가 존재하는 것을 볼 수 있다.

결론

아키텍처의 경계는 어디에나 존재하기 마련이다.

우리는 아키텍처 경계가 언제 필요한지 신중하게 파악해야 하고,

우리가 원하는 이상적인 경계를 구현하려면 비용이 많이 든다는 사실도 인지해야 한다.

또한 이러한 경계가 무시되었다면,

나중에 다시 추가하는 비용이 크다는 사실도 알아야 한다.

아키텍처는 비용을 산정하고,

어디에 아키텍처 경계를 두어야 할지,

그리고 완벽하게 구현할 경계는 무엇인지,

또한 부분적으로 구현할 경계와 무시할 경계는 무엇인지를 결정해야 한다.

하지만 프로젝트 초반에는 구현할 경계가 무엇인지와 무시할 경계가 무엇인지를

쉽게 파악하기 힘들다.

시스템이 발전함에 따라 경계가 필요할 수 도 있어보이는 부분에 주목하고,

경계가 존재하지 않아 생기는 마찰의 어렴풋한 첫 조짐을 신중하게 관찰해야 한다.

관련글 더보기

댓글 영역