[Clean Architecture] 18장. 경계 해부학
|
Introduction
- 시스템 아키텍처는 일련의 소프트웨어 컴포넌트와 그 컴포넌트를 분리하는 경계에 의해 정의됨
- 이러한 경계는 다양한 형태로 나타남
경계 횡단하기
- 적절한 위치에서 경계를 횡단하게 하는 비결은 소스 코드 의존성 관리에 있음
- 왜 소스코드 일까?
- 소스 코드 모듈 하나가 변경되면, 이에 의존하는 다른 소스 코드 모듈도 변경하거나, 다시 컴파일해서 새로 배포해야 할지도 모르기 때문
- 경계는 이러한 변경이 전파되는 것을 막는 방화벽을 구축하고 관리하는 수단으로써 존재
두려운 단일체
- 아키텍처 경계 중 가장 단순하며 가장 흔한 형태는 물리적으로 엄격하게 분리되지 않는 형태
- 이 형태에서는 함수와 데이터가 단일 프로세서에서 같은 주소 공간을 공유하며 그저 나름의 규칙에 따라 분리되어 있을 뿐
- 소스 수준 분리 모드
- 배포 관점에서 보면 이는 소위 단일체 monolith 라고 불리는 단일 실행 파일에 지나지 않음
- 배포관점에서 볼 때 단일체는 경계가 드러나지 않음
- 단일체는 컴포넌트 수준으로 분리되지 않으므로, 배포할 때 개별 컴포넌트를 배포하는 대신 커다란 하나의 파일을 배포
- 따라서 경계가 드러나지 않음
- 최종적으로 정적으로 링크된 단일 실행 파일을 만들더라도, 그 안에 포함된 다양한 컴포넌트를 개발하고 바이너리로 만드는 과정을 독립적으로 수행할 수 있게 하는 일은 대단히 가치 있는 일
- 이러한 아키텍처는 거의 모든 경우에 특정한 동적 다형성에 의존하여 내부 의존성을 관리함
- 바로 이 때문에 최근 수십 년 동안 객체 지향 개발이 아주 중요한 패러다임이 될 수 있었음
- 가장 단순한 형태의 경계 횡단은 저수준 클라이언트에서 고수준 서비스로 향하는 함수 호출
- 이 경우 런타임 의존성과 컴파일타임 의존성은 모두 같은 방향, 즉 저수준 컴포넌트에서 고수준 컴포넌트로 향함
- 고수준 클라이언트가 저수준 서비스를 호출해야 한다면 동적 다형성을 사용하여 제어 흐름과는 반대 방향으로 의존성을 역전시킬 수 있음
- 이렇게 하면 런타임 의존성은 컴파일타임 의존성과는 반대가 됨
- 단일체에서 컴포넌트 간 통신은 매우 빠르고 값이 쌈
- 통신은 전형적인 함수 호출에 지나지 않기 때문
- 결과적으로, 소스 수준에서 결합이 분리되면 경계를 가로지르는 통신은 상당히 빈번할 수 있음
- 단일체를 배포하는 일은 일반적으로 컴파일과 정적 링크 작업을 수반하므로, 대체로 이러한 시스템에서 컴포넌트는 소수 코드 형태로 전달됨
배포형 컴포넌트
- 아키텍처의 경계가 물리적으로 드러날 수도 있는데, 그 중 가장 단순한 형태는 동적 링크 라이브러리
- e.g.) .NET DLL, 자바 jar 파일, 루비 Gem, 유닉스 공유 라이브러리 등
- 컴포넌트를 이 형태로 배포하면 따로 컴파일하지 않고 곧바로 사용할 수 있음
- 대신 컴포넌트는 바이너리와 같이 배포 가능한 형태로 전달됨
- 배포 수준의 컴포넌트는 단일체와 동일함
- 일반적으로 모든 함수가 동일한 프로세서와 주소 공간에 위치하며, 컴포넌트를 분리하거나 컴포넌트 간 의존성을 관리하는 전략도 단일체와 동일함
- 배포형 컴포넌트의 경계를 가로지르는 통신은 순전히 함수 호출에 지나지 않으므로 매우 값이 쌈
- 동적 링크과 런타임 로딩으로 인해 최초의 함수 호출은 오래 걸릴 수 있지만, 대체로 이들 경계를 가로지르는 통신은 매우 빈번할 것
스레드
- 단일체와 배포형 컴포넌트는 모두 스레드를 활용할 수 있음
- 스레드는 아키텍처 경계도 아니며 배포 단위도 아님
- 스레드는 실행 계획과 순서를 체계화하는 방법에 가까움
- 모든 스레드가 단 하나의 컴포넌트에 포함될 수도 있고, 많은 컴포넌트에 걸쳐 분산될 수도 있음
로컬 프로세스
- 훨씬 강한 물리적 형태를 띠는 아키텍처 경계로는 로컬 프로세스
- 로컬 프로세스는 주로 명령행이나 그와 유사한 시스템 호출을 통해 생성됨
- 대개의 경우 로컬 프로세스는 소켓이나 메일박스, 메시지 큐와 같이 운영체제에서 제공하는 통신 기능을 이용하여 서로 통신함
- 각 로컬 프로세스는 정적으로 링크된 단일체이거나 동적으로 링크된 여러 개의 컴포넌트로 구성될 수 있음
- 로컬 프로세스를 일종의 최상위 컴포넌트라고 생각하자
- 즉, 로컬 프로세스는 컴포넌트 간 의존성을 동적 다형성을 통해 관리하는 저수준 컴포넌트로 구성됨
- 로컬 프로세스 간 분리 전략은 단일체나 바이너리 컴포넌트의 경우와 동일
- 소스코드 의존성의 화살표는 단일체나 바이너리 컴포넌트와 동일한 방향으로 경계를 횡단함, 즉 항상 고수준 컴포넌트를 향함
- 저수준 프로세스가 고수준 프로세스의 플러그인이 되도록 만드는 것이 아키텍처 관점의 목표라는 사실을 기억
- 로컬 프로세스 경계를 지나는 통신에는 운영체제 호출, 데이터 마샬링 및 언마샬링, 프로세스 간 문맥 교환 등이 있음
- 이들은 제법 비싼 작업에 속함
- 따라서 통신이 너무 빈번하게 이뤄지지 않도록 신중하게 제한해야 함
서비스
- 물리적인 형태를 띠는 가장 강력한 경계는 바로 서비스
- 서비스는 프로세스로, 일반적으로 명령행 또는 그와 동등한 시스템 호출을 통해 구동됨
- 서비스 경계를 지나는 통신은 함수 호출에 비해 매우 느림
- 따라서 주의를 기울여서, 가능하다면 빈번하게 통신하는 일을 피해야 함
- 이 수준의 통신에서는 지연에 따른 문제를 고수준에서 처리할 수 있어야 함
- 저수준 서비스는 반드시 고수준 서비스에 플러그인되어야 함
- 고수준 서비스의 소스 코드에는 저수준 서비스를 특정 짓는 어떤 물리적인 정보도 절대 포함해서는 안됨
결론
- 단일체를 제외한 대다수의 시스템은 한 가지 이상의 경계 전략을 사용
- 서비스 경계를 활용하는 시스템이라면 로컬 프로세스 경계도 일부 포함하고 있을 수 있음
- 실제로 서비스는 상호작용하는 일련의 로컬 프로세스 퍼사드에 불과할 때가 많음
- 또한 개별 서비스 또는 로컬 프로세스는 거의 언제나 소스 코드 컴포넌트로 구성된 단일체이거나, 혹은 동적으로 링크된 배포형 컴포넌트의 집합
- 대체로 한 시스템 안에서 통신이 빈번한 로컬 경계와 지연을 중요하게 고려해야 하는 경계가 혼합되어 있음을 의미