5부. 아키텍처
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개 중에 어느것이 좋은가? => 모른다. 프로젝트 성숙도에 따라 다르다.
단, 좋은 아키텍처라면 모노리틱이더라도 마이크로서비스 수준까지 성장할 수 있도로록 만들어져야 한다.
또는 마이크로서비스더라도 모노리틱 구조로 되돌릴 수 있어야 한다.
=> 좋은 아키텍처는 이러한 변경으로부터 소스 코드 대부분을 보호한다.
=> 좋은 아키텍처는 결합 분리 모드 를 선택사항으로 남겨두어서 배포 규모에 따라 가장 적합한 모드를 선택해 사용할 수 있게 만들어준다.