[Clean Architecture] 16장. 독립성
|Introduction
- 좋은 아키텍처는 다음을 지원해야 한다.
- 시스템의 유스케이스
- 시스템의 운영
- 시스템의 개발
- 시스템의 배포
유스케이스
- 시스템의 아키텍처는 시스템의 의도를 지원해야 한다는 뜻
- 아키텍트의 최우선 관심사는 유스케이스이며, 아키텍처에서도 유스케이스가 최우선이다.
- 아키텍처는 반드시 유스케이스를 지원해야 한다.
- 좋은 아키텍처가 행위를 지원하기 위해 할 수 있는 일 중에서 가장 중요한 사항은
- 행위를 명확히 하고 외부로 드러내며,
- 이를 통해 시스템이 지닌 의도를 아키텍처 수준에서 알아볼 수 있게 만드는 것
- 시스템의 유스케이스는 시스템 구조 자체에서 한눈에 드러날 것
- 이들 행위는 일급 요소이며, 시스템의 최상위 수준에서 알아볼 수 있으므로, 개발자가 일일이 찾아 해매지 않아도 됨
- 이들 요소는 클래스이거나 함수 또는 모듈로서 아키텍처 내에서 핵심적인 자리를 차지할 뿐만 아니라, 자신의 기능을 분명하게 설명하는 이름을 가질 것
운영
- 시스템의 운영 지원 관점에서 볼 때 아키텍처는 더 실질적이며 덜 피상적인 역할을 맡음
- 아키텍처는 시스템의 요구와 관련된 각 유스케이스에 걸맞는 처리량과 응답시간을 보장해야 함
- 뛰어난 아키텍트라면 열어 두어야 하는 선택사항 중의 하나임
개발
- 아키텍처는 개발환경을 지원하는 데 있어 핵심적인 역할을 수행함
- 콘웨이의 법칙이 작용하는 지점이 바로 여기
시스템을 설계하는 조직이라면 어디든지 그 조직의 의사소통 구조와 동일한 구조의 설계를 만들어 낼 것이다.
- 많은 팀으로 구성되며 관심사가 다양한 조직에서 어떤 시스템을 개발해야 한다면, 각 팀이 독립적으로 행동하기 편한 아키텍처를 반드시 확보하여 개발하는 동안 팀들이 서로를 방해하지 않도록 해야 함
- 이러한 아키텍처를 만들려면 잘 격리되어 독립적으로 개발 가능한 컴포넌트 단위로 시스템을 분할할 수 있어야 함
- 그래야만 이들 컴포넌트를 독립적으로 작업할 수 있는 팀에 할당할 수 있음
배포
- 아키텍처는 배포 용이성을 결정하는 데 중요한 역할을 함
- 이때 목표는 “즉각적인 배포 immediate deployment”
- 좋은 아키텍처는 꼭 필요한 디렉터리나 파일을 수작업으로 생성하게 내버려 두지 않음
- 좋은 아키텍처라면 시스템이 빌드된 후 즉각 배포할 수 있도록 지원해야 함
- 이러한 아키텍처를 만들려면 시스템을 컴포넌트 단위로 적절하게 분할하고 격리시켜야 함
- 여기에는 마스터 컴포넌트도 포함
- 마스터 컴포넌트는 시스템 전체를 하나로 묶고, 각 컴포넌트를 올바르게 구동하고 통합하고 관리해야 함
선택사항 열어놓기
- 좋은 아키텍처는 컴포넌트 구조와 관련된 이 관심사들 사이에서 균형을 맞추고, 각 관심사 모두를 만족시킴
- 말은 쉽지만, 현실에서 이러한 균형을 잡기가 매우 어려움
- 대부분의 경우 우리는 모든 유스케이스를 알 수는 없으며, 운영하는 데 따르는 제약사항, 팀 구조, 배포 요구사항도 알지 못하기 때문
- 요컨대, 우리가 도달하려는 목표는 뚜렷하지 않을 뿐만 아니라 시시각각 변함
- 그러나 이런 변화 속에서도 사라지지 않는 것이 있음
- 몇몇 아키텍처 원칙은 구현하는 비용이 비교적 비싸지 않으며, 관심사들 사이에서 균형을 잡는데 도움이 됨
- 이들 원칙은 시스템을 제대로 격리된 컴포넌트 단위로 분할할 때 도움이 되며, 이를 통해 선택사항을 가능한 한 많이, 그리고 가능한 한 오랫동안 열어 둘 수 있게 해줌
- 좋은 아키텍처는 선택사항을 열어 둠으로써, 향후 시스템에 변경이 필요할 때 어떤 방향으로든 쉽게 변경할 수 있도록 함
계층 결합 분리
- 아키텍트는 시스템의 기본적인 의도는 분명히 알고 있음
- 따라서 아키텍트는 모든 유스케이스를 전부 알지 못하더라도 단일 책임 원칙과 공통 폐쇄 원칙을 적용하여, 그 의도의 맥락에 따라서 다른 이유로 변경되는 것들을 분리하고, 동일한 이유로 변경되는 것들은 묶음
- 서로 다른 이유로 변경되는 것은 무엇일까?
- 몇 가지 분명한 것이 있음
- 사용자 인터페이스가 변경되는 이유는 업무 규칙과는 아무런 관련이 없음
- 뛰어난 아키텍트는 유스케이스에서 UI 부분과 업무 규칙 부분을 서로 분리하고자 할 것
- 업무 규칙은 그 자체가 애플리케이션과 밀접한 관련이 있거나, 혹은 더 범용적일 수도 있음
- 서로 다른 두 유형의 규칙은 서로 분리하고, 독립적으로 변경할 수 있도록 만들어야 함
- 아키텍트는 기술적인 세부사항을 시스템의 나머지 부분으로부터 분리하여 독립적으로 변경할 수 있도록 해야만 함
유스케이스 결합 분리
- 서로 다른 이유로 변경되는 것으로 유스케이스 그 자체가 있음
- 유스케이스는 시스템을 분할하는 매우 자연스러운 방법
- 이와 동시에 유스케이스는 시스템의 수평적인 계층을 가로지르도록 자른, 수직으로 좁다른 조각이기도 함
- 각 유스케이스는 UI의 일부, 애플리케이션 특화 업무 규칙의 일부, 애플리케이션 독립적 업무 규칙의 일부, 그리고 데이터베이스 기능의 일부를 사용함
- 따라서 우리는 시스템을 수평적 계층으로 분할하면서 동시에 해당 계층을 가로지르는, 얇고 수직적인 유스케이스로 시스템을 분할할 수 있음
- 여기에서 패턴을 볼 수 있음
- 시스템에서 서로 다른 이유로 변경되는 요소들의 결합을 분리하면 기존 요소에 지장을 주지 않고도 새로운 유스케이스를 계속해서 추가할 수 있게 됨
- 또한 유스케이스를 뒷받침하는 UI와 데이터베이스를 서로 묶어서 각 유스케이스가 UI와 데이터베이스의 서로 다른 관점 aspect 을 사용하게 되면, 새로운 유스케이스가 추가하더라도 기존 유스케이스에 영향을 주는 일은 거의 없을 것
결합 분리 모드
- 유스케이스에서 서로 다른 관점 aspect 이 분리되었다면, 높은 처치량을 보장해야 하는 유스케이스와 낮은 처리량으로도 충분한 유스케이스는 이미 분리되어 있을 가능성이 높다.
- 간단히 말해 유스케이스를 위해 수행하는 그 작업들 (결합 분리)은 운영에도 도움이 됨
- 하지만 운영 측면에서 이 점을 살리기 위해선 결합을 분리할 때 적절한 모드를 선택해야 함
- 분리된 컴포넌트는 반드시 독립된 서비스가 되어야 하고, 일종의 네트워크를 통해 서로 통신해야 함
- 실제로 서비스에 기반한 아키텍처를 흔히들 서비스 지향 아키텍처 service-oriented architecture, soa 라고 부름
- 여기서 말하고자 하는 핵심은 때때로 컴포넌트를 서비스 수준까지도 분리해야 한다는 것
- 기억해야 할 점은 좋은 아키텍처는 선택권을 열어 둔다는 사실
- 결합 분리 모드는 이러한 선택지 중 하나
개발 독립성
- 컴포넌트가 완전히 분리되면 팀 사이의 간섭은 줄어듦
- 계층과 유스케이스의 결합이 분리되는 한 시스템의 아키텍처는 그 팀 구조를 뒷받침해줄 것
배포 독립성
- 유스케이스와 계층의 결합이 분리되면 배포 측면에서도 고도의 유연성이 생김
- 실제로 결합을 제대로 분리했다면 운영 중인 시스템에서도 계층과 유스케이스를 교체할 수 있음
중복
- 아키텍트는 종종 함정에 빠짐
- 전적으로 중복에 대한 공포로부터 발생하는 함정
- 소프트웨어서 중복은 일반적으로 나쁜 것
- 하지만 중복에도 여러 종류가 있음
- 진짜 중복과 거짓된 또는 우발적인 중복
- 중복으로 보이는 두 코드 영역이 각자의 경로로 발전한다면, 이 두 코드는 진짜 중복이 아님
- 우발적 중복을 제거했을 경우 나중에 다시 분리하느라 큰 수고를 감수해야 함
- 중복이 진짜 중복인지 확인하자
결합 분리 모드(다시)
- 계층과 유스케이스의 결합을 분리하는 방법은 다양함
- 소스 수준 분리 모드
- 소스 코드 모듈 사이의 의존성을 제어할 수 있음
- 이를 통해 하나의 모듈이 변하더라도 다른 모듈을 변경하거나 재컴파일하지 않도록 만들 수 있음
- 이 모드에서는 모든 컴포넌트가 같은 주소 공간에서 실행되고, 서로 통신할 때는 간단한 함수 호출을 사용함
- 컴퓨터 메모리에는 하나의 실행 파일만이 로드됨
- 이러한 구조를 흔히 모노리틱 구조라고 부름
- 배포 수준 분리 모드
- jar 파일, DLL, 공유 라이브러리와 같은 배포 가능한 단위들 사이의 의존성을 제어할 수 있음
- 이를 통해 한 모듈의 소스 코드가 변하더라도 다른 모듈을 재빌드하거나 재배포하지 않도록 만들 수 있음
- 서비스 수준 분리 모드
- 의존하는 수준을 데이터 구조 단위까지 낮출 수 있고, 순전히 네트워크 패킷을 통해서만 통신하도록 만들 수 있음
- 이를 통해 모든 실행 가능한 단위는 소스와 바이너리 변경에 대해 서로 완전히 독립적이게 됨
- 프로젝트가 성숙해갈수록 최적인 모드가 달라질 수 있음
- 한 가지 해결책은 단순히 서비스 수준에서의 분리를 기본 정책으로 삼는 것
- 이 방식은 비용이 많이 들고
- 결합이 큰 단위에서 분리된다는 문제가 있음
- 마이크로서비스가 아무리 작다 micro 고 하더라도, 충분히 작은 fine-grained 단위에서 분리될 가능성은 거의 없음
- 서비스 수준의 결합 분리가 지닌 또 다른 문제점은 개발 시간 측면뿐 아니라 시스템 자원 측면에서도 비용이 많이 든다는 사실
- 이처럼 컴포넌트가 서비스화될 가능성이 있다면 필자는 컴포넌트 결합을 분리하되, 서비스가 되기 직전에 멈추는 방식을 선호
- 컴포넌트들을 가능한 한 오랫동안 동일한 주소 공간에 남겨둠
- 이를 통해 서비스에 대한 선택권을 열어 둘 수 있음
- 점차 문제가 생길 때마다 서비스화하는 방향으로 시스템을 변경해나가면 됨
- 좋은 아키텍처는 시스템이 모노리틱 구조로 태어나서 단일 파일로 배포되더라도, 이후에는 독립적으로 배포 가능한 단위들의 집합으로 성장하고, 또 독립적인 서비스나 마이크로서비스 수준까지 성장할 수 있도록 만들어져야 함
- 또한 좋은 아키텍처라면 나중에 상황이 바뀌었을 때 이 진행 방향을 거꾸로 돌려 원래 형태인 모노리틱 구조로 되돌릴 수 있어야 함
- 좋은 아키텍처는 이러한 변경으로부터 소스 코드 대부분을 보호함
- 좋은 아키텍처는 결합 분리 모드를 선택사항으로 남겨두어서 배포 규모에 따라 가장 적합한 모드를 선택해 사용할 수 있게 만들어 줌
결론
- 물론 이렇게 하기는 까다로움
- 그리고 결합 분리 모드를 변경하기가 설정 값 하나 바꾸듯 쉬워야 한다는 뜻도 아님
- 물론 때로는 이러한 방식이 적합함
- 필자가 말하려는 바는 시스템의 결합 분리 모드는 시간이 지나면서 바뀌기 쉬우며,
- 뛰어난 아키텍트라면 이러한 변경을 예측하여 큰 무리 없이 반영할 수 있도록 만들어야 한다는 점