상세 컨텐츠

본문 제목

[Architecture] 5부 아키텍처 : 18장 경계 해부학

프로그래밍/Architecture

by jisooo 2024. 1. 14. 17:46

본문

 

 

시스템 아키텍처는 소프트웨어 컴포넌트와 그 컴포넌트들을 분리하는 경계에 의해 정의된다.

18장 "경계 해부학"에서는 이러한 경계의 다양한 형태들에 대해 설명하고 있다.

경계 횡단하기

"런타임에 경계를 횡단한다" 함은

그저 경계 한쪽에 있는 기능에서 반대편의 기능을 호출하여 데이터를 전달하는 일이다.

적절한 위치에서 경계를 횡단하는 비결은 소스 코드 의존성 관리에 있다.

소스 코드의 모듈 하나가 변경 되면,

이에 의존하는 다른 소스 코드 모듈도 변경하거나 재컴파일 후 새로 배포해야할 수 있다.

경계는 위와 같은 변경이 전파되는 것을 막기 위해

방화벽을 구축하고 관리하는 수단으로써 존재한다.

두려운 단일체

아키텍처 경계의 종류 중 가장 단순하고 흔한 형태는 물리적으로 엄격히 구분되지 않은 형태이다.

이 형태에서는 함수와 데이터가 단일 프로세서에서 같은 주소 공간을 공유하며,

그저 나름의 규칙을 갖고 분리되어 있을 뿐이다.

이를 저자는 소스 수준 분리 모드라고 불렀다.

배포 관점에서 보면, 이는 "단일체"라고 불리는 단일 실행 파일에 지나지 않는다.

실행 가능한 jar 파일로 묶인 일련의 자바 클래스 파일을 예로 들 수 있다.

이처럼 배포 관점에서는 단일체는 컴포넌트 간 경계가 드러나지 않는다.

이러한 아키텍처에서는 거의 모든 경우에 특정한 동적 다형성에 의존하여 내부 의존성을 관리한다.

객체 지향다형성에 해당하는 메커니즘 덕분에,

아키텍트는 결합도를 적절히 분리할 수 있었다.

"단일체"에서 가장 단순한 형태의 경계 횡단은

저수준 클라이언트에서 고수준 서비스로 향하는 함수 호출이다.

이 경우 런타임 의존성과 컴파일 타임의 의존성은

모두 저수준 컴포넌트에서 고수준 컴포넌트 방향으로 향한다.

위의 그림을 보면, 제어 흐름이 왼쪽에서 오른쪽으로 경계를 횡단하는 것을 볼 수 있다.

저수준의 client 컴포넌트에서 고수준의 Service 컴포넌트의 함수 f()를 호출한다.

이 때 클라이언트는 Data 인스턴스를 전달하는데,

경계에서 호출되는 쪽(Service)에서 Data의 정의가 위치하고 있는 점을 주목하자.

만약 반대로 고수준 클라이언트가 저수준의 서비스를 호출해야 한다면,

동적 다형성을 사용하여 제어흐름과는 반대 방향으로 "의존성을 역전"시킬 수 있다.

이렇게 하면 런타임 의존성은 컴파일타임 의존성과는 반대가 된다.

그림 18.2를 보면, 제어흐름은 18.1과 마찬가지로 왼쪽으로 오른쪽으로 경계를 횡단한다.

고수준 Client는 Service 인터페이스를 통해 저수준인 ServiceImpl의 함수 f()를 호출한다.

주목할 점은 경계를 횡단할 때,

의존성은 모두 오른쪽에서 왼쪽으로,

저수준 컴포넌트에서 고수준 컴포넌트를 향하고 있다.

또한 데이터 구조의 정의가 호출하는 쪽(Client)에 위치하고 있다.

정적 링크된 모노리틱 구조의 실행 파일이라도

이처럼 규칙적인 방식으로 구조를 분리하면,

프로젝트 개발, 테스트, 배포하는 작업에 큰 도움이 된다.

팀들은 서로의 영역에 침범하지 않은 채

자신만의 컴포넌트를 독립적으로 작업할 수 있다.

고수준 컴포넌트는 저수준 세부사항으로부터 독립적으로 유지된다.

이러한 단일체의 형태에서 컴포넌트 간 통신은 전형적인 함수 호출이기 때문에 매우 빠르고 비용이 저렴하다.

결과적으로 소스 수준에서 결합이 분리되면

경계를 가로지르는 통신은 상당히 빈번할 수 있다.

단일체를 배포하는 일은 일반적으로 컴파일과 정적 링크 작업 (패키징)을 수반하므로,

대체로 이러한 시스템에서 컴포넌트는 소스 코드 형태로 전달된다.

배포형 컴포넌트

아키텍처의 경계가 "물리적"으로도 드러날 수 있는데,

그중 가장 단순한 형태는 동적 링크 라이브러리다.

자바로 치면 jar 파일 라이브러리 등이 그 예다.

컴포넌트를 이 형태로 배포하면 따로 컴파일하지 않고 곧바로 사용할 수 있다.

대신 컴포넌트는 바이너리와 같이 배포 가능한 형태로 전달된다.

이는 배포 수준 결합 분리 모드에 해당한다.

이러한 배포 과정에서만 차이가 날 뿐,

배포 수준의 컴포넌트는 단일체와 동일하다.

일반적으로 모든 함수가 동일한 프로세서와 주소 공간에 위치하며,

컴포넌트를 분리하거나 컴포넌트 간 의존성을 관리하는 전략도 단일체와 동일하다.

단일체와 마찬가지로 배포형 컴포넌트의 경계를 가로지르는 통신은

순전히 함수 호출에 지나지 않으므로 매우 값싸다.

동적 링크와 런타임 로딩으로 인해 최초의 함수 호출은 오래걸릴 수 있지만,

대체로 이들 경계를 가로지르는 통신은 매우 빈번할 것이다.

스레드

단일형, 배포형 컴포넌트는 모두 스레드를 활용할 수 있다.

스레드는 아키텍처 경계도, 배포 단위도 아니다.

스레드는 실행 계획과 순서를 체계화하는 방법에 가깝다.

모든 스레드가 단 하나의 컴포넌트에 포함될 수도 있고,

많은 컴포넌틑에 걸쳐 분산될 수도 있다.

로컬 프로세스

훨씬 강한 물리적 형태를 띄는 아키텍쳐 경계로는 로컬 프로세스가 있다.

로컬 프로세스는 주로 명령행이나 그와 유사한 시스템 호출을 통해 생성된다.

로컬 프로세서들은 각각이 독립된 주소 공간에서 실행되고,

종종 공유 메모리 파티션을 사용하기도 하지만,

일반적으로 메모리 보호를 통해 프로세스들이 메모리를 공유하지 못하게 한다.

대개 로컬 프로세스는 소켓이나 메일박스, 메시지 큐와 같이

운영체제에서 제공하는 통신 기능을 이용하여 서로 통신한다.

각 로컬 프로세스는 정적으로 링크된 단일체이거나,

동적으로 링크된 여러개의 컴포넌트로 구성될 수 있다.

로컬 프로세스의 일종의 최상위 컴포넌트라고 생각하자.

로컬 프로세스는 컴포넌트 간 의존성을 동적 당형성을 통해 관리하는 저수준 컴포넌트로 구성된다.

로컬 프로세스 간 분리 전략도 마찬가지로

소스 코드 의존성의 화살표의 방향은 항상 고수준 컴포넌트를 향한다.

따라서 로컬 프로세스에서는

고수준 프로세스의 소스 코드가 저수준 프로세스의 이름, 물리주소, 레지스트리 조회 키를

절대로 포함해서는 안된다.

저수준 프로세스가 고수준 프로세스의 플러그인이 되도록 만드는 것이

아키텍처 관점의 목표라는 사실을 기억해야 한다.

로컬 프로세스 형태의 경계를 지나는 통신에는

운영체제 호출, 데이터 마샬링 및 언마샬링,

프로세스 간 문맥 교환 등이 있으며,

이들은 제법 비싼 작업에 속하기 때문에

통신이 너무 빈번하게 이뤄지지 않도록 신중하게 제한해야 한다.

서비스

물리적인 경계의 형태를 띄는 가장 강력한 경계는 바로 서비스다.

서비스는 프로세스로, 일반적으로 명령행 또는 그와 동등한 시스템 호출을 통해 구동된다.

서비스는 자신의 물리적 위치에 구애받지 않고,

서로 통신하는 서비스들은 물리적으로 동일한 프로세서나 멀티코어에서 동작할 수도 있고,

아닐 수도 있다.

서비스들은 모든 통신이 "네트워크"를 통해 이뤄진다고 가정한다.

서비스 경계를 지나는 통신은 함수 호출에 비해 매우 느리다.

따라서 가능하다면 서비스 간 빈번하게 통신하는 일을 피해야 한다.

이 수준의 통신에서는 지연에 따른 문제를 고수준에서 처리할 수 있어야 한다.

서비스 역시 위의 다양한 형태의 경계들과 공통적인 규칙을 따른다.

저수준 서비스는 반드시 고수준 서비스에 "플러그인"되어야 한다.

고수준 서비스의 소스 코드에는 저수준 서비스를 특정짓는 어떤 물리적인 정보도 절대 포함해서는 안된다. (ex URI)


단일체를 제외한 대다수의 시스템은 한 가지 이상의 경계 전략을 사용한다.

서비스 경계를 활용하는 시스템이라면, 로컬 프로세스 경계도 일부 포함할 수 있다.

또한 개별 서비스 및 로컬 프로세스는 거의 언제나 소스 코드 컴포넌트로 구성된 단일체이거나,

혹은 동적으로 링크된 배포형 컴포넌트의 집합이다.

즉, 대체로 한 시스템 안에서도 통신이 빈번한 로컬 경계와

지연을 중요하게 고려해야 하는 경계가 혼합되어 있음을 알 수 있다.

#클린아키텍처 #cleanArchitecture #컴포넌트경계 #경계해부학

관련글 더보기

댓글 영역