클린 아키텍쳐 1장~5장 정리
1장
- 설계 design와 아키텍쳐 사이에는 어떤 차이가 있을까?
아무런 차이가 없다.
아키텍처는 저수준의 세부사항과는 분리된 고수준의 무언가를 가리킬 때 흔히 사용된다. 설계는 저수준의 구조 또는 결정사항 등을 의미할 때가 많다.
- 설계의 목표는?
필요한 시스템을 만들고 유지보수하는 데 투입되는 인력을 최소화하는 데 있다.
2장
- 모든 소프트웨어 시스템은 이해관계자에게 서로 다른 두 가지 가치를 제공한다.
행위와 구조가 바로 그것이다.
대부분의 프로그래머는 행위(시스템이 동작하도록 하는 것)가 자신이 해야 할 일의 전부라고 생각한다.
소프트웨어라는 단어는 부드러운soft과 제품ware이라는 단어의 합성어이다. 제품이라는 단어는 상품product를 뜻하며, 부드러운soft이라는 단어는 행위를 쉽게 변경할 수 있다라는 점을 뜻한다.
업무 관리자에게 행위와 구조 중 무엇이 더 중요한 지에 대해 물어보면 대부분 행위라고 말하지만 추후 변경 요청에 변경 비용이 너무 커서 불가능하다고 말하면 화를 낼 가능성이 높다.
문제를 중요성과 긴급함을 기준으로 네 가지로 분류했을 때, 긴급하고 중요한 문제는 잘 없다. 행위는 긴급한 문제이고, 아키텍처는 중요한 문제이다.
시스템에 변경을 가하는 일이 불가능해지도록 용납했다면, 소프트웨어 개발팀이 스스로 옳다고 믿는 가치를 위해 충분히 투쟁하지 않았다는 뜻이다.
3장
- 패러다임
패러다임이란 프로그래밍을 하는 방법으로, 대체로 언어에 독립적이다. 패러다임은 어떤 프로그래밍 구조를 사용할지, 그리고 언제 이 구조를 사용해야 하는지를 결정한다.
이후 설명할 세 가지 외에 패러다임은 존재하지 않을 것이다.
- 구조적 프로그래밍
데잌스트라는 goto문은 프로그램 구조에 해롭다는 사실을 제시했다. 이러한 점프들을 if then else와 do while until과 같이 더 익숙한 구조로 대체했다.
구조적 프로그래밍은 제어흐름의 직접적인 전환에 대해 규칙을 부과한다.
- 객체 지향 프로그래밍
함수 호출 스택 프레임을 힙으로 옮기면, 함수 호출이 반환된 이후에도 함수에서 선언된 지역 변수가 오랫동안 유지될 수 있음을 발견했다. 바로 이러한 함수가 클래스의 생성자가 되었고, 지역 변수는 인스턴스 변수, 그리고 중첩 함수는 메서드가 되었다. 함수 포인터를 특정 규칙에 따라 사용하는 과정을 통해 필연적으로 다형성이 등장하게 되었다.
객체 지향 프로그래밍은 제어흐름의 간접적인 전환에 대해 규칙을 부관한다.
- 함수형 프로그래밍
람다 계산법에 직접적인 영향을 받아 만들어진 패러다임. 람다 계산법의 기초가 되는 개념은 불변성으로, 심볼의 값이 변경되지 않는다는 개념이다. 이는 함수형 언어에는 할당문이 전혀 없다는 뜻이기도 하다.
- 패러다임은 프로그래머에게서 권한을 박탈한다.
어느 패러다임도 새로운 권한을 부여하지 않는다. 각 패러다임은 부정적인 의도를 가지는 일종의 추가적인 규칙을 부과한다. 즉 패러다임은 무엇을 해야 할지를 말하기보다는 무엇을 해서는 안 되는지를 말해준다.
세 가지 패러다임 각각은 우리에게서 goto문, 함수 포인터, 할당문을 앗아간다. 더 가져갈 수 있는게 남아있을까? 아마 없을 것이다.
아키텍처 경계를 넘나들기 위한 매커니즘으로 다형성을 이용한다. 함수형 프로그래밍을 이용하여 데이터의 위치와 접근 방법에 대해 규칙을 부과한다. 모듈의 기반 알고리즘으로 구조적 프로그래밍을 사용한다.
패러다임과 아키텍처의 세 가지 큰 관심사(함수, 컴포넌트 분리, 데이터 관리)가 어떻게 서로 연관되는지 주목하자
4장
- 데익스트라는 goto 문장이 모듈을 더 작은 단위로 재귀적으로 분해하는 과정에 방해가 되는 경우가 있다는 사실을 발견했다.
반면 goto 문장을 사용하더라도 모듈을 분해할 때 문제가 되지 않는 경우도 있었다. 데잌스트라는 이런 goto문의 좋은 사용 방식은 if then else와 do while과 같은 분기와 반복이라는 단순한 제어 구조에 해당한다는 사실을 발견했다.
뵘가 야코피니는 프로그램을 순차, 분기, 반복이라는 세 가지 구조만으로 표현할 수 있다는 사실을 증명했다.
모듈을 증명 가능하게 하는 바로 그 제어 구조가 모든 프로그램을 만들 수 있는 제어 구조의 최소 집합과 동일하다는 사실이었다.
- 기능적 분해
거대한 문제 기술서를 받더라도 문제를 고수준의 기능들로 분해할 수 있다. 그리고 이들 각 기능은 다시 저수준의 함수들로 분해할 수 있고, 이러한 분해 과정을 끝없이 반복할 수 있다.
- 프로그래밍은 과학이다.
수학은 증명 가능한 서술이 참임을 입증하는 원리라고 볼 수 있다. 반면, 과학은 증명 가능한 서술이 거짓임을 입증하는 원리라고 볼 수 있다.
- 테스트
프로그램이 잘못되었음을 테스트를 통해 증명할 수는 있지만, 프로그램이 맞다고 증명할 수는 없다. 테스트에 충분한 노력을 들였다면 테스트가 보장할 수 있는 것은 프로그램이 목표에 부합할 만큼은 충분히 참이라고 여길 수 있게 해주는 것이 전부다.
- 가장 작은 기능에서 가장 큰 컴포넌트에 이르기까지 모든 수준에서 소프트웨어는 과학과 같고, 반증 가능성에 의해 주도된다.
소프투에어 아키텍트는 모듈, 컴포넌트, 서비스가 쉽게 반증 가능하도록(테스트하기 쉽도록) 만들기 위해 분주히 노력해야 한다.
5장
- 좋은 아키텍처를 만드는 일은 객체지향 설계 원칙을 이해하고 응용하는 데서 출발한다.
OO란 무엇인가? 누군가는 데이터와 함수의 조합이라고 답할 수 있다. 만족스러운 대답은 아닌데, o.f()가 왠지 f(o)와 다르다는 의미를 내포하기 때문이다. 이는 터무니없는 말이다. OO 훨씬 이전부터 프로그래머는 데이터 구조를 함수에 전달해 왔다.
또는 실제 세계를 모델링하는 새로운 방법이라고 답하지만, 얼버무리는 수준에 지나지 않는다. 도대체 실제 세계를 모델링한다라는 말이 무엇을 의미하며, 왜 우리는 그 방향을 추구해야 하는가? 정의가 너무 모호하다.
OO의 본질을 설명하기 위해 세 가지 주문에 기대는 부류도 있는데, 캡슐화 encapsulation, 상속 inheritance, 다형성 polymorphism이 바로 그 주문이다.
- 캡슐화
캡슐화를 통해 데이터와 함수가 응집력 있게 구성된 집단을 서로 구분 짓는 선을 그을 수 있다. 구분선 바깥에서 데이터는 은닉되고, 일부 함수만이 외부에 노출된다.
하지만 이런 개념들이 OO에만 국한된 것은 아니다. 사실 C언어에서는 완벽한 캡슐화가 가능하다. C++ (OO 언어)가 등장했고, C가 제공하던 완전한 캡슐화가 깨지게 되었다. 자바와 C#은 헤더와 구현체를 분리하는 방식을 모두 버렸고, 이로 인해 캡슐화는 더욱 심화게 훼손되었다.
캡슐화는 OO의 본질이 아니다.
- 상속
OO언어가 더 나은 캡슐화를 제공하지는 못했지만, 상속만큼은 OO언어가 확실히 제공했다.
하지만 상속이란 단순히 어떤 변수와 함수를 하나의 유효 범위로 묶어서 재정의하는 일에 불과하다.
OO언어 이전에도 상속과 비슷한 기법이 사용되었다고 말할 수 있다.
캡슐화에 대해서는 OO에 점수를 줄 수 없고, 상속에 대해서만 0.5점 정도를 부여할 수 있다.
- 다형성
c언어의 getchar() 함수는 STDIN에서 문자를 읽는다. 그러면 STDIN은 어떤 장치인가? putchar() 함수는 STDOUT으로 문자를 쓴다. 그런데 STDOUT은 또 어떤 장치인가? 이러한 함수는 다형적polymorphic이다. 즉 행위가 STDIN과 STDOUT의 타입에 의존한다.
함수를 가리키는 포인터를 응용한 것이 다형성이다.
extern struct FILE* STDIN;
int getchar() {
return STDIN→read();
}
C++에서는 클래스의 모든 가상 함수는 vtable이라는 테이블에 포인터를 가지고 있고, 모든 가상 함수 호출은 이 테이블을 거치게 된다. 파생 클래스의 생성자는 생성하려는 객체의 vtable을 단순히 파생 클래스 자신의 함수들로 덮어 쓸 뿐이다.
- 다형성이 가진 힘
새로운 입력 장치가 생겼을 때 프로그램에는 어떤 변화가 생길까? 아무런 변경도 필요하지 않다. 심지어 다시 컴파일 할 필요 조차 없다. 복사 프로그램의 소스 코드는 입출력 드라이버의 소스코드에 의존하지 않기 때문이다.
다시 말해 입출력 드라이버가 복사 프로그램의 플러그인이 된 것이다. 1950년대 후반에 장치에 의존적인dependent 수많은 프로그램을 만들고 나서야, 다른 장치에서도 동일하게 동작할 수 있도록 만드는 것이 우리가 진정 바라던 것임을 깨달았다.
플러그인 아키텍처plugin architecture는 이처럼 입출력 장치 독립성을 지원하기 위해 만들어졌고, 등장 이후 거의 모든 운영체제에서 구현되었다.
- 의존성 역전
다형성을 안전하고 편리하게 적용할 수 있는 메커니즘이 등장하기 전(함수를 가리키는 포인터를 사용하지 않는 방식) 소프트웨어는 다음과 같았다.
소스 코드 의존성의 방향은 반드시 제어흐름flow of control을 따르게 된다. main 함수가 고수준 함수를 호출하려면 고수준 함수가 포함된 모듈의 이름을 지정해야만 한다. C의 경우 #include가 지정자고, 자바에서는 import 구문이다.
제어흐름은 시스템의 행위에 따라 결정되며, 소스 코드 의존성은 제어흐름에 따라 결정된다.
하지만 다형성이 끼어들면 특별한 일이 일어난다.
HL1 모듈은 ML1 모듈의 F()함수를 호출한다. 소스 코드에서는 HL1 모듈은 인터페이스를 통해 F() 함수를 호출한다. 이 인터페이스는 런타임에는 존재하지 않는다. HL1은 단순히 ML1 모듈의 함수 F()를 호출할 뿐이다.
하지만 ML1과 I 인터페이스 사이의 소스 코드 의존성(상속 관계)이 제어흐름과는 반대인 점이 중요하다. 이는 의존성 역전 dependency inversion이라고 부른다.
OO 언어가 다형성을 안전하고 편리하게 제공한다는 사실은 소스 코드 의존성을 어디에서든 역전시킬 수 있다는 뜻이기도 하다.
OO언어로 개발된 시스템을 다루는 소프트웨어 아키텍트는 시스템의 소스 코드 의존성 전부에 대해 방향을 결정할 수 있는 절대적인 권한을 갖는다. 즉, 소스 코드 의존성이 제어흐름의 방향과 일치되도록 제한되지 않는다.
이것이 바로 OO가 제공하는 힘이다.
예를 들어 비지니스 룰이 데이터베이스와 UI에 의존하는 대신에, 의존성을 반대로 배치하여 데이터베이스와 UI가 업무 규칙에 의존하게 만들 수 있다.
즉, UI와 데이터베이스가 비즈니스 룰의 플러그인이 된다는 뜻이다. 결과적으로 비즈니스 룰, UI, 데이터베이스는 세 가지로 분리된 컴포넌트 또는 배포 가능한 단위로 컴파일 할 수 있고, 이 배포 단위들의 의존성 역시 소스 코드 사이의 의존성과 같다. 따라서 비즈니스 룰을 UI와 데이터베이스와는 독립적으로 배포할 수 있다.
다시 말해 특정 컴포넌트의 소스 코드가 변경되면, 해당 코드가 포함된 컴포넌트만 다시 배포하면 된다. 이것이 바로 배포 독립성이다. 이는 개발 독립성으로 이어진다.
- 결론
OO란 다형성을 이용하여 전체 시스템의 모든 소스 코드 의존성에 대한 절대적인 제어 권한을 획득할 수 있는 능력이다.
OO를 사용하면 아키텍트는 플러그인 아키텍처를 구성할 수 있고, 이를 통해 고수준의 정책을 포함하는 모듈은 저수준의 세부사항을 포함하는 모듈에 대해 독립성을 보장할 수 있다. 저수준의 세부사항은 중요도가 낮은 플러그인 모듈로 만들 수 있고, 고수준의 정책을 포함하는 모듈과는 독립적으로 개발하고 배포할 수 있다.