1장 설계와 아키텍쳐란?
사실 둘 사이에는 큰 차이가 없다.
좋은 소프트웨어 설계의 목표
소프트웨어 아키텍쳐의 목표는 필요한 시스템을 만들고 유지보수하는데 투입되는 인력을 최소화하는 것이 목표
설계 품질의 척도는 고객 요구를 만족시키는 데 드는 비용을 최소화하는데 드는 비용을 재는 척도와 다름이 없다!
사례 연구
한 기업의 사례를 통해, 엉망진창인 코드를 작성하게 될 때 발생할 수 있는 문제점을 설명
-
개발자들은 "지저분한 코드를 작성하면 단기간에는 빠르게 갈 수 있고, 장기적으로 볼 때만 생산성이 낮아진다" 라는 거짓말을 믿는다.
-
TDD를 적용했을 때, 장기적으로 본 결과 이터레이션별 걸린 시간이 TDD가 더 적음.
- 빨리 가는 유일한 방법은 제대로 가는 것이다.
결론
어떤 경우라도 개발 조직이 할 수 있는 가장 최고의 선택지는 조직에 스며든 과신을 인지하여 방지하고, 소프트웨어 아키텍처의 품질을 고민하는 것.
2장 두 가지 가치에 대한 이야기
모든 소프트웨어 시스템은 이해관계자에게 서로 다른 두 가지 가치를 제공. 그것이 행위와 구조.
- 소프트웨어 개발자는 두 가지를 모두 반드시 높게 유지해야하는 책임을 진다.
- 대체로 개발자가 둘 중 덜 중요한 가치에 집중하여 결국에는 소프트웨어 시스템이 쓸모없게 된다.
행위
소프트웨어의 첫번째 가치는 행위이다.
- 프로그래머는 기능 명세서나 요구사항 문서를 구체화할 수 있도록 돕느다.
- 이러한 요구사항을 기계가 위반하면, 프로그래머는 그 문제를 해결한다.
많은 프로그래머가 이러한 행동을 위한 활동이 자신이 해야 할 일의 전부라고 잘못 생각하고 있다.
아키텍쳐
소프트웨어가 본연의 목적을 추구하려면 소프트웨어는 반드시 부드러워야한다. 다시 말해 변경하기 쉬워야 한다.
-
변경사항을 간단하고 쉽게 적용할 수 있어야한다.
- 변경사항을 적용하는 데 드는 어려움은 변경되는 범위에 비례하며, 형태와는 관련이 없어야한다.
-
이해관계자는 범위가 비슷한 일을 맡기지만, 소프트웨어의 형태는 계속하여 변경된다.
- 즉 새로운 요청사항이 발생할 때마다 시스템의 형태와 요구사항의 형태가 계속 맞지 않아, 그 요구사항을 적용하는 것이 계속 힘들어진다.
-
그렇기에 아키텍처는 형태에 독립적이어야하고, 그럴수록 더욱 실용적이다.
더 높은 가치
소프트웨어 시스템을 더 쉽게 변경할 수 있도록 하는 것이 더 중요하다.
-
업무 관리자는 시스템이 동작하는 것을 더 중요하게 여길 수 있다. 또, 개발자는 보통 이것에 동조한다. 하지만 이것은 잘못되었다.
-
완벽하게 동작하지만 수정이 아예 불가능한 프로그램이 된다면, 이 프로그램은 요구사항이 변경될 때 동작하지 않게되고 쓸모가 없어진다.
-
동작은 하지 않지만 변경이 쉬운 프로그램이라면, 프로그램이 돌아가도록 만들 수 있고 여전히 동작하도록 유지보수 할 수 있다. 계속 쓸모있는 채로 남을 것이다.
수정이 아예 불가능한 프로그램이란?
- 사실 수정이 아예 불가능한 프로그램은 거의 없다. 하지만, 변경을 위한 비용이 변경으로 창출되는 수익을 초과하는 경우 이것은 거의 수정이 불가능한 경우가 되어버린다.
만약 추후 업무 관리자에게 "변경 비용이 너무 커서 변경할 수 없다"라고 이야기하게 된다면?
- "변경이 불가능한 상태에 처할 때까지 시스템을 방치했다."라며 화낼 가능성이 높다.
아이젠하워 매트릭스
두 가지 유형의 문제가 존재한다. 하나는 긴급하며, 다른하나는 중요하다. 긴급한 문제는 중요하지 않으며, 중요한 문제는 절대 긴급하지 않다.
- 소프트웨어의 첫 번째 가치인 행위는 긴급하지만 매번 높은 중요도를 가지지 않는다.
- 소프트웨어의 두 번째 가치인 아키텍처는 중요하지만 즉각적인 긴급성을 필요로 하는 경우는 절대 없다.
- 물론 어떤 일은 긴급하며, 중요할 수 있다. 또한, 긴급하지도 않고 중요하지도 않은 일도 존재한다.
위 정리에 따른 일의 우선순위는 다음과 같다.
- 긴급하고 중요한
- 긴급하지 않지만 중요한
- 긴급하지만 중요하지 않은
- 긴급하지도 않고 중요하지도 않은
-
즉, 중요한 일 ( 아키텍처 ) 은 1과 2에 해당하고 행위는 1과 3에 해당한다.
-
보통 업무관리자와 개발자는 3번의 일을 1번으로 격상시키는 경우가 많다. 즉, 긴급하고 중요한 일이 무엇인지를 인지하지 못하는 경우가 생긴다.
- 따라서 기능의 긴급성이 아닌 아키텍처의 중요성을 위해서는 소프트웨어 개발팀이 마땅히 책임을 져야한다.
아키텍쳐를 위해 투쟁해라
이러한 책임을 다하려면, 투쟁해야한다. 아키텍처가 후순위가 되는 것을 용납했다면, 소프트웨어 개발팀이 스스로 옳다고 믿는 가치를 위해 충분히 투쟁하지 않았다는 것이다.
- 효율적인 소프트웨어 개발 팀은 이러한 투쟁에서 정면으로 맞서 싸운다.
- 이것이 소프트웨어의 개발자의 책무 중 하나이다.
- 하나만 기억하자. 아키텍처가 후순위가 되면 시스템 개발의 비용은 더 많이 들고, 일부 또는 전체 시스템에 변경을 가하는 일이 현실적으로 불가능해진다.
3장 패러다임 개요
- 구조적 프로그래밍, 객체 지향 프로그래밍, 함수형 프로그래밍에 대해 설명한다.
구조적 프로그래밍
제어 흐름의 직접적인 전환에 대해 규칙을 부과한다.
데이크스타라는 점프와 같은 프로그램 구조가 해롭다는 사실을 제시하면서, if/then/else와 do/while/until 과 같은 더 익숙한 구조로 대체하였다.
객체 지향 프로그래밍
제어 흐름의 간접적인 전환에 대해 규칙을 부과한다.
요한 달과 크리스텐 니가드에 의해 등장했고, 알골의 함수 호출 스택 프레임을 힙으로 옮기면 지역변수가 오랫동안 유지될 수 있음을 발견하였다.
이를 통해, 클래스, 메서드, 인스턴스 변수, 다형성 등이 등장하게 되었다.
함수형 프로그래밍
할당문에 대해 규칙을 부과한다.
심볼의 값이 변경되지 않는다는 불변성의 개념을 도입했다. 이는 함수형 언어에는 할당문이 전혀 없다는 것이며, 변수 값을 변경하는 방법을 제공하긴 하지만 굉장히 까다로운 조건하에만 가능하다.
생각할 거리
- 각 패러다임은 프로그래머에게서 권한을 박탈하며, 부정적인 의도를 가지는 일종의 추가적인 규칙을 부과한다.
- 즉, 무엇을 하면 안되는 지에 대한 이야기를 한다.
위 세 패러다임은 우리에게 가져갈 수 있는 것들을 각각 가져가고 있으며, 이로 인해 더이상 새로운 프로그래밍 패러다임은 나올 수 없을 것이다.
결론
세 가지 패러다임과 아키텍처의 세 가지 큰 관심사 ( 함수, 컴포넌트 분리, 데이터 관리 ) 가 어떻게 연관되는지에 주목하자.
4장 구조적 프로그래밍
증명
-
데이크스타라가 초기에 인식한 문제는 프로그래밍은 어렵고, 프로그래머는 프로그래밍을 잘하지 못한다는 사실이였다.
-
프로그램은 단순하더라도, 너무 많은 세부사항을 담고 있었고 예상 외의 방식으로 실패하곤 했다.
데이크스트라는 증명이라는 수학적 원리를 통해, 입증된 구조를 이용하고 그것을 다시 코드와 결합시켜 스스로 증명하는 방식을 만드려했다.
- 이 과정중 goto와 같은 문장이 방해가 되는 경우가 있었고, 분기와 반복으로 대체할 수 있음을 알게되었다.
모든 프로그램을 순차, 분기, 반복이라는 세 가지 구조만으로 표현할 수 있음이 증명되으며 이를 통해 프로그램에서도 정리에 대한 유클리드 계층 구조를 만들 수 있을 것이라 생각되었다.
해로운 성명서
- 데이크스트라가 발표한 "goto문의 해로움"은 프로그래밍 세계에 전쟁의 불을 붙였다.
- 하지만, 현대로 오면서 goto 문이 점점 제거되었고 마침내 데이크스트라가 승리하였음을 알 수 있었다.
현대의 우리 모두는 구조적 프로그래머이다. 제어흐름을 제약 없이 직접 전환할 수 있는 선택권 자체를 언어에서 제공하지 않기 때문이다.
기능적 분해
구조적 프로그래밍을 통해 모듈을 증명 가능한 더 작은 단위로 분해 할 수 있게 되었고, 모듈을 기능적으로 분해할 수 있게 되었다.
즉, 대규모 시스템을 모듈과 컴포넌트로 나눌 수 있고, 더 나아가 모듈과 컴포넌트는 입증할 수 있는 아주 작은 기능들로 세분화할 수 있다.
엄밀한 증명은 없었다.
프로그램 관점에서 엄밀한 증명은 끝내 이루어지지 않았지만, 과학적 방법이 있다.
과학이 구출하다.
과학은 서술된 내용이 사실임을 증명하는 방싱이 아니라, 서술이 틀렸음을 증명하는 방식으로 동작한다.
각고의 노력으로도 반례를 들 수 없다면, 그것을 참이라고 본다.
테스트
프로그램이 잘못되었음을 테스트를 통해 증명할 수 있지만, 프로그램이 맞다고 증명할 수는 없다.
테스트에 충분한 노력을 들였다면, 테스트가 보장할 수 있는 것은 프로그램이 목표에 부합할 만큼은 충분히 참이라고 여길 수 있게 해주는 것이 전부다.
거짓임을 증명하려는 테스트가 실패한다면, 이 기능들은 목표에 부합할 만큼은 충분히 참이라고 여기게된다.
결론
-
구조적 프로그래밍이 오늘날까지 가치 있는 이유는 반증 가능한 단위를 만들어 낼 수 있는 능력 때문이다.
- 또한, 아키텍쳐 관점에서는 기능적 분해를 최고의 실천법 중 하나로 여기는 이유이기도하다.
-
소프트웨어 아키텍트는 모듈, 컴포넌트, 서비스가 쉽게 반증 가능하도록 만들기 위해 분주히 노력해야한다.
5장 객체 지향 프로그래밍
캡슐화
많은 OO 언어에서는 캡슐화를 강제하지 않는다.
- c언어에서는 헤더와 구현체를 분리하는 방식으로 캡슐화를 지원했다.
- cpp에서는 클래스의 멤버 변수를 해당 클래스의 헤더 파일에 선언해야했고, 이로 인해 캡슐화를 지키지 못했다.
- Java, C#은 헤더와 구현체를 분리하는 방식을 아예 버렸고, 이로 인해 선언과 정의를 구별하는 것이 어려워졌다.
상속
상속만큼은 OO가 확실히 제공한다. 하지만, 상속이란 단순히 어떠한 변수와 함수를 하나의 유효 범위로 묶어서 재정의하는 일일 뿐이다.
- OO가 있기 전에도, C 프로그래머는 언어의 도움없이 구현 가능했다.
- 멤버 변수의 순서를 그대로 유지하는 방법으로 사용하였다.
- 하지만, 상속만큼 편리한 방법은 아니였다.
- OO에서는 암묵적인 업캐스팅을 통해서, 상속을 구현하였다.
다형성
OO는 제어 흐름을 간접적으로 전환하는 규칙을 부과한다고 결론 지을 수 있다.
- C언어에서는 함수를 가리키는 포인터를 응용한 다형성을 사용한다.
- 함수포인터는 위험하고 초기화하는 관례를 지켜야한다.
- 하지만, 이러한 관례는 망각되기 싶고 이는 버그를 일으키고 찾아내기도 없애기도 힘들다.
- OO 언어는 이러한 관례를 없애고, 실수할 위험을 제거하였다.
다형성이 가진 힘
- 플러그인 아키텍쳐는 입출력 장치 독립성을 지원하기 위해 만들어졌지만, C언어에서는 함수 포인터를 다루어야하는 위험 때문에 거의 사용하지 않았다.
- OO의 등장은 이러한 위험을 제거했고, 플러그인 아키텍쳐를 어디서나 적용할 수 있게 되었다.
의존성 역전
- 옛날에는 고수준의 함수에서 저수준의 함수를 호출하는 방향으로 개발을 진행했다. 즉, 제어의 흐름을 따르게 된다.
- 하지만 다형성이 여기에 끼어들게 된다면, 인터페이스와 구현체 사이의 의존성이 반대가된다. 즉, 의존성 역전이 일어난다.
- 호출을 하는 쪽이든, 받는 모듈이든 상관없이 소프트웨어 아키텍트는 소스 코드 의존성을 원하는 방향으로 설정할 수 있다.
- 이를 통해 분리된 컴포넌트 또는 배포 가능 단위로 컴파일할 수 있게 될 것이며, 의존성 역시 소스 코드 사이의 의존성과 같아질 것이다.
- 배포 독립성과, 개발 독립성을 이루어 낼 수 있다.
결론
- OO란 다형성을 이용하여 전체 시스템의 모든 소스 코드 의존성에 대한 절대적인 제어 권한을 획득할 수 있는 능력이다.
- OO를 통해 아키텍트는 플러그인 아키텍처를 구성할 수 있고, 고수준의 정책을 포함하는 모듈은 저수준의 세부사항을 포함하는 모듈에 대해 독립성을 보장할 수 있다.
- 저수준의 세부사항은 중요도가 낮은 플러그인으로 만들 수 있으며, 배포도 독립적으로 가능해진다.
6장 함수형 프로그래밍
정수를 제곱하기
- 함수형 언어에는 가변 변수가 전혀 없기에, 변수는 변경되지 않는다.
(println ;___ 출력한다
(take 25;___ 처음부터 25까지
(map (fn [x] (* x x))) ;__ 제곱을
(range)))) ;___ 정수의
불변성과 아키텍처
- Race condition, deadlock, concurrent update 모두 가변 변수로 인해 발생한다.
- 다시말해, 동시성 어플리케이션에서 발생하는 모든 문제는 가변 변수가 없다면 발생하지 않는다.
- 불변성은 많은 데이터를 담을 공간을 필요로하기 때문에, 어느 정도 타협이 필요하다.
가변성의 분리
- 가장 중요한 타협은 불변 컴포넌트와 가변 컴포넌트를 분리하는 일이다.
- 불변 컴포넌트는 어떤 가변변수도 사용되지 않으며, 변수의 상태를 변경할 수 있는 다른 컴포넌트와 서로 통신한다.
이벤트 소싱
결론
- 구조적 프로그래밍은 제어흐름의 직접적인 전환에 부과되는 규율이다.
- 객체 지향 프로그래밍은 제어흐름의 간접적인 전환에 부과되는 규율이다.
- 함수형 프로그래밍은 변수 할당에 부과되는 규율이다.
- 소프트웨어는 순차, 분기, 반복, 참조로 구성된다. 그 이상 그 이하도 아니다.
3부 설계 원칙
- 아키텍쳐를 정의하는 원칙이 필요한데, 이 때 사용되는 원칙이 SOLID다.
- SOLID 원칙은 중간 수준의 소프트웨어가 아래와 같게 만들기 위함이다.
- 변경에 유연하다.
- 이해하기 쉽다.
- 많은 소프트웨어 시스템에 사용할 수 있는 컴포넌트의 기반이 된다.
7장 SRP
콘웨이 법칙에 따른 따름정리. 소프트웨어 시스템이 가질 수 있는 최적의 구조는 시스템을 만드는 조직의 사회적 구조에 커다란 영향을 받는다. 따라서 각 소프트웨어 모듈의 변경의 이유는 단 하나여야한다.
징후1: 우발적 중복
-
급여 어플리케이션에서 Employee 클래스를 생각해보자.
- 이 클래스는 calculatePay(), reportHours(), save() 를 가진다.
- 이 클래스는 SRP를 위반한다. 각 메서드가 서로 매우 다른 세 명의 액터를 책임지기 때문.
- calculatePay()
- reportHours()
- save()
-
개발자가 위 세가지 메서드를 단일 클래스에 배치하여 세 액터가 서로 결합되어 버렸다.
- 예를 들어, calculatePay() 메서드와 reportHours() 메서드가 regularHour() 메서드를 공유한다고 생각해보자.
- 이 때, 회계팀의 이유로 인해 regularHour() 가 변경되면 reportHours() 가 영향을 받게된다.
-
이는 세가지 액터가 의존하는 코드가 너무 가까이 있기 때문이다.
징후 2: 병합
- 서로 다른 개발자가, Employee 클래스를 체크아웃하고 변경한다고 생각해보자.
- 이들의 변경사항은 결과적으로 서로 충돌하며 병합이 발생할 것이다.
- 병합이 발생하는 모든 경우를 해결할 수 없기에, 병합은 항상 위험이 뒤따른다.
- 이 문제를 해결하는 방법은 서로 다른 액터를 뒷받침하는 코드를 서로 분리하는 것이다.
해결책
- 모두가 메서드를 각기 다른 클래스로 이동 시키는 방식이다.
- 각 클래스를 세 개의 클래스로 분리하고, 간단한 데이터 구조만 공유하도록한다. 이를 통해 우연한 중복을 피할 수 있다.
- 이 해결책은 세 클래스를 인스턴스화하고 추적해야하는 단점이 있으며, 이를 빠져나오기 위해 퍼사드 패턴을 많이 사용한다.
- 어떤 개발자는 가장 중요한 업무 규칙을 데이터와 가깝게 배치하는 방식을 선호한다.
- 이 방식에는 중요 메서드를 기존 클래스에 유지하되, 그 클래스를 덜 중요한 다머지 메서들에 대한 퍼사드로 사용할 수도 있다.
- 어떤 사람은 위 해결책이 모든 클래스가 단 하나의 메서드를 가져야한다는 주장을하며 반대할 수 있다.
- 하지만 여러 메서드는 하나의 가족을 이루고, 메서드의 가족을 포함하는 각 클래스는 하나의 유효범위가된다. 해당 유효범위 바깥에서는 이 가족에게 감춰진 식구가 있는지를 전혀 알 수 없다.
결론
- 단일 책임 원칙은 클래스 수준의 원칙이다.
- 하지만, 컴포넌트 수준에서는 "공통 폐쇄 원칙"이 되며 아키텍쳐 수준에서는 아키텍쳐 경계의 생성을 책임지는 "변경의 축"이된다.
8장 OCP
기존 코드를 수정하는 방식보다, 새로운 코드를 추가하는 방식으로 시스템의 행위를 변경할 수 있도록 설계해야하만 소프트웨어 시스템을 변경할 수 있다는 것이 요지이다.
- 소프트웨어 객체의 행위는 확장할 수 있어야하지만, 그 때 변경을 하면 안된다는 것.
사고 실험
- FinancialDataMapper는 구현관계를 통해, FinancialDataGateway를 알고 있지만, FinancialDataGateway는 FinancialDataMapper에 대해 아무것도 알지 못한다.
- 화살표는 단방향이며, 변경으로부터 보호하려는 컴포넌트를 향하도록 그려진다.
-
컴포넌트 관계는 모두 단방향으로 이루어져 있다. ( A 컴포넌트의 변경으로부터 B를 보호하려면, A가 B를 의존해야한다. )
-
예제의 경우
- Presenter에서 발생한 변경으로 부터 Controller을 보호하고자한다.
- View에서 발생한 변경으로 부터 Presenter을 보호하고자한다
- Interactor는 어느것에서 발생한 변경으로부터 보호된다.
- Interactor가 업무 규칙 ( 고수준의 정책 ) 을 포함하기 때문이다.
- Interactor > Controller, Databse > Prensenter > View
-
아키텍트는 기능이 왜, 어떻게, 언제 발생하는지에 따라 기능을 분리하고, 분리한 기능을 컴포넌트의 계층구조로 조직화 한다.
- 이를 통해, 저수준의 컴포넌트에서 발생한 변경으로 부터 고수준 컴포넌트를 보호할 수 있다.
방향성 제어
- 위 예시에서 FinancialDataGateway 인터페이스는 FinancialReportGenerator 와 FinancialDataMapper 사이에 존재하는데, 이는 의존성을 역전시키기 위해서이다.
- 위 인터페이스가 없었다면, 의존성이 Interactor 컴포넌트와 Database 컴포넌트로 바로 향하게 된다.
정보 은닉
-
FinancialReportRequeser 인터페이스는 방향성 제어와는 다른 정보 은닉을 위해 존재한다.
- 이 인터페이스는 FinancialReportController가 Interactor 내부에 대해 너무 많이 알지 못하도록 막기 위해서 존재한다.
- 만약 인터페이스가 없었다면, Controller는 FinancialEntities에 대해 추이 종속성을 가지게된다.
- 추이 종속성 : A -> B, B -> C 와 같은 의존성을 가지면 A는 C를 의존하게되며 이를 추이 종속성이라한다.
- 추이 종속성을 가지게 되면, 소프트웨어 엔티티는 자신이 직접 사용하지 않는 요소에 절대로 의존하면 안된다는 원칙을 위반하게 된다.
-
다시 말해, Controller에서 발생한 변경으로 부터 Interactor를 보호하는 일의 우선순위가 가장 높지만, 반대로 Interactor에서 발생한 변경으로부터 Controller도 보호되기를 바란다.
- 이를 위해, Interactor 내부를 은닉한다.
결론
- OCP는 시스템의 아키텍쳐를 떠받치는 원동력 중 하나이다.
- 시스템을 확장하기 쉬운 동시에 변경으로 인해 시스템이 너무 많은 영향을 받지 않도록 하는데 있다.
- 이 목표를 달성하기 위해서는 시스템을 컴포넌트 단위로 분리하고, 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있는 형태의 의존성 계층구조가 만들어져야한다.
9장 LSP
상호 대체 가능한 구성요소를 이용해 소프트웨어 시스템을 만들 수 있으려면, 이들 구성요소는 반드시 서로 치환 가능해야 한다는 계약을 반드시 지켜야한다.
상속을 사용하도록 가이드하기
-
License에 PersonalLicense, BusinessLicense 라는 두가지 하위 타입이 존재한다.
-
이러한 방식으로 상속을 사용하면, License를 사용하는 Billing 클래스는 어떠한 구체 License 클래스를 사용하는지 알 수 없고 치환할 수 있다. 즉, LSP를 준수한다.ㅇ
정사각형 / 직사각형 문제
- 일반적인 상식으로 정사각형은 직사각형의 하위 집합이다.
- 하지만 상속을 사용했을 때, 정사각형이 직사각형을 상속하는 것은 적합하지 않다.
- 직사각형은 높이와 너비가 독립적으로 변경될 수 있지만, 정사각형은 그렇지 않기 때문이다.
- 이러한 LSP 위반을 막기 위한 방법은 직사각형이 실제로는 정사각형인지 검사하는 메커니즘을 추가하는 것이다.
- 하지만, 행위가 사용하는 타입에 의존하기 때문에 타입을 치환할 수 없게된다.
LSP와 아키텍쳐
- 초창기에는 상속을 사용하도록 가이드 되었다. 하지만 시간이 지나면서 인터페이스와 구현체에도 적용되는 광범위한 설계 원칙으로 변모하였다.
- 이는 언어만이 아닌 여러 경우에도 사용할 수 있으며, 잘 정의된 인터페이스와 그 인터페이스의 구현체끼리의 상호 치환 가능성에 기대는 사용자들이 존재하기 때문이다.
LSP 위배 사례
- REST API를 만들었다고 가정했을 때, 어떠한 업체의 대응을 위해 구현 내부에 if문을 추가했다고 가정해보자.
- 이 업체가 더 커져서 다른 업체가 인수하여 이름이 변경되었을 때, if문을 더 추가하거나 수정해야하나? -> 이는 곧 문제를 일으킨다.
- 아키텍트는 이러한 것으로부터 시스템을 격리해야한다. ( 데이터베이스등을 사용하는 방법으로.. )
결론
- LSP는 아키텍쳐 수준까지 확장할 수 있고, 확장해야하만 한다.
- 치환 가능성을 조금이라도 위배하면, 시스템 아키텍처가 오염되며 별도 메커니즘을 추가해야 할 수 있기 때문이다.
10장 ISP
소프트웨어 설계자는 사용하지 않은 것에 의존하지 않아야 한다.
- 여러 클래스가 각각 특정한 클래스의 메서드를 단 하나씩만 사용하는 경우를 생각해보자.
- 이 경우 특정 클래스의 메서드 하나만 변경되어도, 모든 클래스가 다시 컴파일되고 배포되어야한다.
- 이것을 각각 인터페이스로 분리하면, 모든 클래스가 다시 컴파일 및 배포될 필요가 없어진다.
ISP와 언어
ISP는 아키텍쳐가 아니라, 언어와 관련된 문제이다.
-
정적 타입 언어는 소스코드에 포함된 선언문으로 인해 소스 코드 의존성이 발생하고, 재컴파일 또는 재배포가 강제되는 상황이 발생한다.
-
동적 타입 언어는 런타임 추론을 통해 재컴파일과 재배포가 필요 없어진다.
- 그렇기에 동적 타입 언어가 정적 타입 언어보다 유연하며 결합도가 낮은 시스템을 만들 수 있다.
ISP와 아키텍쳐
- 필요 이상으로 많은 걸 포함하는 모듈에 의존하는 것은 해로운 일이다.
- 소스 코드 뿐 아니라 이러한 일은 아키텍쳐에도 발생한다.
결론
- 불필요한 짐을 실은 무언가에 의존하면 예상치도 못한 문제에 빠진다.
11장 DIP
고수준 정책의 코드는 저수준 세부사항을 구현하는 코드에 절대로 의존하면 안된다. 대신 세부사항이 정책에 의존해야 한다.
- 유연성이 극대화되었다는 것은 소스 코드 의존성이 추상에 의존하며, 구체에는 의존하지 않는 것이다.
- 정적 타입언어에서는 추상적인 선언만을 참조해야하며, 구체적인 대상에는 절대로 의존해서는 안된다.
- 하지만 위 정책은 현실성이 없고, 바로 변동성이 큰 구체적인 요소 즉 우리가 개발하는 자주 변경될 수밖에 없는 모듈에서라도 의존하지 않도록 피해야한다.
안정된 추상화
- 구체적인 구현체의 변경이 있더라도 대부분의 인터페이스에 영향이 없다.
- 인터페이스는 구현체보다 변동성이 낮다
- 인터페이스를 변경하지 않고 구현체를 변경하는 방식이 소프트웨어 설계의 기본이다.
- 즉, 안정된 소프트웨어 아키텍쳐란 변동성이 큰 구현체에 의존하는 일은 지양하고, 안정된 추상 인터페이스를 선호하는 아키텍쳐라는 뜻이다.
구체적인 코딩 실천법
- 변동성이 큰 구체 클래스를 참조하지 말라
- 변동성이 큰 구체 클래스로부터 파생하지 말라
- 구체 함수를 오버라이드 하지 말라
- 구체적이며 변동성이 크다면 절대로 그 이름을 언급하지 말라
팩토리
- 이러한 규칙을 준수하기 위해서는, 구체적인 클래스를 생성할 때 주의해야한다.
- 아키텍쳐 경계는 시스템을 컴포넌트로 분리하며, 추상 컴포넌트와 구체 컴포넌트로 분리한다.
- 추상 컴포넌트는 애플레이케이션의 모든 고수준 업무 규칙을 포함한다.
- 구체 컴포넌트는 업무 규칙을 다루기 위해 필요한 모든 세부사항을 포함한다.
- 소스 코드 의존성은 제어 흐름과는 반대로 역전되며, 이를 의존성 역전이라 부른다.
구체 컴포넌트
- 구체 컴포넌트에는 구체적인 의존성이 존재하여, DIP를 위배한다.
- 이는 일반적이며, DIP를 위배하는 클래스들을 적은 수의 구체 컴포넌트 내부로 모아 분리하는 방법을 사용할 수 있다.
결론
- 앞으로 DIP는 아키텍쳐 다이어그램에서 가장 눈에 드러나는 원칙이 될 것이다.
- 의존성은 추상적인 엔티티가 있는 쪽을 향해야하며, 이 규칙은 추후 의존성 규칙이라 부를 것이다.
12. 컴포넌트
컴포넌트는 배포 단위이다.
컴포넌트의 간략한 역사
- 소프트웨어 개발 초창기에는 메모리에서의 프로그램 위치와 레이아웃을 프로그래머가 직접 제어 했다.
- 프로그래머가 라이브러리 함수의 소스코드를 애플리케이션 코드에 직접 포함시켜 단일 프로그램으로 컴파일 했다.
- 애플리케이션과 라이브러리가 계속 커지면서, 단편화가 발생하여 이 문제를 해결할 필요가 있었다.
재배치성
- 해결책은 재배치가 가능한 바이너리 였다.
- 지능적인 로더를 사용해서 메모리에 재배치 할 수 있는 형태의 바이너리를 생성하도록 컴파일러를 수정하자는 것.
- 프로그램이 라이브러리 함수를 호출하면 라이브러리 함수 이름을 외부참조로 생성.
- 이렇게 링킹 로더가 탄생하였다.
링커
- 링킹 로더가 너무 느려, 로드와 링크를 분리.
- 링커는 링크가 완료된 재배치 코드를 만들었고, 이를 통해 로딩을 빠르게 할 수 있었다.
- 하지만, 전체 모듈을 컴파일 하는 시간은 계속 커지게 되었음.
- but, 무어의 법칙에 따라 디스크의 속도가 매우 빨라져 다수의 .jar 파일 또는 다수의 공유 라이브러리를 순식간에 서로 링크한 후, 링크가 끝난 프로그램을 사용할 수 있게 되었다.
결론
- 런타임에 플러그인 형태로 결합할 수 있는 동적 링크 파일이 이 책에서 말하는 소프트웨어 컴포넌트에 해당한다.
13. 컴포넌트 응집도
이 장에서 컴포넌트 응집도와 관련된 세가지 원칙을 논의한다.
- REP : 재사용/릴리스 등가 원칙
- CCP : 공통 폐쇄 원칙
- CRP : 공통 재사용 원칙
REP : 재사용/릴리스 등가 원칙 (Reuse/Release Equivalence Principle)
재사용의 단위는 릴리스 단위와 같다.
CCP : 공통 폐쇄 원칙 (Common Closure Principle)
동일한 이유로 동일한 시점에 변경되는 클래스를 같은 컴포넌트로 묶어라. 서로 다른 시점에 다른 이유로 변경되는 클래슨느 다른 컴포넌트로 분리하라.
- SRP 에서 단일 클래스는 변경의 이유가 여러개 있어서는 안된다고 말하듯, 공통 폐쇄 원칙(CCP)에서도 마찬가지로 단일 컴포넌트는 변경의이유가 여러개 있어서는 안된다고 말한다.
- 애플리케이션에서 코드가 반드시 변경되어야 한다면, 이러한 변경이 여러 컴포넌트 도처에 분산되어 발생하기 보다, 단일 컴포넌트에서 발생하는 편이 낫다.
- CCP는 같은 이유로 변경될 가능성이 있는 클래스는 모두 한곳으로 묶을 것을 권한다.
- CCP에서는 동일한 유형의 변경에 대해 닫혀 있는 클래스들을 하나의 컴포넌트로 묶음으로써 OCP에서 얻은 교훈을 확대 적용한다.
SRP와의 유사성
동일한 시점에 동일한 이유로 변경되는 것들을 한데 묶어라. 서로 다른 시점에 다른 이유로 변경되는 것들은 서로 분리하라.
- CCP는 컴포넌트 수준의 SRP다.
- SRP에서 서로 다른 이유로 변경되는 메서드를 서로 다른 클래스로 분리하라고 말한다.
- CCP에서는 서로 다른 이유로 변경되는 클래스를 서로 다른 컴포넌트로 분리하라고 말한다.
CRP: 공통 재사용 원칙 (Common Reuse Principle)
컴포넌트 사용자들을 필요하지 않는것에 의존하게 강요하지 말라.
-
CRP에서는 같이 재사용 되는 경향이 있는 클래스와 모듈은 같은 컴포넌트에 포함해야 한다고 한다.
-
CRP는 각 컴포넌트에 어떤 클래스들을 포함시켜야 하는지 설명해 주며, 동일한 컴포넌트로 묶어서는 안되는 클래스가 무엇인지도 말해준다.
-
따라서, 강하게 결합되지 않는 클래스들을 동일한 컴포넌트에 위치시켜서는 안된다고 말한다.
ISP와 관계
필요하지 않는 것에 의존하지 말라.
- CRP는 인터페이스 분리원칙(ISP)의 포괄적인 버전이다.
- ISP는 사용하지 않는 메서드가 있는 클래스에 의존하지 말라고 한다.
- CRP는 사용하지 않는 클래스를 가진 컴포넌트에 의존하지 말라고 한다.
컴포넌트 응집도에 대한 균형 다이어그램
응집도 세원칙은 서로 상충된다는 것을 눈치챘을 것이다.
- REP와 CCP는 포함 원칙이다.
- CRP는 배제 원칙이다.
아키텍트는 여기에서 균형을 잘 잡아야한다.
- REP와 CRP에 중점을 두면 사소한 변경시 너무 많은 컴포넌트에 영향을 미친다.
- CCP와 REP에만 과도하게 집중하면 불필요한 릴리즈가 빈번해 진다.
결론
어느 클래스들을 묶어서 컴포넌트로 만들지 결정할 때, 재사용성과 개발 가능성이라는 상충하는 힘을 반드시 고려해야 한다
14. 컴포넌트 결합
ADP: 의존성 비순환 원칙
컴포넌트 의존성 그래프에 순환이 있어서는 안된다.
주단위 빌드
- 4일은 개발하고 금요일은 통합과 관련된 일을 한다.
- 프로젝트가 커질수록 통합하는 일은 커지고 오래걸리고, 어렵고, 팀끼리 빠른 피드백이 어렵게 된다.
순환 의존성 제거하기
- 개발환경을 릴리즈 가능한 컴포넌트 단위로 분리하는 것
- 개발자는 자신만의 공간에서 컴포넌트를 지속 수정하고, 나머지 개발자는 릴리즈된 버전을 사용한다.
- 새 컴포넌트가 릴리즈 되면 다른 팀에서는 새 버전또는 이전버전을 적용할지 결정할수 있다.
- 특정 컴포넌트가 변경되더라도 다른팀에 즉각 영향을 주지 않는다.
순환이 컴포넌트 의존성 그래프에 미치는 영향
- 순환은 즉각적 문제를 일으킨다.
- 어떤순서로 빌드해야 올바를지 파악하기 힘들다. -> 순환이 생기면 올바른 순서 자체가 없을수 있다.
순환끊기
컴포넌트 사이 순환을 끊고 의존성을 다시 DAG로 복구하기는 언제든 가능하다.
- 의존성 역전 원칙 (DIP)을 적용한다.
- 순환이 발생하는 컴포넌트가 모두 의존하는 새로운 컴포넌트를 만들어, 모두 의존하는 클래스를 새로운 컴포넌트로 이동시킨다.
흐트러짐
- 2번 해결책에서 요구사항 변경으로 컴포넌트 구조도 변경되었다.
- 실제 애플리케이션이 성장함에 따라 컴포넌트 의존성 구조는 서서히 흐트러지며 또 성장한다.
- 의존성 구조에 순환이 발생하는지 항상 관찰해야 한다.
- 순환이 발생하면 어떤식으로든 끊어야 한다.
하향식 설계
컴포넌트 구조는 하향식으로 설계될 수 없다.
- 컴포넌트는 시스템에서 가장 먼저 설계할 수 있는 대상이 아니다. 오히려 시스템이 성장하고 변경될 때 함께 진화한다.
SDP : 안정된 의존성 원칙
안정성의 방향으로(더 안정된 쪽에) 의존하라.
- 변동성을 지닌 컴포넌트는 언젠간 변경된다고 예상함.
- 따라서, 변경이 쉽지않은 컴포넌트가 변동성을 지닌 컴포넌트에 의존되면 안됨.
안정성
- 의존하는 컴포넌트가 많을 수록 안정적이다고 이야기하고, 반대인 경우 의존적이라고 말한다.
- 의존하는 컴포넌트가 많을수록 변경되면 안되는 이유가 많아진다.
안정성 지표
- 불안정성 : I = Fan-out / (Fan-in + Fan-out) , I =1이면, 최고로 불안정한 컴포넌트, I = 0이면, 최고로 안정한 컴포넌트다.
- 의존성 방향으로 갈수록 I지표 값이 감소해야 한다.
- Fan-in: 안으로 들어오는 의존성.
- 이 지표는 컴포넌트 내부의 클래스에 의존하는 컴포넌트 외부의 클래스 개수를 나타낸다.
- Fan-out : 바깥으로 나가는 의존성.
- 이 지표는 컴포넌트 외부의 클래스에 의존하는 컴포넌트 내부의 클래스 개수를 나타낸다.
모든 컴포넌트가 안정적이여야 하는 것은 아니다
- 모든 컴포넌트가 최고로 안정적이라면, 변경이 불가능하다
- 불안정한 컴포넌트와, 안정적인 컴포넌트가 함께 존재해야한다.
- 안정적인 컴포넌트가 갑자기 다른 컴포넌트를 의존하게 되면 SDP를 위반한다.
- 이를 위해 추상 컴포넌트를 하나 만들어 각 컴포넌트가 추상 컴포넌트를 의존하는 방식으로 해결할 수 있다.
추상 컴포넌트
SAP: 안정된 추상화 원칙
컴포넌트는 안정된 정도만큼만 추상화되어야 한다.
고수준 정책을 어디에 위치 시켜야 하는가?
- 고수준의 정책은 안정적인 컴포넌트에 위치해야한다.
- 하지만, 이렇게 되면 수정이 어려워진다.
- OCP로 해결할 수 있다.
- 어떤 클래스가 이 원칙을 준수하는가? 바로 추상 클래스이다.
안정된 추상화 원칙
- 안정된 컴포넌트는 추상 컴포넌트여야 하며, 이를 통해 안정성이 컴포넌트를 확장하는 일을 방해해서는 안 된다고 말한다.
- 불안정한 컴포넌트는 반드시 구체 컴포넌트여야한다고 말하는데, 불안정하므로 컴포넌트 내부의 구체적인 코드를 쉽게 변경할 수 있어야 하기 때문이다.
- SAP와 SDP를 결합하면 컴포넌트에 대한 DIP나 마찬가지가 된다.
- 실제로 SDP에서는 의존성이 반드시 안정성의 방향으로 향해야 한다고 말하고, SDP에서는 안정성이 결국 추상화를 의미한다고 말하기 때문이다.
- 따라서 의존성은 추상화의 방향으로 향하게 된다.
추상화 정도 측정하기
다음은 컴포넌트의 클래스 총 수 대비 인터페이스와 추상 클래스의 개수를 단순히 계산한 값이다.
- NC: 컴포넌트의 클래스 개수
- Na: 컴포넌트의 추상 클래스와 인터페이스의 개수
- A: 추상화 정도. A = Na / Nc
- A가 0이면 추상 클래스가 한개도 없고, 1이면 오로지 추상 클래스만 있음
주계열
-
최고로 안정적이며 추상화된 컴포넌트는 좌측 상단인 (0, 1)에 위치한다.
-
최고로 불안정하며 구체화된 컴포넌트는 우측 하단인 (1, 0)에 위치한다.
-
모든 컴포넌트가 이 두 지점에 위치하는 것은 아니다.
- 고통의 구역과 쓸모 없는 구역을 보며 알아보자
고통의 구역
- (0, 0) 주변 구역에 위치한 컴포넌트는 매우 안정적이며 구체적이다.
- 뻣뻣한 상태이기 때문에, 추상적이지 않아서 확장할 수 없고, 안정적이므로 변경하기 상당히 어렵다.
하지만 변동성이 없는 컴포넌트는 (0, 0) 구역에 위치하더라고 해롭지 않다. 변동될 가능성이 거의 없기 때문이다.
쓸모 없는 구역
- (1, 1) 주변의 컴포넌트는 최고로 추상적이다.
- 하지만, 누구도 그 컴포넌트에 의존하지 않는다.
이러한 컴포넌트는 쓸모가 없다. 따라서 이 구역은 쓸모없는 구역(Zone of Uselessness)이라고 부른다.
배제 구역 벗어나기
- 변동성이 큰 컴포넌트는 두 배제 구역으로부터 가능한 멀리 떨어뜨려야한다.
- 각 배제 구역으로부터 가장 멀리 떨어진 곳을 주계열이라 부르며, 주계열 위 또는 가깝게 위치해야 하며, 이렇게 위치하면 '너무 추상적'이지도 않고, 추상화 정도에 비해 '너무 불안정'하지도 않다.
주계열과의 거리
-
컴포넌트가 주계열 바로 위에, 또는 가까이 있는 것이 바람직하다면, 이 같은 이상적인 상태로부터 컴포넌트가 얼마나 멀리 떨어져 있는지 측정하는 지표를 만들어 볼 수 있다.
-
D : 거리. D = | A + I - 1|
- D가 0이면 컴포넌트가 주계열 바로 위에 위치한다는 뜻이며, 1이면 가장 멀리 있다는 뜻.
-
D거리 지표를 통해 주 계열에서 멀리 벗어난 컴포넌트의 원인을 파악하고 조사해 볼 가치가 있다.
결론
- 이 장에서 설명한 의존성 관리 지표는 설계의 의존성과 추상화 정도가 '훌륭한' 패턴이라고 생각하는 수준에 얼마나 잘 부합하는지를 측정한다.
- 저자는 경험을 통해 좋은 의존성도 있지만 좋지 않은 의존성도 있다는 것을 배웠으며, '훌륭한' 패턴은 이러한 경험을 반영한다.
15. 아키텍처란
아키텍처란 시스템을 구축했던 사람들이 만들어낸 시스템의 형태이다. 시스템을 컴포넌트로 분할하고, 배치하고, 의사소통하는 방법에 따라 정해진다.
그리고 그 형태는 아키텍처 안에 담긴 소프트웨어 시스템이 쉽게 개발, 배포, 운영, 유지보수되도록 만들어진다.
- 아키텍처의 주된 문적은 시스템의 생명주기를 지원하는 것.
- 좋은 아키텍처는 시스템을 쉽게 이해하고, 쉽게 개발하며, 쉽게 유지보수하고, 쉽게 배포하도록 해준다.
개발
- 5명으로 이루어진팀에서는 모놀리틱한 시스템을 개발할 수 있다.
- 7명씩 이루어진 5개의 팀에서는 시스템을 신뢰할 수 있고 안정된 인터페이스를 갖춘, 잘 설계된 컴포넌트 단위로 분리하지 않으면 개발이 진척되지 않는다.
- 다른 요소를 고려하지 않는다면, 이 시스템의 아키텍처는 각 팀 마다 하나씩 발전 될 가능성이 높다.
배포
소프트웨어 시스템이 사용될 수 있으려면 반드시 배포할 수 있어야 한다. 소프트웨어 아키텍처는 시스템을 한 번에 쉽게 배포할 수 있도록 만드는데 그목표를 두어야한다.
- 초기 개발 단계에서는 배포 전략을 거의 고려하지 않는다.
- 마이크로서비스 아키텍쳐를 사용하면 개발을 매우 쉽게 할 수 있겠지만, 배포 시기에 여러 마이크로서비스로 인해 문제가 발생할 수 있다.
- 만약 아키텍트가 초기에 배포를 고려했다면, 더 적은 서비스에서 컴포넌트와 프로세스 수준의 컴포넌트를 하이브리드 형태로 융합하며 좀 더 통합된 도구를 사용하여 상호 연결을 관리했을 것이다.
운영
운영을 방해하는 아키텍처가 개발, 배포, 유지보수를 방해하는 아키텍처보다는 비용이 덜 든다.
- 운영은 더 좋은 하드웨어로 어느 정도 타협 가능하다.
- 하지만, 좋은 소프트웨어 아키텍쳐는 시스템을 운영하는데 필요한 요구도 알려준다.
- 시스템 아키텍처는 유스케이스, 기능, 시스템의 필수 행위를 일급 엔티티로 격상시키고 이들 요소가 개발자에게 주요 목표로 인식되도록 해야한다.
유지보수
선택사항 열어 두기
소프트웨어를 부드럽게 유지하는 방법은 선택사항을 가능한 한 많이, 그리고 가능한 오랫동안 열어 두는 것이다.
- 열어 두어야 할 선택사항은 중요하지 않은 세부 사항을 의미한다.
- 모든 소프트웨어 시스템은 주요한 두 가지 구성요소인 정책(Polciy)과 세부사항(Detail)으로 분해할 수 있다.
- 정책 요소는 모든 업무 규칙(Business Rules)과 업무 절차(Procuedures)를 구체화 한다.
- 세부사항은 사람, 외부 시스템, 플그래머가 정책과 소통할 대 필요한 요소지만, 정책이 가진 행위에는 조금도 영향을 미치지 않는다.
- 이러한 세부 사항에는 입출력 장치, 데이터 베이스, 웹 시스템, 서버, 프레임워크, 통신 프로토콜 등이 있다.
결론
좋은 아키텍트는 세부사항을 정책으로부터 신중하게 가려내고, 정책이 세부사항과 결합되지 않도록 엄격하게 분리한다.
16. 독립성
좋은 아키텍쳐는 다음을 지원해야 한다.
- 시스템의 유스케이스
- 시스템의 운영
- 시스템의 개발
- 시스템의 배포
유스케이스
- 시스템 아키텍처는 시스템의 의도를 지원해야한다.
- 행위를 명확히 하며 시스템이 지닌 의도를 아키텍처 수준에서 알아볼 수 있게 만들어야한다.
운영
- 각 컴포넌트를 적절히 독립시켜 유지
- 각 컴포넌트간의 통신방식을 제한하지 않음
위 사항을 지킨다면, 요구사항이 바뀌더라도 스레드, 프로세스, 서비스로 구성된 기술 스펙트럼 사이를 전환하는 일이 훨씬 쉬어질 것이다.
개발
- 각 팀이 독립적으로 개발하기 편한 아키텍처가 만들어져야 한다.
- 잘 격리되어 독립적으로 개발가능한 컴포넌트 단위로 시스템을 분할해야하며, 그래야 컴포넌트를 독립적으로 작업할 수 있는 팀에 할당할 수 있다.
배포
- 좋은 아키텍처라면 시스템이 빌드된 후 즉각 배포할 수 있도록 지원해야한다.
- 각 컴포넌트를 적절하게 분리하고 격리시켜야한다
- 마스터 컴포넌트는 시스템 전체를 하나로 묶고, 각 컴포넌트들을 올바르게 구동하고 통합하고 관리해야한다.
선택사항 열어 놓기
- 아키텍처 원칙은 시스템을 제대로 격리된 컴포넌트 단위로 분할할 때 도움이 되며, 이를 통해 선택사항을 가능한 한 많이, 그리고 가능한한 오랫동안 열어 둘 수 있게 해준다.
- 선택사항을 열어둠으로써, 향후 시스템의 변경이 필요할 때 쉽게 변경이 가능하도록 해야한다.
계층 결합 분리
- 아키텍트는 단일 책임 원칙과 공통 폐쇄 원칙을 적용하여, 그 의도의 맥락에 따라서 다른 이유로 변경되는 것들은 분리하고, 동일한 이유로 변경되는 것들은 묶는다.
- UI, 유스케이스, DB는 서로 아무 관련이 없다. 아키텍트는 이를 시스템의 나머지 부분으로부터 분리하여 독립적으로 변경될 수 있게 해야한다.
- 이것이 각각의 서로 결합되지 않는 시스템을 수평적으로 분할하는 방법이다.
유스케이스 결합 분리
- 각 유스케이스는 UI 일부/앱 특화 업무규칙 일부/앱 독립 업무규칙 일부/DB 일부를 사용한다.
- 유스케이스는 시스템의 수평적인 계층을 가로지르도록 자른, 수직으로 좁다란 조각이다.
- ex) 주문 추가 유스케이스의 UI와 주문 삭제 유스케이스의 UI 를 분리.
수평은 각 컴포넌트, 수직은 유스케이스를 의미하는 것 같음.
결합 분리 모드
- 유스케이스를 위해 수행하는 결합 분리는 운영에 도움이된다.
- 운영 측면에서는, 결합을 분리할 때 적절한 모드를 선택해야한다.
- 우리는 때때로 컴포넌트를 서비스 수준까지도 분리해야하며, 이것을 선택할 수 있는 선택권을 열어두어야한다.
개발 독립성
- 컴포넌트가 완전히 분리되면 팀 사이 간섭은 줄어든다.
배포 독립성
- 유스케이스와 계층의 결합이 분리되면, 배포 측면에서 고도의 유연성이 생긴다.
- 결합을 제대로 분리했다면, 운영중에도 유스케이스를 교체하고 추가하는 것이 가능하다.
중복
- 코드가 진짜 중복되었다면, 그것은 제거해야한다.
- 하지만 코드가 각자의 경로로 발전한다면, 이것은 중복이 아니다.
- 화면이 유사한 구조를 가졌다고, 그것을 통합하게 되면 추후 다시 분리하느라 고생할 수 있음.
- 유스케이스를 수직으로 분리할 때, 자동반사적으로 중복을 제거하려는 유혹을 떨쳐내야한다
- 수평으로 분리하는 경우, 대부분의 경우 우발적인 중복이기 때문에 조심하자.
결합 분리 모드 ( 다시 )
계층과 유스케이스의 결합을 분리하는 방법 3가지가 있다.
- 소스코드 수준
- 소스 코드 모듈 사이의 의존성 제어
- 함수호출을 통한 통신
- 바이너리 코드 수준
- 배포 가능한 단위들 사이의 의존성 제어
- 함수호출을 통한 통신
- 어떠한 컴포넌트는 다른 프로세서에 상주하여, 프로세스간 통신을 통해 통신 가능
- 실행 단위 수준
- 서비스 또는 마이크로 서비스를 통한 방식
- 네트워크를 통한 통신
이러한 분리방식은 프로젝트 성숙도에 따라 최적인 모드가 달라질 수 있다.
- 좋은 아키텍처는 결합 분리 모드를 선택사항으로 남겨두어 추후 선택권을 줄 수 있도록 한다.
결론
- 시스템 결합 분리 모드는 시간이 지나면서 바뀌기 쉬우며, 뛰어난 아키텍트라면 이러한 변경을 예측하여 큰 무리 없이 반영 할 수 있도록 만들어야한다.
17. 경계: 선 긋기
아키텍처는 선을 긋는 기술이며, 이것을 경계라고 부른다.
- 경계는 한편에 있는 요소가 반대편에 있는 요소를 알지 못하도록 막는다.
- 어느 선은 매우 초기에 작성되는 경우도 있고, 매우 나중에 그어지는 경우도 있다.
- 선은 인적 자원을 최소화하는데 도움을 준다. -> 결합을 최소화함으로써.
두가지 슬픈 이야기
- 아키텍트가 너무 일찍 결정을 내려 개발 비용을 엄청 가중시킨 사례
FitNesse
-
DB 접근에 대한 것을 인터페이스로 두고, Stub을 만드는 방식을 통해 업무규칙과 데이터베이스 사이의 경계선을 그었음.
- 실제로, MySQL을 도입하기로 마음 먹었을 때 하루만에 MySQL을 사용하는 구현체를 추가할 수 있었음.
-
경계선을 긋는 행위는 결정을 늦추고 연기하는데 도움이 되었고, 시간을 절약시켜주었다.
어떻게 선을 그을까? 그리고 언제 그을까?
관련이 있는 것과 없는 것 사이에 선을 긋는다.
- 예시) 업무 규칙과 DB에 의존하지 않게 선을 그으면, 업무 규칙은 DB에 대해 하나도 알지 못하며 DB는 업무 규칙을 알 수 있다.
입력과 출력은?
입력과 출력은 중요하지 않다.
- 인터페이스는 모델에게 있어서 중요하지 않다. 중요한 것은 업무 규칙이다.
플러그인 아키텍처
데이터베이스와 GUI에 대한 결정을 하나로 합치면, 컴포넌트 추가와 관련된 일종의 패턴이 만들어진다.
이것이 "플러그인 아키텍처"이다.
- 소프트웨어 개발 기술의 역사는 플러그인을 손쉽게 생성하여, 확장 가능하며 유지소바 쉬운 아키텍처를 확립할 수 있게 만드는 방법에 대한 이야기이다.
- 나머지 컴포넌트는 업무 규칙으로부터 분리되어 있고, 독립적이다.
플러그인에 대한 논의
- 플러그인은 업무규칙의 변경에 따라 완전히 무력화 될 수 있지만, 그 반대는 이루어질 수 없는 비대칭적인 관계이다.
- 이를 통해, 입출력 플러그인의 변경이 업무 규칙에 영향을 끼칠 수 없는 방화벽을 만들 수 있다.
결론
- 경계선을 그리려면, 컴포넌트 단위로 분할해야한다.
- 의존성 역전 원칙과 안정된 추상화 원칙을 통해 저수준 세부사항에서 고수준의 추상화를 향하도록 배치하자.
18. 경계 해부학
경계는 다양한 방식으로 나타나며, 이 장에서 간단히 알아보자.
경계 횡단하기
- 경계 횡단은 경계 한쪽에 있는 기능에서 반대편을 호출하여 데이터를 전달하는 일
- 적절한 위치에서 횡단하는 방법은 소스코드 의존성 관리에 있음
- 의존하는 소스 코드를 다시 컴파일해서 변경하거나 배포해야할지 모르기 때문에, 경계는 방화벽의 역할
두려운 단일체
배포형 컴포넌트
- 자바의 jar 파일과 같은 동적 링크 라이브러리 등이 배포형 컴포넌트의 예시
- 배포 과정에서만 차이가 날 뿐, 배포 수준의 컴포넌트는 단일체와 동일하다
스레드
- 단일체와 배포형 컴포넌트 모두 사용 가능
- 모든 스레드가 단 하나의 컴포넌트에 포함될 수도 있고, 많은 컴포넌트에 걸쳐 분산될 수 있음
로컬 프로세스
-
로컬프로세스들은 동일한 프로세서 또는 하나의 멀티코어 시스템에 속한 여러 프로세서들에서 실행되지만, 각각이 독립된 주소 공간에서 실행된다.
-
메모리 공유가 되지 않으므로 통신은 운영체제 호출, 데이터 마샬링, 프로세스간 문맥교환 등 비싼 작업이다.
- 따라서 통신이 너무 빈번하게 이뤄지지 않도록 신중하게 제한해야 한다.
서비스
- 서비스는 프로세스로, 일반적으로 명령행 또는 그와 동등한 시스템 호출을 통해 구동된다.
- 서비스는 물리적으로 동일한 프로세서나 멀티코어에서 동작할 수도 있고, 아닐 수도 있다.
- 모든 통신이 네트워크를 통해 이루어진다.
- 서비스 경계를 지나는 통신은 함수 호출에 비해 매우 느리다. 따라서 빈번하게 통신하는 일을 피해야 한다.
- latency에 다른 문제를 고수준에서 처리할 수 있어야한다.
- 고수준 프로세스가 저수준 프로세스의 정보 ( URL 등.. )를 소스코드에 포함하면 안된다.
결론
- 단일체를 제외한 대다수의 시스템은 한 가지 이상의 경계 전략을 사용
- 서비스의 경계를 활용하는 시스템은 로컬 프로세스 경계도 포함하고 있을 수 있으며, 일련의 로컬 프로세스 퍼사드에 불과할 때가 많음
- 즉, 대체로 한 시스템 안에서도 경계와 지연을 중요하게 고려해야하는 경계가 혼합되어 있음
19. 정책과 수준
수준
-
수준은 "입력과 출력까지의 거리"이다.
- 즉, 입출력과 거리가 멀 수록 시스템에서 높은 수준의 컴포넌트를 의미하게 된다.
-
소스 코드는 그 수준에 따라 결합되어야하며, 데이터 흐름을 기준으로 결합되면 안된다. ( 아래가 잘못된 예시 )
fun encrypt() {
while (true) {
writeChar(translate(readChar()))
}
}
-
이 시스템을 개선하면 아래와 같이 개선할 수 있다.
interface CharReader {
fun read(): String
}
interface CharWriter {
fun write(char: String)
}
class ConsoleReader : CharReader {
override fun read(): String {
}
}
class ConsoleWriter : CharWriter {
override fun write(char: String) {
}
}
class Encrypter(
private val charReader: CharReder,
private val charWriter: CharWriter
) {
fun encrypt() {
while (true) {
val input = charReader.read()
val translated = translate(input)
charWriter.write(translated)
}
}
}
class Main {
fun main() {
val encrypter = Encrypter(ConsoleReader(), ConsoleWriter())
encrypter.encrypt()
}
}
결론
- 이 장에서 설명한 정책은 단일 책임 원칙, 개방 폐쇄 원칙, 공통 폐쇄 원칙, 의존성 역전 원칙, 안정된 의존성 원칙, 안정된 추상화 원칙을 모두 포함한다.
20. 업무 규칙
업무 규칙은 사업적으로 수익을 얻거나 비용을 줄일 수 있는 규칙 또는 절차이다.
엔티티
엔티티는 핵심 업무 데이터를 기반으로 동작하는 일련의 조그만 핵심 업무 규칙을 구체화한다.
아래의 Loan 엔티티는 세 가지의 핵심 업무 데이터를 포함하며, 세 가지 핵심 업무 규칙을 인터페이스로 제공한다.
Loan |
---|
- principle - rate - period |
+ makePayment() + applyInterest() + chargeLateFee() |
- 이 클래스는 업무의 대표자로서 독립적으로 존재한다.
- DB, UI, Framework등에 의해 오염되면 안된다.
유스케이스
자동화된 시스템이 사용되는 방법을 설명한다. 사용자가 제공해야하는 입력, 사용자에게 보여줄 출력, 해당 출력을 생성하기 위한 처리 단계를 기술한다.
-
유스케이스는 엔티티 내부의 핵심 업무 규칙을 어떻게, 그리고 언제 호출할지를 명시하는 규칙을 담는다.
- 유스케이스는 엔티티가 언제 춤을 출지를 제어한다.
- 유스케이스는 사용자에게 어떻게 보이는지를 설명하지 않는다.
-
유스케이스는 단일 어플리케이션을 위한 것이기 때문에 저수준이며, 엔티티는 다양한 애플리케이션에서 사용될 수 있도록 일반화된 것이므로 고수준이다.
- 유스케이스는 엔티티에 의존한다. 반면, 엔티티는 유스케이스에 의존하지 않는다.
요청 및 응답 모델
- 유스케이스는 데이터를 또 다른 컴포넌트와 주고 받는 방식에 대해서 전혀 눈치챌 수 없어야한다.
- 요청 및 응답 모델이 엔티티와 매우 유사하여 데이터 구조에 포함하려는 유혹을 가질 수도있지만, 시간이 지나면 매우 다른 노선으로 바뀌기 때문에 잘 분리하자.
결론
- 업무 규칙은 소프트웨어 시스템이 존재하는 이유다.
- 업무 규칙은 사업적으로 수익을 얻거나 비용을 줄일 수 있는 코드를 포함한다.
- UI, DB와 같은 저수준의 관심사에 의해 오염되면 안된다.
- 업무 규칙은 시스템에서 가장 독립적이며 가장 많이 재사용할 수 있는 코드여야 한다.
21. 소리치는 아키텍쳐
애플리케이션 아키텍처는 "어떠한 시스템이야"라고 소리쳐야한다. 자신이 "스프링/하이버네트야"라고 소리치면 안된다.
아키텍쳐의 테마
- 소프트웨어 아키텍처는 시스템의 유스케이스를 지원하는 구조
- 소프트웨어 애플리케이션의 아키텍처는 유스케이스에 대해 소리쳐야한다.
- 프레임워크가 중심이면 절대 안된다.
아키텍처의 목적
- 좋은 아키텍처는 유스케이스를 그 중심에 두기 때문에, 프레임워크와 도구 그리고 그 환경에 제약을 받지 않아야함.
- 좋은 소프트웨어 아키텍처는 프레임워크, 데이터베이스, 웹 서버, 그리고 여타 개발 환경 문제나 도구에 대해서는 결정을 미룰 수 있어야한다.
하지만 웹은?
- 웹은 입출력 장치일 뿐이며, 아키텍처가 아님
- 미루어야 할 결정사항 중 하나
프레임워크는 도구일 뿐, 삶의 방식이 아니다
프레임워크가 모든 것을 하게 하자는 우리가 취하고 싶은 태도가 아니다.
- 프레임워크가 아키텍처의 중심을 차지하는 일을 막을 수 있는 전략을 개발하자.
테스트하기 쉬운 아키텍처
- 프레임워크를 전혀 준비하지 않더라도 필요한 유스케이스 전부에 대해 단위테스트를 할 수 있어야한다.
- 테스트를 돌릴 때, 웹서버 혹은 데이터베이스가 필요한 상황이 오면 안됨.
- 유스케이스 객체가 엔티티 객체를 조작해야함.
- 프레임워크로인한 어려움을 겪지 않고, 반드시 있는 그대로 테스트 할 수 있어야한다.
결론
- 아키텍처는 시스템을 이야기해야함. 프레임워크에대해 이야기하면 안됨.
22. 클린 아키텍처
관심사의 분리가 중요하다.
헥사고날, DCI, BCE 아키텍처는 모두 아래의 특징을 지니도록 만든다.
- 프레임워크 독립성
- 테스트 용이성
- UI 독립성
- 데이터베이스 독립성
- 모든 외부 에이전시와의 독립성
의존성 규칙
소스코드 의존성은 반드시 안쪽으로, 고수준의 정책을 향해야 한다.
- 내부의 원에 속한 요소는 외부의 원에 속한 어떤 것도 알지 못한다.
- 내부의 원에 속한 요소는 외부의 요소의 어떤 것에 대해서도 언급하면 안된다.
엔티티
- 엔티티는 전사적인 핵심 업무 규칙을 캡슐화한다.
- 특정 애플리케이션에 무언가 변경이 필요하더라도 엔티티 계층에는 절대로 영향을 주어서는 안된다.
유스케이스
인터페이스 어댑터
- 일련의 어댑터들로 구성된다.
- 어댑터는 엔티티와 유스케이스에 가장 편리한 형식에서 영속성용으로 사용중인 임의의 프레임워크가 이용하기에 가장 편리한 형식으로 변환한다.
- 데이터를 외부 서비스와 같은 외부적인 형식에서 유스케이스나 엔티티에서 사용되는 내부적인 형식으로 변환하는 또 다른 어댑터가 필요하다.
프레임워크와 드라이버
- 모든 세부사항이 위치하는 곳이다.
- 우리는 이러한 것들을 모두 외부에 위치시켜서 피해를 최소화한다.
원은 네 개여야만 하나?
- 예시일뿐, 더 많아도 상관 X
- 하지만, 어떤 경우에도 의존성 규칙은 적용된다.
경계 횡단하기
- 제어흐름과 의존성의 방향이 명백히 반대인경우, 의존성 역전을 사용한다.
- 따라서, 유스케이스가 내부 원의 인터페이스를 호출하도록하고, 외부 원의 프레젠터가 그 인터페이스를 구현하도록 만든다.
경계를 횡단하는 데이터는 어떤 모습인가
- 간단한 데이터 구조로 이루어져있다.
- 경계를 가로질러 데이터를 전달할 때, 데이터는 항상 내부의 원에서 사용하기에 가장 편리한 형태여야한다.
전형적인 시나리오
- Controller에서 InputBoundary interface를 통해 Use Case Interactor에 전달한다.
- Use Case Interactor는 Entities 가 어떻게 춤출지를 제어하고, Data Access Interface를 통해 Entity가 사용할 데이터를 메모리에 로드한다.
- Entities가 완성되면, Use Case Interactor는 Entities로부터 데이터를 모아 OutputData를 구성하고 OutputBoundary 인터페이스를 통해 Presenter로 전달한다.
- Presenter는 OutputData를 ViewModel과 같이 화면에 출력할 수 있는 형식으로 재구성하고, 이것을 View에서 출력한다.
결론
- 간단한 규칙들을 준수하는 일은 어렵지 않으며, 고통거리를 덜어줄 것이다.
- 계층을 분리하고, 의존성 규칙을 준수한다면 테스트하기 쉬운 프로그램을 만들게 될 것이며, 그에 따른 이점을 얻을 수 있다.
23. 프레젠터와 험블 객체
험블 객체 패턴
- 테스트하기 어려운 행위와 테스트하기 쉬운 행위를 단위 테스트 작성자가 분리하기 쉽게 하는 방법으로 고안된 디자인 패턴.
- 행위들을 두 개의 모듈 또는 클래스로 나눈다. 이 중 하나가 험블이고, 가장 기본적인 본질은 남기고 테스트하기 어려운 행위를 모두 험블 객체로 옮긴다.
프레젠터와 뷰
- 뷰는 험블 객체이고 테스트하기 어렵다
- 뷰는 데이터를 GUI로 이동시키지만, 데이터를 직접 처리하지 않는다.
- 프레젠터는 테스트하기 쉬운 객체다. 프레젠터의 역할은 애플리케이션으로부터 데이터를 받아 화면에 표현할 수 있는 포맷으로 만든다.
- 뷰는 뷰 모델의 데이터를 화면으로 로드할뿐, 뷰가 맡은 역할은 없다. 뷰는 보잘 것 없다.
테스트와 아키텍처
- 테스트 용이성은 좋은 아키텍처가 지녀야 할 속성으로 오랫동안 알려져왔다.
- 테스트하기 쉬운 부분과 테스트하기 어려운 부분으로 분리하면 아키텍처 경계가 정의된다.
데이터베이스 게이트웨이
- 데이터베이스 게이트웨이 인터페이스를 유스케이스가 호출
- 구현체는 데이터베이스 계층에 위치하고, 험블 객체임.
- 하지만 인터페이스를 통해, 테스트 더블 혹은 스텁을 구현할 수 있음. 따라서, 유스케이스는 테스트하기 쉬움.
데이터 매퍼
- 데이터는 모두 private으로 선언되므로, 객체의 사용자는 데이터를 볼 수 없음.
- public 메서드만 볼 수 있고, 사용자 입장에서 객체는 오퍼레이션의 집합.
- 그렇기에, ORM은 단순히 데이터 매퍼임. 데이터베스 게이트웨이와 데이터베이스 사이에 존재하는 험블객체이다.
서비스 리스너
- 서비스 리스너에서도 험블 객체 패턴을 발견할 수 있음.
결론
- 아키텍처 경계에서 험블 객체 패턴을 사용하면 전체 시스템 테스트 용이성을 크게 높일 수 있다.
24. 부분적 경계
선행적인 설계가 YAGNI 원칙을 위배하기 때문에 애자일쪽에서는 탐탁치않게 여긴다. 하지만, 아키텍트라면 그것이 어쩌면 필요할 수 있다는 생각을 할 수 있고 그것을 부분적 경계를 통해 구현해볼 수 있다.
마지막 단계를 건너뛰기
- 독립적으로 컴파일하고 배포할 수 있는 컴포넌트를 만들기 위한 작업은 모두 수행한 후, 단일 컴포넌트에 그대로 모아두기.
일차원 경계
- 완벽한 형태의 아키텍처 경계는 양방향으로 격리된 상태를 유지해야 하므로 쌍방향 Boundary 인터페이스를 사용한다.
- 격리된 상태를 유지하려면 초기 설정할 때나 지속적으로 유지할 때도 비용이 많이든다.
- 전략 패턴을 사용하여 의존성 역전을 지킬 수 있음.
- 하지만, ServiceImpl이 직접 Client를 의존하는 것을 막을 방법은 없음.
퍼사드
-
퍼사드 클래스에는 모든 서비스 클래스를 메서드 형태로 정의하고, 서비스 호출이 발생하면 해당 서비스 클래스로 호출을 전달한다.
- 클라이언트는 이들 클래스에 직접 접근 할 수 없다.
-
하지만 추이 종속성을 가져, 서비스 클래스 중 하나에서 소스 코드가 변경되면, client도 무조건 재컴파일해야할 것이다.
결론
- 위 세가지 접근법은 각각 장단점을 가진다.
- 아키텍처 경계가 언제, 어디에 존재해야 할지, 그리고 그 경계를 완벽히 혹은 부분적을 구현할지를 정하는 것도 아키텍트의 역할이다.
25. 계층과 경계
-
이 장에서는 옴퍼스 사냥게임이라는 간단한 게임을 기반으로, 그것을 점점 확장해나간다.
-
의존성의 방향과, 각 경계의 흐름을 분리하고, 어떠한 추가사항이 발생했을 대 그것을 더 많은 흐름으로 분리하여 경계를 생성하는 방식에 대해서 설명한다.
결론
- 아키텍처 경계는 어디에나 존재한다.
- 아키텍처 경계가 언제 필요로한지, 신중하게 파악해야하며 오버 혹은 언더 엔지니어링이 되지 않도록 주의 깊게 살펴봐야한다.
- 조짐을 잘 살피고, 경계를 구현하는 비용과 무시할 때 비용을 가늠하여 변곡점에서 경계를 구현해야한다.
26장 메인(Main) 컴포넌트
메인 컴포넌트는 나머지 컴포넌트를 생성하고, 조정하며, 관리한다.
궁극적인 세부사항
- 메인 컴포넌트는 궁극적인 세부사항이며, 가장 낮은 수준의 정책이다.
- 메인 컴포넌트는 시스템의 초기 진입점이다.
- 의존성 주입 프레임워크에서 의존성이 주입되는 부분은 메인 컴포넌트여야한다.
결론
- 메인 컴포넌트를 애플리케이션의 플러그인으로 생각하자.
27장 ‘크고 작은 모든’ 서비스들
서비스를 사용하면 상호결합이 분리되는 것처럼 보이지만, 일부만 맞는 말이다. 배포 독립성을 지원하는 것처럼 보이지만, 이것도 일부만 맞는 말이다.
서비스 아키첵텨
-
서비스 그 자체로는 아키텍쳐를 정의하지 않는다.
-
아키텍처를 정의하는 요소는 바로 의존성 규칙을 따르며 아키텍처 경계를 넘나드는 함수 호출들이다.
-
서비스도 마찬가지다. 서비스는 프로세스나 플랫폼 경계를 가로지르는 함수 호출에 지나지 않는다.
서비스의 이점?
결합 분리의 오류
- 결합이 분리될거라는 생각은 네트워크 상의 공유 자원 때문에 결합될 가능성이 여전히 존재한다.
- 더욱이 공유하는 데이터 때문에 이들 서비스는 강력하게 결합되어 버린다.
- 서비스를 오가는 데이터의 필드가 추가되면, 모든 서비스에 추가되어야한다.
개발 및 배포 독립성의 오류
- 서비스는 확장 가능한 시스템을 구축하는 유일한 선택지가 아니다.
- 위의 결합 분리의 오류에 따르면, 항상 독립적으로 개발하고, 배포하며, 운영할 수 있는 것이 아니다.
- 어느 정도 결합되어 있다면 결합된 정도에 맞게 개발, 배포, 운영을 조정해야만 한다.
야옹이 문제
객체가 구출하다
- 컴포넌트 기반 아키텍쳐였다면, 어떻게 해결했을까?
- 서비스 로직 중 대다수가 이 객체 모델의 기반 클래스들 내부로 녹아들게 했다.
- 추상 기반 클래스를 템플릿 메서드나 전략 패턴등을 이용해서 오버라이드한다.
컴포넌트 기반 서비스
- 각 서비스 내부를 컴포넌트 기반 아키텍쳐와 같이, 파생 클래스를 만드는 방식으로 신규 기능을 추가할 수 있다.
횡단 관심사
아키텍쳐 경계가 서비스 사이에 있지 않다.
- 횡단 관심사를 처리하기 위해서는, 서비스 내부는 의존성 규칙도 준수하는 컴포넌트 아키텍처로 설계해야 한다.
- 아키텍처 경계를 정의하는 것은 서비스 내에 위치한 컴포넌트이다.
결론
-
서비스는 시스템의 확장성과 개발 가능성 측면에서 유용하지만, 아키텍쳐적으로 중요한 요소는 아니다.
-
시스템의 아키텍처는 시스템 내부에 그어진 경계와 경계를 넘나드는 의존성에 의해 정의된다.
-
서비스는 다수의 컴포넌트로 구성할 수 있다.
28장 테스트 경계
테스트는 시스템의 일부이며. 아키텍처에도 관여한다.
시스템 컴포넌트인 테스트
- 테스트는 가장 바깥 원이다.
- 테스트는 독립적으로 배포 가능하다.
- 테스트는 시스템 컴포넌트 중에서 가장 고립되어 있다.
테스트를 고려한 설계
- 시스템에 강력하게 의존되어 있는 테스트는 깨지기 쉽다.
테스트 API
테스트가 모든 업무 규칙을 검증하는데 사용할 수 있도록 특화된 API여야한다. 이 API는 인터랙터와 인터페이스 어댑터들의 상위 집합이 될 것이다.
구조적 결합
- 테스트가 사용 클래스나 메서드에 구조적으로 결합되어 있으면 깨지기 쉽다.
- 테스트 API의 역할은 애플리케이션의 구조를 테스트로부터 숨기는데 있다.
보안
- 테스트 API는 운영 시스템에는 배포하지 않도록, 분리되어야한다.
결론
- 테스트는 시스템의 일부다.
- 잘 설계하여 깨지지 않도록 해야한다.
29장 클린 임베디드 아키텍처
- 세부사항을 잘 숨겨라.
- 하드웨어, 프로세서, OS는 세부사항이다.
30장 데이터베이스는 세부사항이다
데이터베이스는 엔티티가 아니다. 데이터베이스는 세부사항이며, 일개 소프트웨어일 뿐이다.
관계형 데이터베이스
- 관계형 데이터베이스의 기술이 얼마나 뛰어나든, 이는 세부사항이다.
- 외부 원에 위치한 최하위 수준의 함수만 알아야한다.
데이터베이스 시스템은 왜 이렇게 널리 사용되는가?
- 디스크 때문이다.
- 디스크의 지연을 해결할 방법과 데이터를 정의 할 표준적인 방식이 필요했다.
디스크가 없다면 어떻게 될까?
- 데이터가 데이터베이스에 없고, 파일이나 램에 있더라도 우리는 다루기 편리한 형태로 그 구조를 변경할 것이다.
- 즉, 데이터베이스는 중요하지 않다.
세부사항
- 데이터베이스는 그저 메커니즘이며, 장기적으로 저장하는 공간에 지나지 않는다.
- 우리는 데이터가 어떤 형태인지는 절대로 신경쓰면 안된다. 디스크 자체가 존재한다는 사실까지도.
하지만 성능은?
- 성능은 업무 규칙과 분리할 수 있는 저수준의 관심사이다.
- 아키텍처와 아무런 관련이 없다.
개인적인 일화
- 마케팅적인 이유로 RDBMS를 사용하자는 마케팅 팀과 싸운 얘기 ( 졌음 ) 및 후회
결론
- 체계화된 데이터 구조와 데이터 모델은 아키텍처적으로 중요하다.
- 하지만, RDBMS와 SQL은 세부사항일 뿐이다.
- 데이터는 중요하지만, 데이터베이스는 중요하지 않다.
31. 웹은 세부사항이다
32. 프레임워크는 세부사항이다
프레임워크 제작자
- 프레임워크 제작자는 자신이 불편한 점을 해소하기 위해 개발하고 공개한다.
- 하지만, 프레임워크 제작자와 불편한점이 동일할 수 없다.
혼인 관계의 비대칭성
- 프레임워크 제작자는 프레임워크와 결합하기를 원한다.
- 프레임워크 제작자가 제어권을 지고 있기에, 프레임워크 제작자는 위험이 없다.
- 하지만, 사용자는 한 번 결합하면 변경이 어렵고 헌신해야하는 비대칭적인 혼인관계를 맺게 된다.
위험 요인
- 프레임워크는 의존성 규칙을 위반하는 경우가 있다.
- 제품이 성숙해지면, 프레임워크가 제공하는 기능에서 벗어나야할 때가 있다. 그런데, 결혼했다면 그게 어려워진다.
- 프레임워크는 당신에게 도움되지 않는 방향으로 진화할 수 있다.
- 새롭고 더 나은 프레임워크로 갈아타고 싶을 수 있다.
해결책
- 프레임워크와 결혼하지 말라!
- 업무 규칙에 플러그인할 수있는 컴포넌트에 프락시를 위치시켜라.
- 스프링 사용시
@Autowired
를 사용하지 말고, 메인에서 직접 조립해라.
이제 선언합니다
- 표준 라이브러리와 결혼하는 것은 피하기 힘들다. 하지만 선택적으로 사용하라.
결론
- 프레임워크와 바로 결혼하지 말라. 연애를 오랫동안 할 수 있는 방법이 있는지 확인해라.
33. 사례연구 : 비디오 판매
- 비디오 판매 사이트를 예시로 유스케이스와 컴포넌트 아키텍쳐를 설계하는 방법을 설명
유스케이스 분석
- 단일 책임 원칙에 따라, 액터가 시스템이 변경되어야 할 근원이다.
- 액터와 유스케이스를 분석하고, 비슷한 유스케이스는 추상 유스케이스로 설정
컴포넌트 아키텍쳐
- 뷰, 프레젠터, 인터랙터, 컨트롤러로 분리.
- 추상 유스케이스는 추상 클래스가 되어, 추상클래스로 부터 상속한 뷰와 프레젠터를 포함.
의존성 관리
- 의존성 규칙을 준수하고, 의존성이 단방향으로 흐르도록 관리
- 의존성이 올바른 방향을 가리키게하여, 저수준의 변경이 고수준의 정책에 영향을 끼치지 않도록하자.
34. 빠져있는 장
- 클린 아키텍쳐를 잠시 제쳐두고, 설계와 코드 조직화를 알아보자.
계층 기반 패키지
-
코드는 계층이라는 얇은 수평 조각으로 나뉘며, 각 계층은 유사한 종류의 것들을 묶는 도구로 사용된다.
- 엄격한 계층형 아키텍쳐는 반드시 바로 아래 계층에만 의존해야한다.
-
이 계층형 기반은 복잡함 없이 빠르게 무언가를 작동시킬 수 있지만, 추후에는 더 잘게 모듈화해야하는지에 대해 고민하게 될 것이다.
-
또한, 계층형 아키텍처는 업무 도메인에 대해 아무 것도 말해주지 않는다.
기능 기반 패키지
- 서로 연관된 기능, 도메인 개념 또는 Aggreate Root에 기반하여 수직의 얇은 조각으로 코드를 나누는 방식이다.
- 코드의 상위 수준 구조가 도메인에 대해 알려주고, 유스케이스를 변경해야 할 경우 모두 찾는 작업이 쉬워질 수 있다.
- 계층 기반 보다는 낫지만, 차선책이다.
포트와 어댑터
- 포트와 어댑터 패턴에서는 기술의 세부 구현을 분리하기 위해, 내부 ( 도메인 ) - 외부 ( 인프라 ) 로 구성함.
- 도메인 주도 설계 관점에서는 내부에 속하는 것은 반드시 유비쿼터스 도메인 언어 관점에서 기술하도록 조언한다.
- 우리는 주문에 대해 말하지, 주문 repository에 대해 말하지 않는다.
컴포넌트 기반 패키지
-
계층형 기반 패키지는 개발시 계층을 건너뛸 수 있다는 문제가 있음.
-
컴포넌트 기반 패키지는 큰 단위의 단일 컴포넌트와 관련된 모든 책임을 하나의 자바 패키지로 묶는데 주안점을 둔다.
-
컴포넌트는 "멋지고 깔끔한 인터페이스로 감싸진 연관된 기능들의 묶음"이다
구현 세부사항엔 문제가 있다
- 위 4가지 기반 패키지는 서로 다른 아키텍쳐 스타일로 여길 수 있지만, 접근 지시자등을 방만하게 사용할 경우 캡슐화등의 이점을 활용하지 못한다.
조직화 vs 캡슐화
다른 결합 분리 모드
- 프로그래밍 언어가 제공하는 방법 외에도 소스 코드 의존성을 분리하는 방법이 있음. ( 자바 OSIG.. )
- 소스코드 수준에서의 의존성 분리를 통해 도메인에서 다른 계층을 모르게하는 방법도 있음.
- 하지만 이렇게 나누면 복잡성이 증가하기도 한다.
- 이보다 간단한 방법으로, 내부 ( 도메인 ) - 외부 ( 인프라 ) 를 나누기도 하는데 이 접근법은 페리페리크 안티패턴이다.
결론 : 빠져 있는 조언
- 설계를 어떻게 해야만 원하는 코드 구조로 매핑할 수 있을지, 어떻게 조직화할지, 런타임과 컴파일타임 중 언제 결합 분리 모드를 적용할지 고민하라
- 가능하다면 선택사항을 열어두되, 실용주의적으로 행하라
- 팀 규모, 기술 수준, 해결책의 복잡성을 일정 및 예산과 동시에 고려하라