클린아키텍처 5부: 아키텍처(1)

Jihyun·2021년 12월 12일
0

이 글은 한달 한권 챌린지의 두번째 책, 클린아키텍처를 읽고 정리한 내용이다.


15장. 아키텍처란?

좋은 아키텍처는 시스템을 쉽게 이해하고, 쉽게 개발하며, 쉽게 유지보수하고, 또 쉽게 배포하게 해준다.

아키텍처의 궁극적인 목표는 시스템의 수명과 관련된 비용은 최소화하고, 프로그래머의 생산성은 최대화하는 데 있다.

개발

개발하기 힘든 시스템이라면 수명이 길지도 않고 건강하지도 않을 것이다. 따라서 시스템 아키텍처는 개발팀이 시스템을 쉽게 개발할 수 있도록 뒷받침해야만 한다.

배포

소프트웨어 시스템이 사용될 수 있으려면 반드시 배포할 수 있어야 한다.

배포 비용이 높을수록 시스템의 유용성은 떨어진다. 따라서 소프트웨어 아키텍처는 시스템을 단 한 번에 쉽게 배포할 수 있도록 만드는 데 그 목표를 두어야 한다.

운영

아키텍처가 시스템 운영에 미치는 영향은 개발, 배포, 유지보수에 미치는 영향보다는 덜 극적이다. 운영에서 겪는 대다수의 어려움은 소프트웨어 아키텍처에는 단순히 스토리지와 서버를 추가하는 것만으로 해결할 수 있다.

그렇더라도 시스템을 운영할 때 아키텍처가 맡는 또 다른 역할이 있다. 좋은 소프트웨어 아키텍처는 시스템을 운영하는 데 필요한 요구도 알려준다. 시스템 아키텍처가 개발자에게 시스템의 운영 방식을 잘 드러내 준다고 할 수 있다. 이를 통해 시스템을 이해하기 쉬워지며,따라서 개발과 유지보수에 큰 도움이 된다.

유지보수

유지보수는 모든 측면에서 봤을 때 소프트웨어 시스템에서 비용이 가장 많이 든다.

기존 소프트웨어에 새로운 기능을 추가하거나 결함을 수정할 때, 소프트웨어를 파헤쳐서 어디를 고치는 게 최선인지, 그리고 어떤 전략을 쓰는 게 최적일지를 결정할 때 비용이 발생한다. 이러한 변경사항을 반영할 때 의도치 않은 결함이 발생할 가능성은 항상 존재하며, 이로 인한 위험부담 비용이 추가된다.

주의를 기울여 신중하게 아키텍처를 만들면 이 비용을 크게 줄일 수 있다. 시스템을 컴포넌트로 분리하고, 안정된 인터페이스를 두어 서로 격리한다. 이를 통해 미래에 추가될 기능에 대한 길을 밝혀 둘 수 있을 뿐만 아니라 의도치 않은 장애가 발생할 위험을 크게 줄일 수 있다.

선택사항 열어 두기

모든 소프트웨어 시스템은 정책(Policy)과 세부사항(Detail) 이라는 주요한 두 가지 구성요소로 분해할 수 있다.

정책 요소는 모든 업무 규칙과 업무 절차를 구체화한다. 세부사항은 사람, 외부 시스템, 프로그래머가 정책과 소통할 때 필요한 요소지만, 정책이 가진 행위에는 조금도 영향을 미치지 않는다. 이러한 세부사항에는 입출력 장치, 데이터베이스, 웹 시스템, 서버, 프레임워크, 통신 프로토콜 등이 있다.

소프트웨어를 부드럽게 유지하는 방법은 중요치 않은 세부사항, 즉 선택사항을 가능한 한 많이, 그리고 가능한 한 오랫동안 열어 두는 것이다.

아키텍트의 목표는 시스템에서 정책을 가장 핵심적인 요소로 식별하고, 동시에 세부사항은 정책에 무관하게 만들 수 있는 형태의 시스템을 구축하는 데 있다. 이를 통해 세부사항을 결정하는 일은 미루거나 연기할 수 있게 된다.

현재 동작하고 있는 일부 고수준 정책이 있고, 이들 정책이 데이터베이스 에 독립적이라면 다양한 데이터베이스를 후보로 두고 그 적용 가능성과 성능 을 검토해볼수 있다. 웹 시스템, 웹 프레임워크, 심지어 웹 자체에 대해서도 마찬가지다.

좋은 아키텍트는 세부사항을 정책으로부터 신중하게 가려내고, 정책이 세부사항과 결합되지 않도록 엄격하게 분리한다. 이를 통해 정책은 세부사항에 관한 어떠한 지식도 갖지 못하게 되며, 어떤 경우에도 세부사항에 의존하지 않게 된다. 좋은 아키텍트는 세부사항에 대한 결정을 가능한 한 오랫동안 미룰 수 있는 방향으로 정책을 설계한다.


16장. 독립성

유스케이스

시스템의 아키텍처는 시스템의 의도를 지원해야 한다. 아키텍처는 반드시 유스케이스를 지원해야 한다.

하지만 앞서 논의한 바와 같이 아키텍처는 시스템의 행위에 그다지 큰 영향을 주지 않는다.

좋은 아키텍처가 행위를 지원하기 위해 할 수 있는 일 중에서 가장 중요한 사항은 행위를 명확히 하고 외부로 드러내며, 이를 통해 시스템이 지닌 의도를 아키텍처 수준에서 알아볼 수 있게 만드는 것이다.

운영

시스템이 초당 100,000명의 고객을 처리해야 한다면, 아키텍처는 이 요구와 관련된 각 유스케이스에 걸맞은 처리량과 응답시간을 보장해야 한다.

만약 시스템에서 수 밀리초 안에 3차원의 빅데이터 테이블에 질의해야 한다면, 반드시 이러한 운영 작업을 허용할 수 있는 형태로 아키텍처를 구조화해야 한다.

형태에 대한 결정은 뛰어난 아키텍트라면 열어 두어야 하는 선택사항 중의 하나다.

만약 시스템이 단일체(monoiith)로 작성되어 모노리틱 구조를 갖는다면, 다중 프로세스, 다중 스레드, 또는 마이크로 서비스 형태가 필요해질 때 개선하기가 어렵다. 그에 비해 아키텍처에서 각 컴포넌트를 적절히 격리하여 유지하고 컴포넌트 간 통신 방식을 특정 형태로 제한하지 않는다면, 시간이 지나 운영에 필요한 요구사항이 바뀌더라도 스레드, 프로세스, 서비스로 구성된 기술 스펙트럼 사이를 전환하는 일이 훨씬 쉬워질 것이다.

개발

아키텍처는 개발환경을 지원하는 데 있어 핵심적인 역할을 수행한다.

콘웨이(Conway)의 법칙은 다음과 같다.

시스템을 설계하는 조직이라면 어디든지 그 조직의 의사소통 구조와 동일한 구조의 설계를 만들어 낼 것이다.

많은 팀으로 구성되며 관심사가 다양한 조직에서 어떤 시스템을 개발해야 한 다면, 각 팀이 독립적으로 행동하기 편한 아키텍처를 반드시 확보하여 개발하는 동안 팀들이 서로를 방해하지 않도록 해야 한다. 이러한 아키텍처를 만들려면 잘 격리되어 독립적으로 개발 가능한 컴포넌트 단위로 시스템을 분할할 수 있어야 한다. 그래야만 이들 컴포넌트를 독립적으로 작업할 수 있는 팀에 할당할 수 있다.

배포

아키텍처는 배포 용이성을 결정하는 데 중요한 역할을 한다. 이때 목표 는 ‘즉각적인 배포(immediate deployment)’다.

좋은 아키텍처는 꼭 필요한 디렉터리나 파일을 수작업으로 생성하게 내버려 두지 않는다. 시스템이 빌드된 후 즉각 배포할 수 있도록 지원해야 한다.

이러한 아키텍처를 만들려면 시스템을 컴포넌트 단위로 적절하게 분할하고 격리시켜야 한다. 여기에는 마스터 컴포넌트도 포함되는데, 마스터 컴포넌트는 시스템 전체를 하나로 묶고, 각 컴포넌트를 올바르게 구동하고 통합하고 관리해야 한다.

선택사항 열어놓기

좋은 아키텍처는 컴포넌트 구조와 관련된 이 관심사들 사이에서 균형을 맞추고, 각 관심사 모두를 만족시킨다.

현실에서는 이러한 균형을 잡기가 매우 어렵다. 대부분의 경우 우리는 모든 유스케이스를 알 수는 없으며, 운영하는 데 따르는 제약사항, 팀 구조, 배포 요구사항도 알지 못하기 때문이다. 더 심각한 문제는 이러한 사항들을 알고 있더라도, 시스템이 생명주기의 단계를 하나씩 거쳐감에 따라 이 사항들도 반드시 변해간다는 사실이다. 우리가 도달하려는 목표는 뚜렷하지 않을 뿐만 아니라 시시각각 변한다.

그러나 이런 변화 속에서도 사라지지 않는 것이 있다. 몇몇 아키텍처 원칙은 구현하는 비용이 비교적 비싸지 않으며, 관심사들 사이에서 균형을 잡는 데 도움이 된다. 심지어 목표점을 명확히 그릴 수 없는 경우에도 도움이 된다. 이들 원칙은 시스템을 제대로 격리된 컴포넌트 단위로 분할할 때 도움이 되며, 이를 통해 선택사항을 가능한 한 많이, 그리고 가능한 한 오랫동안 열어둘수있게해준다.

좋은 아키텍처는 선택사항을 열어 둠으로써, 향후 시스템에 변경이 필요 할 때 어떤 방향으로든 쉽게 변경할 수 있도록 한다.

계층 결합 분리

아키텍트는 필요한 모든 유스케이스를 지원할 수 있는 시스템 구조를 원하지만, 유스케이스 전부를 알지는 못한다.

하지만 아키텍트는 시스템의 기본적인 의도는 분명히 알고 있다. 그 시스템이 장바구니 시스템인지, 주문 처리 시스템인지 안다는 뜻이다.

따라서 아키텍트는 단일 책임 원칙과 공통 폐쇄 원칙을 적용하여 그 의도의 맥락에 따라서 다른 이유로 변경되는 것들은 분리하고, 동일한 이유 로 변경되는 것들은 묶는다.

사용자 인터페이스가 변경되는 이유는 업무 규칙과는 아무런 관련이 없다.

만약 유스케이스가 두 가지 요소를 모두 포함한다면, 뛰어난 아키텍트는 유스케이스에서 UI 부분과 업무 규칙 부분을 서로 분리하고자 할 것이다. 이렇게 함으로써 두 요소를 서로 독립적으로 변경할 수 있을 뿐만 아니라, 유스케이스는 여전히 가시적이며 분명하게 유지할 수 있다.

업무 규칙은 그 자체가 애플리케이션과 밀접한 관련이 있거나, 혹은 더 범용적일 수도 있다.

예를 들어 입력 필드 유효성 검사는 애플리케이션 자체와 밀접하게 관련된 업무 규칙이다. 반대로 계좌의 이자 계산이나 재고품 집계 는 업무 도메인에 더 밀접하게 연관된 업무 규칙이다. 이들 서로 다른 두 유형의 규칙은 각자 다른 속도로, 다른 이유로 변경될 것이다. 따라서 이들 규칙은 서로 분리하고, 독립적으로 변경할 수 있도록 만들어야만 한다.

데이터베이스, 쿼리 언어, 심지어 스키마조차도 기술적인 세부사항이며, 업무 규칙이나 UI와는 아무런 관련이 없다. 이들은 시스템의 다른 측면과는 다른 속도로, 다른 이유로 변경된다. 결론적으로 아키텍트는 이들을 시스템의 나머지 부분으로부터 분리하여 독립적으로 변경할 수 있도록 해야만한다.

이제 우리는 시스템을 서로 결합되지 않은 수평적인 계층으로 분리하는 방법을 알게 되었다. 이러한 계층의 예로는 UI, 애플리케이션에 특화된 업무 규칙, 애플리케이션과는 독립적인 업무 규칙, 데이터베이스 등을 들 수 있다.

유스케이스 결합 분리

우리는 시스템을 수평적 계층으로 분할하면서 동시에 해당 계층을 가로지르는 얇은 수직적인 유스케이스로 시스템을 분할할 수 있다.

이와 같이 결합을 분리하려면 주문 추가 유스케이스의 UI와 주문 삭제 유스케이스의 UI를 분리해야 한다. 유스케이스의 업무 규칙과 데이터베이스 부분도 마찬가지다. 이런 식으로 시스템의 맨 아래 계층까지 수직으로 내려가며 유스케이스들이 각 계층에서 서로 겹치지 않게 한다.

시스템에서 서로 다른 이유로 변경되는 요소 들의 결합을 분리하면 기존 요소에 지장을 주지 않고도 새로운 유스케이스를 계속해서 추가할 수 있게 된다. 또한 유스케이스를 뒷받침하는 이와 데이터 베이스를 서로 묶어서 각 유스케이스가 UI와 데이터베이스의 서로 다른 관점을 사용하게 되면, 새로운 유스케이스를 추가하더라도 기존 유스케이스에 영향을 주는 일은 거의 없을 것이다.

개발 독립성

컴포넌트가 완전히 분리되면 팀 사이의 간섭은 줄어든다.

업무 규칙이 UI를 알지 못하면 UI에 중점을 둔 팀은 업무 규칙에 중점을 둔 팀에 그다지 영향을 줄 수 없다.

기능 팀, 컴포넌트 팀, 계층 팀, 혹은 또 다른 형태의 팀이라도 계층과 유스케이스의 결합이 분리되는 한 시스템의 아키텍처는 그 팀 구조를 뒷받침해줄 것이다.

배포 독립성

유스케이스와 계층의 결합이 분리되면 배포 측면에서도 고도의 유연성이 생긴다. 실제로 결합을 제대로 분리했다면 운영 중인 시스템에서도 계층과 유 스케이스를 교체할 수 있다.

중복

소프트웨어에서 중복은 일반적으로 나쁜 것이다.

하지만 중복으로 보이는 두 코드 영역이 각자의 경로로 발전한다면, 즉 서로 다른 속도와 다른 이유로 변경된다면 이 두 코드는 진짜 중복이 아니다. 몇 년이 지나 다시 보면 두 코드가 매우 다르다는 사실을 알게 될 것이다.

예를 들어 두 유스케이스의 화면 구조가 매우 비슷하다고 가정해 보자. 아키텍트는 이 구조에 사용할 코드를 통합하고 싶은 유혹을 강하게 느낄 것이다.
하지만 정말 그래야 할까? 이는 진짜 중복이 아닌 우발적 중복일 가능성이 높다. 시간이 지나면서 두 화면은 서로 다른 방향으로 분기하며, 결국에는 매우 다른 모습을 가질 가능성이 높다. 이러한 이유로, 해당 코드를 통합하지 않도록 유의해야 한다. 그렇지 않으면 나중에 코드를 다시 분리하느라 큰 수고를 감수해야 한다.

유스케이스를 수직으로 분리할 때 이러한 문제와 마주칠테고, 이들 유스케이스를 통합하고 싶다는 유혹을 받게 될 것이다.
자동 반사적으로 중복을 제거해 버리는 잘못을 저지르는 유혹을 떨쳐내라. 중복이 진짜 중복인지 확인하라.

마찬가지로 계층을 수평으로 분리하는 경우, 특정 데이터 베이스 레코드의 데이터 구조가 특정 화면의 데이터 구조와 상당히 비슷하다는 점을 발견 할 수도 있다. 이때 데이터 베이스 레코드와 동일한 형태의 뷰 모델(view model)을 만들어서 각 항목을 복사하는 게 아니라, 데이터베이스 레코드를 있는 그대로 전달하고 싶다는 유혹을 받을 수도 있다.
이러한 중복은 거의 확실히 우발적이다. 뷰 모델을 별도로 만드는 일은 그다지 많은 노력이 들지 않을 뿐만 아니라, 계층 간 결합을 적절하게 분리하여 유지하는 데도 도움이 될 것이다.

결합 분리 모드

유스케이스를 위해 수행하는 그 작업들(결합 분리)은 운영에도 도움이 된다. 하지만 운영 측면에서 이점을 살리기 위해선 결합을 분리할 때 적절한 모드를 선택해야 한다.

계층과 유스케 이스의 결합을 분리하는 방법은 다양하다.


소스 수준 분리 모드

소스 코드 모듈 사이의 의존성을 제어할 수 있다. 이를 통해 하나의 모듈이 변하더라도 다른 모듈을 변경하거나 재컴파일하지 않도록 만들 수 있다.

이 모드에서는 모든 컴포넌트가 같은 주소 공간에서 실행되고, 서로 통신할 때는 간단한 함수 호출을 사용한다. 컴퓨터 메모리에는 하나의 실행 파일만이 로드된다. 이러한 구조를 흔히 모노리틱 구조라고 부른다.

배포 수준 분리 모드

jar 파일, DLL, 공유 라이브러리와 같이 배포 가능한 단위들 사이의 의존성을 제어할 수 있다. 이를 통해 한 모듈의 소스 코드가 변하더라도 다른 모듈을 재빌드하거나 재배포하지 않도록 만들 수 있다. 많은 컴포넌트가 여전히 같은 주소 공간에 상주하며, 단순한 함수 호출을 통해 통신할 수 있다. 어떤 컴포넌트는 동일한 프로세서의 다른 프로세스에 상주하고, 프로세스 간 통신,소켓, 또는 공유 메모리를 통해 통신할수있다. 이 모드의 중요한 특징은 결합이 분리된 컴포넌트가 jar파일, Gem 파일, DLL과 같이 독립적으로 배포할 수 있는 단위로 분할되어 있다는 점이다.

서비스 수준 분리 모드

의존하는 수준을 데이터 구조 단위까지 낮출 수 있고, 순전히 네트워크 패킷을 통해서만 통신하도록 만들 수 있다. 이를 통해 모든 실행 가능한 단위는 소스와 바이너리 변경에 대해 서로 완전히 독립적이게 된다. (예: 서비스 또는 마이크로서비스)


어떤 모드가 시용하기에 가장 좋은가?
프로젝트 초기 단계는 어떤 모드가 최선인지 알기 어렵다는 게 답이다. 사실 프로젝트가 성숙해갈수록 최적인 모드가 달라질 수 있다.

좋은 아키텍처는 시스템이 모노리틱 구조로 태어나서 단일 파일로 배포 되더라도, 이후에는 독립적으로 배포 가능한 단위들의 집합으로 성장하고, 또 독립적인 서비스나 마이크로서비스 수준까지 성장할 수 있도록 만들어져야 한다. 또한 좋은 아키텍처라면 나중에 상황이 바뀌었을 때 이 진행 방향을 거 꾸로 돌려 원래 형태인 모노리틱 구조로 되돌릴 수도 있어야 한다.

좋은 아키텍처는 이러한 변경으로부터 소스 코드 대부분을 보호한다. 좋은 아키텍처는 결합 분리 모드를 선택사항으로 남겨두어서 배포 규모에 따라 가장 적합한 모드를 선택해 사용할 수 있게 만들어 준다.


17장. 경계: 선 긋기

소프트웨어 아키텍처는 선을 긋는 기술이며, 이 책에서는 경계(boundary) 라고 표현했다.

경계는 소프트웨어 요소를 서로 분리하고, 경계 한편에 있는 요소가 반대편에 있는 요소를 알지 못하도록 막는다.

초기에 그어지는 선들은 가능한 한 오랫동안 결정을 연기시키기 위해, 그래서 이들 결정이 핵심적인 업무 로직을 오염시키지 못하게 만들려는 목적으로 쓰인다.

아키텍트의 목표는 필요한 시스템을 만들고 유지하는 데 드는 인적 자원을 최소화하는 것이라는 사실을 상기하자. 그렇다면 인적 자원의 효율을 떨어뜨리는 요인은 무엇일까? 바로 결합(coupling)이다. 특히 너무 일찍 내려진 결정에 따른 결합이다.

어떤 종류의 결정이 이른 결정일까? 바로 시스템의 업무 요구사항, 즉 유스케이스와 아무런 관련이 없는 결정이다.
프레임워크, 데이터베이스, 웹 서버, 유틸리티 라이브러리, 의존성 주입에 대한 결정 등이 여기 포함된다.

좋은 시스템 아키텍처란 이러한 결정이 부수적이며, 결정을 연기할 수 있는 아키텍처다. 좋은 시스템 아키텍처는 이런 결정에 의존하지 않는다. 좋은 시스템 아키텍처는 이러한 결정을 가능한 한 최후의 순간에 내릴 수 있게 해주며, 결정에 따른 영향이 크지 않게 만든다.

어떻게 선을 그을까? 그리고 언제 그을까?

GUI는 업무 규칙과는 관련 없기 때문에, 이 둘 사이에는 반드시 선이 있어야 한다. 데이터베이스는 GUI와는 관련이 없으므로, 이 둘 사이에도 반드시 선이 있어야 한다. 데이터 베이스는 업무 규칙과 관련이 없으므로, 이 둘 사이에도 선이 있어야한다.

데이터베이스는 업무 규칙이 간접적으로 사용할 수 있는 도구다. 업무 규칙은 스키마, 쿼리언어, 또는 데이터 베이스와 관련된 나머지 세부사항에 대해 어떤 것도 알아서는 안 된다. 업무 규칙이 알아야 할 것은 데이터를 가져오고 저장할 때 사용할 수 있는 함수 집합이 있다는 사실이 전부다. 이러한 함수 집합을 통해서 우리는 데이터 베이스를 인터페이스 뒤로 숨길 수 있다.

입력과 출력은?

입력과 출력은 중요하지 않다. 이 원칙은 처음에는 이해하기 힘들다. 우리는 시스템의 행위를 입출력이 지닌 행위적 측면에서 생각하는 경향이 있다.

비디오 게임의 경우 사용자 경험은 인터페이스에 의해 좌우된다. 화면, 마우스, 버튼, 음향이 바로 그 인터페이스다.
이러한 인터페이스 뒤에는 인터페이스를 조작하는 모델(데이터 구조와 함수로 구성된 정교한 집합)이 존재한다는 사실을 잊어버린다. 더 중요한 사실은 모델은 인터페이스를 전혀 필요로 하지 않는다는 점이다. 게임이 화면에 전혀 출력되지 않더라도 모델은 게임에서 발생되는 모든 이벤트를 모델링하면서 주어진 역할을 충실하게 수행한다. 인터페이스는 모델에게 있어 중요하지 않다. 중요한 것은 업무 규칙이다.

플러그인 아키텍처

데이터베이스와 GUI에 대해 내린 두 가지 결정을 하나로 합쳐서 보면 컴포넌트 추가와 관련한 일종의 패턴이 만들어진다. 이 패턴은 시스템에서 서드 파티 플러그인을 사용할 수 있게 한 바로 그 패턴과 동일하다.

사실 소프트웨어 개발 기술의 역사는 플러그인을 손쉽게 생성하여, 확장 가능하며 유지보수가 쉬운 시스템 아키텍처를 확립할 수 있게 만드는 방법에 대한 이야기다. 선택적이거나 또는 수많은 다양한 형태로 구현될 수 있는 나머지 컴포넌트로부터 핵심적인 업무 규칙은 분리되어 있고, 또한 독립적이다.

시스템을 플러그인 아키텍처로 배치함으로써 변경이 전파될 수 없는 방화 벽을 생성할 수 있다. GUI가 업무 규칙에 플러그인 형태로 연결되면 GUI에 서 발생한 변경은 절대로 업무규칙에 영향을 미칠 수 없다.

경계는 변경의 축(axis of change)이 있는 지점에 그어진다. 경계의 한쪽에 위치한 컴포넌트는 경계 반대편의 컴포넌트와는 다른 속도로, 그리고 다른 이유 로 변경된다.

GUI는 업무 규칙과는 다른 시점에 다른 속도로 변경되므로, 둘 사이에는 반드시 경계가 필요하다. 업무 규칙은 의존성 주입 프레임워크와는 다른 시 점에 그리고 다른 이유로 변경되므로, 둘 사이에도 반드시 경계가 필요하다.

이 역시도 순전히 단일 책임 원칙에 해당한다. 단일 책임 원칙은 어디에 경계를 그어야 할지를 알려준다.

결론

소프트웨어 아키텍처에서 경계선을 그리려면 먼저 시스템을 컴포넌트 단위 로 분할해야 한다. 일부 컴포넌트는 핵심 업무 규칙에 해당한다. 나머지 컴

포넌트는 플러그인으로,핵심 업무와는 직접적인 관련이 없지만 필수 기능을 포함한다. 그런 다음 컴포넌트 사이의 화살표가 특정 방향, 즉 핵심 업무를 향하도록 이들 컴포넌트의 소스를 배치한다.

이는의존성 역전원칙과안정된추상화원칙을응용한것임을눈치챌수 있어야 한다. 의존성 화살표는 저수준 세부사항에서 고수준의 추상화를 향 하도록 배치된다.


18장. 경계 해부학

시스템 아키텍처는 일련의 소프트웨어 컴포넌트와 그 컴포넌트들을 분리하는 경계에 의해 정의된다. 이러한 경계는 다양한 형태로 나타난다.

경계 횡단

'런타임에 경계를 횡단한다' 함은 그저 경계 한쪽에 있는 기능에서 반대편 기능을 호출하여 데이터를 전달히는 일에 불과하다. 적절한 위치에서 경계를 횡단하게 하는 비결은 소스 코드 의존성 관리에 있다.

왜 소스 코드일까? 왜냐하면 소스 코드 모듈 하나가 변경되면, 이에 의존하는 다른 소스 코드 모듈도 변경하거나, 다시 컴파일해서 새로 배포해야 할 지도 모르기 때문이다. 경계는 이러한 변경이 전파되는 것을 막는 방화벽을 구축하고 관리하는 수단으로써 존재한다.

단일체

아키텍처 경계 중에서 가장 단순하며 가장 혼한 형태는 물리적으로 엄격하게 구분되지 않는 형태다. 이 형태에서는 함수와 데이터가 단일 프로세서에서 같은 주소 공간을 공유하며 그저 나름의 규칙에 따라 분리되어 있을 뿐이다. (소스 수준 분리 모드)

배포 관점에서 보면 이는 소위 단일체(monolith)라고 불리는 단일 실행 파일에 지나지 않는다.

이처럼 배포 관점에서 볼 때 단일체는 경계가 드러나지 않는다. 그렇다고 해서 단일체에는 경계가 실제로 존재하지 않거나, 경계 자체가 무의미하 다는 뜻은 아니다.

이러한 아키텍처는 거의 모든 경우에 특정한 동적 다형성에 의존하여 내부 의존성을 관리한다. 바로 이 때문에 최근 수십 년 동안 객체 지향 개발이 아 주 중요한 패러다임이 될 수 있었다.

가장 단순한 형태의 경계 횡단은 저수준 클라이언트에서 고수준 서비스로 향하는 함수 호출이다.
이 경우 런타임 의존성과 컴파일타임 의존성은 모두 같은 방향, 즉 저수준 컴포넌트에서 고수준 컴포넌트로 향한다. 고수준 클라이언트가 저수준 서비스를 호출해야 한다면 동적 다형성을 사용하여 제어흐름과는 반대 방향으로 의존성을 역전시킬 수 있다. 이렇게 하면 런타임 의존성은 컴파일타임 의존성과는 반대가 된다.

정적 링크된 모노리틱 구조의 실행 파일이라도 이처럼 규칙적인 방식으로 구조를 분리하면 프로젝트를 개발, 테스트, 배포하는 작업에 큰 도움이 된다. 팀들은 서로의 영역에 침범하지 않은 채 자신만의 컴포넌트를 독립적으로 작업할 수 있다. 고수준 컴포넌트는 저수준 세부사항으로부터 독립적으로 유 지된다.

단일체에서 컴포넌트 간 통신은 매우 빠르고 값싸다. 통신은 전형적인 함수 호출에 지나지 않기 때문이다. 결과적으로, 소스 수준에서 결합이 분리되 면 경계를 가로지르는 통신은 상당히 빈번할 수 있다.

배포형 컴포넌트

아키텍처의 경계가 물리적으로 드러날 수도 있는데 그중 가장 단순한 형태는 동적 링크 라이브러리다. 컴포넌트를 이 형태로 배포하면 따로 컴파일하지 않고 곧바로 사용할 수 있다. 대신 컴포넌트는 바이너리와 같이 배포 가능한 형태로 전달된다. (배포 수준 결합 분리 모드)

이러한 배포 과정에서만 차이가 날 뿐,배포 수준의 컴포넌트는 단일체와 동일하다. 일반적으로 모든 함수가 동일한 프로세서와 주소 공간에 위치하며, 컴포넌트를 분리하거나 컴포넌트 간 의존성을 관리하는 전략도 단일체와 동일하다.

단일체와 마찬가지로 배포형 컴포넌트의 경계를 가로지르는 통신은 순전히 함수 호출에 지나지 않으므로 매우 값싸다. 동적 링크와 런타임 로딩으로 인해 최초의 함수 호출은 오래 걸릴 수 있지만,대체로 이들 경계를 가로지르는 통신은 매우 빈번할 것이다.

스레드

단일체와 배포형 컴포넌트는 모두 스레드를 활용할 수 있다. 스레드는 아키텍처 경계도 아니며 배포 단위도 아니다. 이보다 스레드는 실행 계획과 순서 를 체계화하는 방법에 가깝다. 모든 스레드가 단 하나의 컴포넌트에 포함될 수도 있고, 많은 컴포넌트에 걸쳐 분산될 수도 있다.

로컬 프로세스

훨씬 강한 물리적 형태를 띠는 아키텍처 경계로는 로컬 프로세스가 있다. 로컬 프로세스는 주로 명령행이나 그와 유사한 시스템 호출을 통해 생성된다.

각 로컬 프로세스는 정적으로 링크된 단일체 이거나 동적으로 링크된 여러개의 컴포넌트로 구성될 수 있다. 전자의 경우, 여러 모노리틱 프로세스가 같 은 컴포넌트들을 가지고 있을 수 있다. 반면 후자의 경우, 동적으로 링크된 배포형 컴포넌트들을 서로 공유할 수 있다.

로컬 프로세스 간 분리 전략은 단일체나 바이너리 컴포넌트의 경우와 동일하다. 소스 코드 의존성의 화살표는 단일체나 바이너리 컴포넌트와 동일한 방향으로 경계를 횡단한다. 즉,항상 고수준 컴포넌트를 향한다.

따라서 로컬 프로세스에서는 고수준 프로세스의 소스 코드가 저수준 프로세스의 이름, 물리 주소, 레지스트리 조회 키를 절대로 포함해서는 안된다. 저수준 프로세스가 고수준 프로세스의 플러그인이 되도록 만드는 것이 아키텍처 관점의 목표라는 사실을 기억하자.

로컬 프로세스 경계를 지나는 통신은 제법 비싼 작업에 속한다. 따라서 통신이 너무 빈번하게 이뤄지지 않도록 신중하게 제한해야 한다.

서비스

물리적인 형태를 띠는 가장 강력한 경계는 바로 서비스다. 서비스는 프로세스로, 일반적으로 명령행 또는 그와 동등한 시스템 호출을 통해 구동된다. 서비스는 자신의 물리적 위치에 구애받지 않는다

로컬 프로세스에 적용한 규칙이 서비스에도 그대로 적용 된다. 저수준 서비스는 반드시 고수준 서비스에 ‘플러그인’되어야 한다. 고수준 서비스의 소스 코드에는 저수준 서비스를 특정 짓는 어떤 물리적인 정보도 절대 포함해서는 안 된다.

서비스 경계를 지나는 통신은 함수 호줄에 비해 매우 느리다. 따라서 주의를 기울여서, 가능하다면 빈번하게 통신하는 일을 피해야 한다. 이 수준의 통신에서는 지연(latency)에 따른 문제를 고수준에서 처리할 수 있어야 한다.


19장. 정책과 수준

소프트웨어 시스템이란 정책을 기술한 것이다. 실제로 컴퓨터 프로그램의 핵심부는 이게 전부다. 컴퓨터 프로그램은 각 입력을 출력으로 변환하는 정책을 상세하게 기술한 설명서다.

대다수의 주요 시스템에서 하나의 정책은 이 정책을 서술하는 여러 개의 조그만 정책들로 쪼갤 수 있다. 소프트웨어 아키텍처를 개발하는 기술에는 이러한 정책을 신중하게 분리하고, 정책이 변경되는 양상에 따라 정책을 재편성하는 일도 포함된다.

이 장에서 설명한 정책에 대한 논의는 단일 책임 원칙, 개방 폐쇄 원칙, 공통 폐쇄 원칙, 의존성 역전 원칙, 안정된 의존성 원칙, 안정된 추상화 원칙을 모두 포함한다. 이 원칙들의 설명을 다시 읽어 보며 각 원칙이 어디에서 무슨 이유로 사용되었는지를 찾아보자.


정책을 컴포넌트로 묶는 기준은 정책이 변경되는 방식에 달려있다

단일 책임 원칙(SRP)과 공통 폐쇄 원칙(CCP)에 따르면 동일한 이유로 동일한 시점에 변경되는 정책은 동일한 수준에 위치하며, 동일한 컴포넌트에 속해야 한다. 서로 다른 이유로, 혹은 다른 시점에 변경되는 정책은 다른 수준에 위치하며,반드시 다른 컴포넌트로 분리해야 한다.

고수준 정책, 즉 입력과 출력에 서부터 멀리 떨어진 정책은 저수준 정책에 비해 덜 빈번하게 변경되고, 보다 중요한 이유로 변경되는 경향이 있다. 저수준 정책, 즉 입력과 출력에 가까이 위치한 정책은 더 빈번하게 변경되며, 보다 긴급성을 요하며, 덜 중요한 이유로 변경되는 경향이 있다.

혼히 아키텍처 개발은 재편성된 컴포넌트들을 비순환 방향 그래프(DAG) 구성하는 기술을 포함한다. 그래프에서 정점(node)은 동일한 수준 의 정책을 포함하는 컴포넌트에 해당한다. 방향이 있는 간선(edge)은 컴포넌트 사이의 의존성을 나타낸다. 간선은 다른 수준에 위치한 컴포넌트를 서로 연결한다.

이러한 의존성은 소스 코드, 컴파일 타임의 의존성이다. 이러한 의존성은 컴파일러가 제대로 동작하기 위해서 필요하다.

소스 코드 의존성은 그 수준에 따라 결합되어야 하며, 데이터 흐름을 기준으로 결합되어서는 안 된다.

좋은 아키텍처라면 각 컴포넌트를 연결할 때 의존성의 방향이 컴포넌트의 수준을 기반으로 연결되도록 만들어야 한다.

모든 소스 코드 의존성의 방향이 고수준 정책을 향할 수 있도록 정책을 분리했다면 변경의 영향도를 줄일 수 있다. 시스템의 최저 수준에서 중요하지 않지만 긴급한 변경이 발생하더라도, 보다 높은 위치의 중요한 수준에 미치는 영향은 거의 없게 된다.

이 논의는 저수준 컴포넌트가 고수준 컴포넌트에 플러그인되어야 한다는 관점으로 바라볼 수도 있다.


20장. 업무 규칙

애플리케이션을 업무 규칙과 플러그인으로 구분하려면 업무 규칙이 실제로 무엇인지를 잘 이해해야만 한다.

업무 규칙은 소프트웨어 시스템이 존재하는 이유다. 업무 규칙은 핵심적인 기능이다. 업무 규칙은 수익을 내고 비용을 줄이는 코드를 수반한다.

업무 규칙은 사용자 인터페이스나 데이터베이스와 같은 저수준의 관심사로 인해 오염되어서는 안되며, 원래 그대로의 모습으로 남아 있어야 한다.
이상적으로는 업무 규칙을 표현하는 코드는 반드시 시스템의 심장부에 위치해야 하며, 덜 중요한 코드는 이 심장부에 플러그인되어야 한다. 업무 규칙은 시스템에서 가장 독립적이며 가장 많이 재사용할 수 있는 코드여야 한다.


21장. 소리치는 아키텍처

아키텍처는 시스템을 이야기해야 하며,시스템에 적용한 프레임워크에 대해이야기해서는 안 된다.

여러분의 애플리케이션 아키텍처는 뭐라고 소리치는가? 상위 수준의 디렉터리 구조, 최상위 패키지에 담긴 소스파일을 볼 때, 이 아키텍처는 “헬스 케어 시스템이야” 라고 소리치는가? 아니면 “레일스(Rails)야”, “스프링(Spring) / 하이버네이트(Hibernate)야”,아니면 “ASP야” 라고 소리치는가?

헬스 케어 시스템을 구축하고 있다면, 새로 들어온 프로그래머가 소스 저장소를 봤을 때 첫 인상은 “오, 헬스 케어 시스템이군” 이어야만 한다. 새로 합류한 프로그래머는 시스템이 어떻게 전달될지 알지 못한 상태에서도 시스템의 모든 유스케이스를 이해할 수 있어야 한다.

아키텍처의 테마

이바 야콥슨(lvarJacobson)이 소프트웨어 아키텍처에 대해 쓴 저서인 'Object Oriented Software Engineering'의 부제가 '유스케이스 주도 접근법(Use Case Driven Approach)'이라는 점을 주목하자.

이 책에서 야콥슨은 소프트웨어 아키텍처는 시스템의 유스케이스를 지원하는 구조라고 지적했다. 소프트웨어 애플리케이션의 아키텍처도 애플리케 이션의 유스케이스에 대해 소리쳐야 한다.

아키텍처는 프레임워크에 대한 것이 아니다. 아키텍처를 프레임워크로부터 제공받아서는 절대 안된다. 프레임워크는 사용하는 도구일 뿐, 아키텍처가 준수해야 할 대상이 아니다. 아키텍처를 프레임워크 중심으로 만들어버리면 유스케이스가 중심이 되는 아키텍처는 절대 나올 수 없다.

아키텍처의 목적

좋은 아키텍처는 유스케이스를 그 중심에 두기 때문에, 프레임워크나 도구, 환경에 전혀 구애받지 않고 유스케이스를 지원하는 구조를 아무런 문제 없이 기술할 수 있다.

주택에 대한 계획서를 다시 한번 생각해 보자. 아키텍트가 주목하는 첫 번째 관심사는 주택이 거주하기에 적합한 공간임을 확실히 하는 것이지,벽돌로 지어지는지를 확인하는 것이 아니다. 실제로 아키텍트는 외장재를 소유주가 결정할 수 있도록 애쓰지만, 이 역시도 계획서가 유스케이스를 확실히 충족시킨 이후다.

좋은 소프트웨어 아키텍처는 프레임워크, 데이터베이스,웹 서버,그리고 여타 개발 환경 문제나 도구에 대해서는 결정을 미룰 수 있도록 만든다. 프레 임워크는 열어 둬야 할 선택사항이다. 좋은 아키텍처는 프로젝트의 훨씬 후반까지 레일스, 스프링, 하이버네이트,톰켓, MySQL에 대한 결정을 하지 않아도 되도록 해준다. 뿐만 아니라 이러한 결정을 쉽게 번복할 수 있도록 한다.

테스트하기 쉬운 아키텍처

아키텍처가 유스케이스를 최우선으로 한다면, 프레임워크를 전혀 준비하지 않더라도 필요한 유스케이스 전부에 대해 단위 테스트를 할 수 있어야 한다. 테스트를 돌리는 데 웹 서버가 반드시 필요한 상황이 되어서는 안 된다. 데이터베이스가 반드시 연결되어 있어야만 테스트를 돌릴 수 있어서도 안 된다.

엔티티 객체는 반드시 오래된 방식의 간단한 객체(plain old object)여야 하며, 프레임워크나 데이터베이스, 또는 여타 복잡한 것들에 의존해서는 안된다. 유스케이스 객체가 엔티티 객체를 조작해야 한다.

최종적으로, 프레임워크로 인한 어려움을 겪지 않고도 반드시 이 모두를 있는 그대로 테스트 할 수 있어야한다.

0개의 댓글