5부. 아키텍처
15장. 아키텍처란?
요약: 좋은 아키텍트는 세부사항을 정책으로부터 신중하게 가려내고, 정책이 세부사항과 결합되지 않도록 분리한다. 이를 통해 정책은 세부사항에 관한 어떠한 지식도 갖지 못하게 되며, 어떤 경우에도 세부사항에 의존하지 않게 된다. 좋은 아키텍트는 세부사항에 대한 결정을 가능한 한 오랫동안 미룰 수 있는 방향으로 정책을 설계한다.
소프트웨어 아키텍처란? 시스템을 구축했던 사람들이 만들어낸 시스템의 형태
시스템 아키텍처 영향도
- 높은곳: 개발, 배포, 유지보수
- 낮은곳: 운영
시스템 아키텍처는 시스템의 동작 여부와는 거의 관련이 없다.
대체로 운영에서의 문제는 겪지 않으며(대부분 잘 동작한다), 배포/유지보수 과정에서 어려움을 겪는다.
아키텍처 목적: 시스템의 생명주기를 지원하는 것.
아키텍처의 궁극적인 목표
시스템의 수명과 관련된 비용을 최소화하고, 프로그래머의 생산성을 최대화하는 것.
좋은 아키텍처란?
누구든지 시스템을 쉽게 이해하고, 쉽게 개발하며, 쉽게 유지보수하고, 쉽게 배포가능한 시스템
개발
시스템 아키텍처는 개발팀이 시스템을 쉽게 개발할 수 있는 구조여야 한다.
배포
소프트웨어 시스템이 사용되려면 반드시 배포가 필요하다.
배포 비용을 낮추어 생산성을 높여야 한다.
개발 초기 단계에서부터 배포에 대한 고려가 필요하다.
운영
아키텍처가 운영에 미치는 영향은 적다.
why?
운영에서 겪는 대다수의 어려움은 하드웨어를 투입해서 해결할 수 있다.
“하드웨어는 값싸고 인력은 비싸다”
-> 운영을 방해하는 아키텍처가 개발, 배포, 유지보수를 방해하는 아키텍처보다 비용이 덜하다.
유지보수
비용이 가장 비싼 부분.
유지보수란?
- 새로운 기능 추가
- 기존 기능 결함 대응
유지보수의 가장 큰 비용은 탐사와 이로 인한 위험부담 이다.
- 탐사 비용: 기존 소프트웨어에 새로운 기능을 추가하거나 결함을 수정할 때 어디를 고칠지, 어떤 전략을 사용할지 등을 고민하고 결정하는데 드는 비용
- 위험부담 비용: 변경사항을 반영할 때 의도치 않은 결함이 발생 가능성에 대한 비용
비용 줄이는 법: 아키텍처를 잘 만들자
- 시스템을 컴포넌트로 분리
- 안정된 인터페이스를 두어 서로 격리
=> 탐사 비용을 줄이고, 위험부담 비용도 줄인다
소프트웨어를 부드럽게 유지하는 방법: 선택사항 열어 두기
소프트웨어를 만든 이유는 기계의 행위를 빠르고 쉽게 변경하기 위함이다.
이러한 유연성은 시스템의 형태, 컴포넌트의 배치방식, 컴포넌트가 상호 연결되는 방식에 크게 의존함.
소프트웨어를 부드럽게 유지하기 위해서는 선택사항을 가능한 한 많이, 그리고 가능한 한 오랫동안 열어두어야 한다.
(열어 둬야 할 선택사항: 중요치 않은 세부사항)
모든 소프트웨어 시스템은 주요한 두 가지 구성요소로 분해할 수 있다.
- 정책 : 시스템의 진정한 가치(모든 업무 규칙과 업무 절차를 구체화)
- 세부사항 : 사람, 외부 시스템, 프로그래머가 정책과 소통할 때 필요한 요소. 정책이 가진 행위에 조금도 영향을 미치지 않음. (eg. 입출력 장치, DB, 웹 시스템, 서버, 프레임워크, 통신 프로토콜 등)
정책을 가장 핵심적인 요소로 식별하고,
세부사항은 정책에 무관하게 만들 수 있는 형태의 시스템을 구축해야 한다.
=> 이를 통해 세부사항을 결정하는 일을 미루거나 연기.
예제 4가지.
-
DB 개발 초기에는 DB를 선택할 필요가 없다. 고수준의 정책은 어떤 종류의 DB를 사용하는지 신경 써서는 안 된다. 정말 신중한 아키텍트라면, 고수준의 정책을 DB가 관계형/분산형/계층형/플랫파일 인지와는 관련이 없도록 만들어야 한다.
-
웹서버 개발 초기에는 웹 서버를 선택할 필요가 없다. 고수준의 정책은 자신이 웹을 통해 전달된다는 사실을 알아서는 안 된다. HTML/AJAX/JSF 과 같은 웹개발 기술들에 대해 고수준의 정책이 전혀 알지 못하게 만들면, 프로젝트 후반까지는 어떤 종류의 웹 시스템을 사용할지 결정하지 않아도 된다.
-
사용자 인터페이스(?) 개발 초기에는 REST를 적용할 필요가 없다. 고수준의 정책은 외부 세계로의 인터페이스에 대해 독립적이어야 하기 때문이다.
마이크로서비스/SOA 도 적용할 필요가 없다. 고수준의 정책은 이러한 것들에 신경써서는 안된다!! -
의존성 주입 프레임워크 개발 초기에는 의존성 주입 프레임워크를 적용할 필요가 없다.
고수준의 정책은 의존성을 해석하는 방식에 대해 신경 써서는 안 된다.
세부사항에 대해 신경쓰지 않고 고수준의 정책을 만든다면, 세부사항에 대한 결정을 오랫동안 미루거나 연기할 수 있다.
이로인한 장점은 더 많은 실험/시도/정보를 토대로 제대로 된 결정을 내릴 수 있다.
정책이 만들어지면, 정책에 대해 다양한 DB를 후보로 두고, 적용가능성/성능 을 검토해볼 수 있다. (DB 뿐만 아니라, 웹시스템, 웹 프레임워크 등도 마찬가지다.)
(뭔가 정책이 도메인영역/유즈케이스 인 듯한 느낌..?)
코드베이스로 생각해보자. => 정책. 세부사항. 할인정책은 세부사항일 수 있겠다…
16장. 독립성
시스템의 결합 분리 모드는 시간이 지나면서 바뀌기 쉬우며,
뛰어난 아키텍트라면 이러한 변경을 예측하여 큰 무리 없이 반영할 수 있도록 만들어야 한다.
좋은 아키텍처는 다음을 지원해야 한다.
- 시스템의 유스케이스,운영,개발,배포
유스케이스 관점
아키텍트의 최우선 관심사이다.
아키텍처는 시스템의 행위 에는 그다지 큰 영향을 주지 않는다.
다만 행위를 명확히 하고 외부로 드러내며, 이를 통해 시스템이 지닌 의도를 아키텍처 수준에서 알아볼 수 있게 만들어야 한다.
eg. 장바구니 애플리케이션
시스템의 유스케이스는 시스템 구조 자체에서 한눈에 드러나야 한다.
클래스/함수/모듈로서 아키텍처 내에서 핵심적인 자리를 차지할 뿐만 아니라, 자신의 기능을 분명하게 설명하는 네이밍을 가져야 한다.
(21장)
애플리케이션 상위수준의 디렉터리 구조, 최상위 패키지에 담긴 소스 파일을 볼 때
- 아키텍쳐는 헬스케어시스템이야/재고관리시스템이야 라고 소리치는가? (Good)
- 아니면 Rails야. String이야. Hibernate야. 라고 소리치는가? (Bad)
운영 관점
아키텍처는 각 유스케이스에 걸맞는 처리량과 응답시간을 보장해야 한다. (eg. 시스템은 초당 100,000명의 고객을 처리해야 한다.)
이런 운영 작업을 허용할 수 있는 형태로 아키텍처를 구조화해야 한다.
병렬 실행 / 멀티쓰레드의 자원공유 / 모노리틱 등이 고려될 것이다.
뛰어난 아키텍처라면 이러한 결정을 미뤄둘 것이다. (선택사항 미루기)
Bad Case
만약 시스템이 모노리틱 구조를 갖는다면, 다중프로세스/다중스레드/MSA 형태가 필요해질 때 개선하기가 어렵다.
Good Case
아키텍처에서 각 컴포넌트를 적절히 격리하여 유지하고 컴포넌트 간 통신 방식을 특정 형태로 제한하지 않는다면, 시간이 지나 운영에 필요한 요구사항이 바뀌더라도 스레드, 프로세스, 서비스로 구성된 기술 스펙트럼 사이를 전환하는 일은 훨씬 쉬워질 것이다.
개발
아키텍처는 개발환경을 지원하는 데 있어 핵심적인 역할을 수행한다.
콘웨이의 법칙
시스템을 설계하는 조직이라면 어디든지 그 조직의 의사소통 구조와 동일한 구조의 설계를 만들어 낼 것이다.
(많은 팀으로 구성되며 관심사가 다양한 조직에서 어떤 시스템을 개발해야 한다면, 각 팀이 독립적으로 행동하기 편한 아키텍처를 반드시 확보하여 개발하는 동안 팀들이 서로를 방해하지 않도록 해야 한다. 이러한 아키텍처를 만들려면 잘 격리되어 독립적으로 개발 가능한 컴포넌트 단위로 시스템을 분할할 수 있어야 한다. 그래야만 이들 컴포넌트를 독립적으로 작업할 수 있는 팀에 할당할 수 있다.)
배포
좋은 아키텍처는 수십 개의 작은 설정 스크립트나 속성 파일을 약간씩 수정하는 방식을 사용하지 않는다. 좋은 아키텍처는 꼭 필요한 디렉터리나 파일을 수작업으로 생성하게 내버려 두지 않는다. 좋은 아키텍처는 시스템이 빌드된 후 즉각 배포할 수 있도록 지원한다.
선택사항 열어놓기
좋은 아키텍처는 선택사항을 열어 둠으로써, 향후 시스템에 변경이 필요할 때 어떤 방향으로든 쉽게 변경할 수 있도록 한다.
좋은 아키텍처는 컴포넌트 구조와 관련된 이 관심사들 사이에서 균형을 맞추고, 각 관심사 모두를 만족시킨다.
- 균형을 맞추기는 굉장히 어렵지만, 아키텍처 원칙들은 관심사들 사이에서 균형을 맞추는데 도움이 된다.
- 균형을 맞추려는 목표점이 없는 경우에도 도움이 된다.
- 시스템을 제대로 격리된 컴포넌트 단위로 분할할 때 도움이 된다.
- 결과적으로, 선택사항을 가능한 한 많이, 가능한 한 오랫동안 열어 둘 수 있게 해준다.
계층 결합 분리 (Layer)
다른 이유로 변경되는 것들은 분리하고 동일한 이유로 변경되는 것들은 묶는다. => 단일책임원칙 / 공통 폐쇄 원칙 을 적용하자.
-
UI vs 업무규칙 -> 장점: 두 요소를 서로 독립적으로 변경할 수 있을 뿐만 아니라, 유스케이스는 여전히 가시적이며 분명하게 유지할 수 있다.
- 업무 규칙 분리 (애플리케이션과 관련 O / X)
- 애플리케이션과 밀접한 관련이 있는 업무 규칙 (eg. 입력 필드 유효성 검사)
- 애플리케이션과 독립적인 업무 규칙 (eg. 계좌의 이자 계산 / 재고품 집계) -> 서로 다른 속도로, 다른 이유로 변경될 것이다. 이에 서로 분리하고 독립적으로 변경할 수 있도록 만들어야 한다.
- DB / Query Language / Scheme 또한 기술적인 세부사항이다. (UI 또는 업무 규칙과 관련없다.)
결론: 아키텍트는 이들을 시스템의 나머지 부분으로부터 분리하여 독립적으로 변경할 수 있도록 해야만 한다.
자. 수평적인 계층으로 분리하는 방법을 알게 되었다.
eg. UI, 애플리케이션에 특화된 업무규칙, 애플리케이션과 독립적인 업무 규칙, DB
유스케이스 결합 분리
유스케이스도 서로 다른 이유로 변경되는 경우가 있다.
유스케이스는 시스템을 분할하는 매우 자연스러운 방법이다.
eg. 주문입력시스템에서 아래 두 유스케이스는 서로 다른 속도, 다른 이유로 변경된다.
- 주문을 추가하는 유스케이스
- 주문을 삭제하는 유스케이스
각 유스케이스는 UI 일부/앱 특화 업무규칙 일부/앱 독립 업무규칙 일부/DB 일부를 사용한다.
따라서 우리는 시스템을 수평적 계층으로 분할하면서 동시에 해당 계층을 가로지르는, 얇은 수직적인 유스케이스로 시스템을 분할할 수 있다.
eg. 주문 추가 유스케이스의 UI와 주문 삭제 유스케이스의 UI 를 분리.
시스템에서 서로 다른 이유로 변경되는 요소들의 결합을 분리하면 기존 요소에 지장을 주지 않고도 새로운 유스케이스를 계속해서 추가할 수 있게 된다.
결합 분리 모드
결합을 분리 했을 때 장점
1. 운영 관점
개발 독립성
컴포넌트가 완전히 분리되면 팀 사이 간섭은 줄어든다.
eg. 업무 규칙이 UI를 알지 못하면 UI에 중점을 둔 팀은 업무 규칙에 중점을 둔 팀에 그다지 영향을 줄 수 없다.
eg. 유스케이스 자체도 서로 결합이 분리되면 addOrder 유스케이스에 중점을 둔 팀이 deleteOrder 유스케이스에 중점을 둔 팀에 개입할 가능성은 거의 없다.
배포 독립성
배포 측면에서 유연성이 생긴다. 결합이 제대로 분리가 되었다면 시스템에서 계층과 유스케이스를 교체할 수 있다.
새로운 유스케이스를 추가하는 일은 시스템의 나머지는 그대로 둔 채 새로운 jar파일이나 서비스 몇 개를 추가하면 끝나는 정도로 단순한 일이 된다.
중복
중복은 두가지 종류가 있다.
- 진짜 중복
- 중복으로 보이는 두 코드 영역이 한 곳 변경 시 다른 곳도 변경해주어야 하는 경우
- 하나의 코드로 합치는 것이 관리에 용이하다.
- 우발적 중복
- 중복으로 보이는 두 코드 영역이 각자의 경로로 발전하는 경우
- 시간이 지나면서 두 코드는 서로 다른 방향으로 변경될 가능성이 높다.
- 우발적 중복은 코드를 통합하지 않도록 유의해야 한다. 그렇지 않으면 나중에 다시 코드를 분리하느라 큰 수고를 감수해야 한다.
유스케이스를 수직으로 분리하는 경우 - 우발적 중복 발생
서로 비슷한 화면 구조, 비슷한 알고리즘, 비슷한 DB 쿼리/스키마 를 가지기 때문이다.
eg. 두 유스케이스의 화면 구조가 매우 비슷한 경우
=> 이들 유스케이스를 통합하고 싶다는 유혹을 받게 될 것이다.
하지만 코드를 통합하면 안된다. 시간이 지나면서 두 화면은 서로 다른 방향으로 분기하며, 결국에는 매우 다른 모습을 가질 가능성이 높기 때문이다.
중복이 진짜 중복인지 확인하라!
계층을 수평으로 분리하는 경우 - 우발적 중복 발생
특정 DB 레코드의 데이터 구조가 특정 화면의 데이터 구조와 상당히 비슷하다는 점을 발견할 수도 있다. 이때 DB 레코드를 그대로 UI까지 전달하고 싶다는 유혹을 받을 수 있다. 이러한 중복은 거의 확실히 우발적이다.
View Model을 별도로 만들어라. 노력도 별로 안들고 계층간 결합을 분리하여 유지하는데 도움이 될 것이다.
결합 분리 모드(다시)
계층과 유스케이스의 결합을 분리하는 방법 3가지
- 소스 코드 수준에서 분리
- 소스 코드 모듈 사이의 의존성 제어
- 모든 컴포넌트가 같은 주소 공간에서 실행됨.
- 통신: 함수 호출을 사용
- 모노리틱 구조
- 단점: 컨포넌트 간 의존성 꼬이기 쉽다(내생각)
- 바이너리 코드 수준에서 분리
- 배포 가능한 단위들(jar 파일 / DLL / 공유 라이브러리) 사이의 의존성 제어
- 많은 컴포넌트가 여전히 같은 주소 공간에서 실행됨.
- 통신: 함수 호출을 사용
- 어떤 컴폰너트는 동일한 프로세서의 다른 프로세스에 상주하고, 프로세스간 통신/소켓/공유메모리 를 통해 통신할 수 있다
- 실행 단위 수준에서 분리
- 통신: 네트워크 패킷을 통해서
- 마이크로서비스 구조
- 단점: 리소스 낭비 가능성
3개 중에 어느것이 좋은가? => 모른다. 프로젝트 성숙도에 따라 다르다.
단, 좋은 아키텍처라면 모노리틱이더라도 마이크로서비스 수준까지 성장할 수 있도로록 만들어져야 한다.
또는 마이크로서비스더라도 모노리틱 구조로 되돌릴 수 있어야 한다.
=> 좋은 아키텍처는 이러한 변경으로부터 소스 코드 대부분을 보호한다.
=> 좋은 아키텍처는 결합 분리 모드 를 선택사항으로 남겨두어서 배포 규모에 따라 가장 적합한 모드를 선택해 사용할 수 있게 만들어준다.