2부 벽돌부터 시작하기: 프로그래밍 패러다임
3장. 패러다임 개요
소프트웨어 아키텍처는 코드로부터 시작한다. 따라서 아키텍처에 대한 논의도 코드가 최초로 작성된 시점부터, 우리가 코드를 통해 배운 내용을 살펴보는데서 출발
구조적 프로그래밍
- 무분별한 점프(goto 문장)는 프로그램 구조에 해롭다는 사실을 제시해 if/then/else와 do/whild/until과 같이 더 익숙한 구조로 대체
구조적 프로그래밍은 제어 흐름의 직접적인 전환에 대해 규칙을 부과한다.
객체 지향 프로그래밍
- 알골(ALGOL)언어의 함수 호출 스택 프레임(stack frame)을 힙(heap)으로 옮기면 함수 호출이 반환된 이후에도 함수에서 선언된 지역 변수가 오랫동안 유지될 수 있음을 발견했고,
- 바로 이러한 함수가 클래스의 생성자가 되었고 지역 변수는 인스턴스 변수, 중첩함수는 메서드가 되었다.
객체 지향 프로그래밍은 제허 흐름의 간접적인 전환에 대해 규칙을 부과한다.
함수형 프로그래밍
- 어떤 수학적 문제를 해결하는 과정에서 람다(lambda) 계산법을 발명했는데 함수형 프로그래밍은 이러한 연구결과에 직접적인 영향을 받아 만들어졌음
- 존 매카시가 만든 LISP언어의 근간이 되는 개념이 바로 람다 계산법인데 기초가 되는 개념은 불변성(immutability)으로 심볼(symbol)의 값이 변경되지 않는 다는 개념이다. 할당문이 전혀 없다는 뜻이기도 하다.
함수형 프로그래밍은 할당문에 대해 규칙을 부과한다.
생각할 거리
- 패러다임은 무엇을 해야 할지를 말하기 보다는 무엇을 해서는 안되는 지를 말해준다.
4장 구조적 프로그래밍
증명
- 데이크스트라가 초기에 인식한 문제는 모든 프로그램은 설령 단순할지라도 인간의 두뇌로 감당하기에는 너무 많은 세부사항을 담고 있었다.
- 데이크스트라는 연구를 진행하면서 goto 문장이 모듈을 더 작은 단위로 재귀적으로 분해하는 과정에 방해가 되는 경우가 있다는 사실을 발견했다.
- 대신
if/then/else
와 do/whild
과 같은 분기와 반복이라는 단순한 제어구조에 해당한다는 사실을 발견했다.
- 이러한 제어 구조는 순차 실행(sequential execution)과 결합했을 때 특별하다는 사실을 깨달았다.
- 뵘과 야코피니가 데이크스트라보다 2년 앞서 발견했는데 모든 프로그램을 순차, 분기, 반복이라는 세 가지 구조만으로 표현할 수 있다는 사실을 증명했다.
- 즉, 모듈을 증명가능하게 하는 바로 그 제어 구조가 모든 프로그램을 만들 수 있는 제어구조의 최소 집합과 동일하다는 사실이었고, 구조적 프로그램이은 이렇게 탄생했다.
기능적 분해
모듈을 고수준의 기능들로 분해하고 저수준의 함수들로 분해할 수 있다.
테스트
구조적 프로그래밍은 프로그램을 증명 가능한 세부 기능 집합으로 재귀적으로 분해할 것을 강요한다. 그리고나서 테스트를 통해 증명 가능한 세부 기능들이 거짓인지를 증명하려고 시도한다.
이처럼 거짓임을 증명하려는 테스트가 실패한다면, 이 기능들은 목표에 부합할 만큼은 충분히 참이라고 여기게 된다.
결론
프로그래밍에서 모듈, 컴포넌트, 서비스가 쉽게 반증 가능한 단위를 만들어 낼 수 있는 바로 이 능력 때문
5장 객체 지향 프로그래밍
캡슐화(encapsulation), 상속(inheritance), 다형성(polymorphism)
OO
-> Object-Oriented
캡슐화?
데이터와 함수를 쉽고 효과적으로 캡슐화 하는 방법을 제공하고 이를 통해 데이터와 함수가 응집력 있게 구성된 집단을 서로 구분 짓는 선을 그을 수 있다.
struct Point;
struct Point* makePoint(double x, double y);
double distance (struct Point *p1, struct Point *p2);
#include "point.h"
#include <stdlib.h>
#include <math.h>
struct Point {
double x,y;
};
struct Point* makepoint(double x, double y) {
struct Point* p = malloc(sizeof(struct Point));
p->x = x;
p->y = y;
return p;
}
double distance(struct Point* p1, struct Point* p2) {
double dx = p1->x - p2->x;
double dy = p1->y - p2->y;
return sqrt(dx*dx+dy*dy);
}’
- 다음 간단한 C 프로그램에서 point.h를 사용하는 측에서 struct Point멤버에 접근할 방법이 전혀 없다. 함수를 호출할수는 있지만 구조체의 데이터구조와 함수가 어떻게 구현되었는지에 대해 조금도 알지 못하며 이것이 완벽한 캡슐화이다.
- 하지만 OO언어는 헤더와 구현체를 분리하는 방식을 모두 버렸고 캡슐화가 심하게 훼손되었고 언어에
public
, private
, protected
키워드를 도입해 어느정도 보완하기는 했지만 컴파일러가 헤더파일에서 멤버 변수를 볼 수 있어야 했기 때문에 조치한 임시방편일 뿐이다.
상속?
- 상속이란 단순히 어떤 변수와 함수를 하나의 유효 범위로 묶어서 재정의하는 일에 불과하다.
- OO언어가 고안되기 훨씬 이전에도 상속과 비슷한 기법이 사용되었는데 지금은 그때보다 편리한 방식으로 제공한다고 볼 수 있다.
- 간략히 요약하자면 캡슐화에 대해서는 점수를 줄 수 없고, 상속에 대해서만 0.5점 정도 부여할 수 있다.
다형성?
- 함수를 가리키는 포인터를 응용한 것이 다형성이라는 점이다.
- OO언어는 좀 더 안전하고 편리하게 사용할 수 있게 해주는데 포인터를 직접 사용하는 관례를 없애주며 실수할 위험이 없다.
다형성이 가진 힘
- 복사 프로그램 예제에서 새로운 입출력 장치가 생긴다면 다형성을 고려했기 때문에 그 프로그램에서는 아무런 변경도 필요하지 않다.
- 플러그인 아키텍처는 이처럼 입출력 장치 독립성을 지원하기 위해 만들어졌고 OO언어의 등장으로 언제 어디서든 플러그인 아키텍처를 적용할 수 있게 되었다.
의존성 역전
- 전형적인 호출 트리의 경우 고수준 함수는 중간 수준의 함수를, 중간 수준 함수는 다시 저수준 함수를 호출한다. 즉 제어흐름 (flow of contorl)을 따르게 된다.
- 고수준 함수를 호출하려면 고수준 함수가 포함된 모듈의 이름을 지정해야한 한다. 소스 코드 의존성은 제어 흐름에 따라 결정됐는데 다형성이 끼어들면 특별한 일이 생긴다.
- 인터페이스는 런타임에는 존재하지 않는다. 또한
HL1
은 단순히 ML1
모듈의 함수 F()
를 호출할 뿐이다.
- 하지만 소스코드 의존성(상속 관계)이 제어흐름과는 반대인 점을 주목하자 이는 의존성 역전(dependency inversion)이라고 부르며 심오한 의미를 갖는다.
- OO언어가 다형성을 안전하고 편리하게 제공한다는 사실은 소스코드의 의존성을 어디에서든 역전 시킬 수 있다는 뜻이기도 하다.
- 이러한 접근법을 사용한다면, OO언어로 개발된 시스템을 다루는 소프트웨어 아키텍트는 시스템의 소스코드 의존성 전부에 대해 방향을 결정할 수 있는 절대적인 권한을 갖는다. 즉, 소스토드 의존성이 제어흐름의 방향과 일치되도록 제한되지 않는다.
- 소프트웨어 아키텍트는 소스코드 의존성을 원하는 방향으로 설정할 수 있고 OO가 제공하는 힘이고 지향하는 것이다.
- UI와 데이터 베이스가 업무 규칙의 플러그인이 된다는 뜻이다. 업무규칙의 소스코드에서는 UI나 데이터베이스를 호출하지 않는다.
- 결과적으로 업무 규칙, UI, 데이터베이스는 세가지로 분리된 컴포넌트 또는 배포 가능한 단위로 컴파일 할 수 있다. 즉, 업무 규칙을 UI와 데이터페이스와는 독립적으로 배포할 수 있다. 다시말해 특정 컴포넌트의 소스코드가 변경되면 해당 코드가 포함된 컴포넌트만 다시 배포하면되는데 바로 배포 독립성(Independent deployability)이다.
결론
OO란 다형성을 이용하여 전체 시스템의 모든 소스코드 의존성에 대한 절대적인 제어 권한을 획득할 수 있는 능력이다.
6장 함수형 프로그래밍
함수형 언어에서 변수는 변경되지 않는다.
불변성과 아키텍처
- 아키텍트는 왜 변수의 가변성을 염려하는가?
- -> 경합(race), 교착상태(deadlock), 동시 업데이트(concurrent update) 문제가 모두 가변 변수로 인해 발생하기 때문이다.
- 즉, 동시성 애플리케이션에서 마주치는 모든 문제, 즉 다수의 스레드와 프로세스를 사용하는 애플리케이션에서 마주치는 모든 문제는 가변 변수가 없다면 절대로 생기지 않는다.
- 불변성은 실현 가능하겠지만 일종의 타협을 해야 한다.
가변성의 분리
- 즉 순수함수형 컴포넌트가 아닌 하나 이상의 다른 컴포넌트와 서로 통신한다.
- 상태변경은 컴포넌트를 갖가지 동시성 문제에 노출하는 꼴이므로, 흔히 트랜잭션 메모리(transactional memory)와 같은 실천법을 사용하여 동시 업데이트와 경합 조건 문제로부터 가변변수를 보호한다.
- 애플리케이션을 제대로 구조화하려면 변수를 변경하는 컴포넌트와 변경하지 않는 컴포넌트를 분리해야 한다는 것이다. 그리고 이렇게 분리하려면 가변 변수들을 보호하는 적절한 수단을 동원해 뒷받침해야 한다.
이벤트 소싱
- 이제 프로세서가 초당 수십억개의 명령을 수행하고 램 용량은 수십억 바이트인 시대가 되었다. 더 많은 메모리를 확보할수록, 기계가 더 빨라질수록, 필요한 가변 상태는 적어진다.
- 하지만 시간이 지날수록 트랜잭션 수는 끝없이 증가하고, 계산에 필요한 컴퓨팅 자원은 걷잡을 수 없이 커진다.
- -> 애플리케이션의 수명주기 동안만 문제없이 동작할 정도의 저장공간과 처리 능력만 있으면 충분하고 이벤트 소싱(event sourcing)에 깔려있는 기본 발상이다. 상태가 아닌 트랜잭션을 저장하자는 전략이다.
- -> 저장공간은 급격하게 증가하기 때문
- -> 저장공간과 처리 능력이 충분하면 애플리케이션이 완전한 불변성을 갖고록 만들 수 있고 따라서 완전한 함수형으로 만들 수 있다.
결론
-
구조적 프로그래밍은 제어흐름의 직접적인 전환에 부과되는 규율이다.
-
객체 지향 프로그래밍은 제어 흐름의 간접적인 전환에 부과되는 규율이다.
-
함수형 프로그래밍은 변수 할당에 부과되는 규율이다.
즉 소프트웨어는 급격히 발전하는 기술이 아니라는 진실과 마주하게 된다.
컴퓨터 프로그램은 순차(sequence), 분기(selection), 반복(iteration), 참고(indirection)로 구성된다.
클린 아키텍처 소프트웨어 구조와 설계의 원칙
로버트 C. 마틴 저
http://www.yes24.com/Product/Goods/77283734