클린아키텍처를 스터디하면서 책의 내용, 토론한 내용, 개인 생각을 정리한 글입니다. 2부 3장, 4장, 5장, 6장
3장 패러다임 개요
- 패러다임 : 프로그래밍을 하는 방법
- 언어네는 독립적이며 어떤 프로그래밍 구조를 사용할 지, 언제 이 구조를 사용할 지 결정한다.
- 현재까지의 프로그래밍 패러다임에는 3가지 종류가 있다.
- 이 3가지 외의 패러다임은 현재까지는 존재하지 않는다
1. 구조적 프로그래밍
- 최초로 적용된 패러다임(하지만 최초로 만들어진 패러다임은 아니다).
- 1968년 네덜란드의 에르허츠 비버 데이크스트라 가 발견했다.
- 무분별한 점프문(goto문)은 구조에 해롭고 이걸 if / then / else / do / while /until과 같이 더 익숙한 구조로 대체했다
- 구조적 프로그래밍은 제어흐름의 직접적인 전환에 대해 규칙 을 부과한다
2. 객체지향 프로그래밍
- 사실 객체지향 프로그래밍은 구조적 프로그래밍보다 2년 앞선 1966년에 등장했따.
- 함수호출 스택 프레임을 힙(heap)으로 옮기면, 함수에서 선언된 지역변수가 오랫동안 유지될 수 있다는 걸 발견했다
- 이 함수가 클래스의 생성자가 되었고, 지역 변수는 인스턴스 변수, 중첩 함수는 메서드 가 되었다.
- 객체지향 프로그래밍은 제어흐름의 간접적인 전환에 대해규칙을 부과한다.
3. 함수형 프로그래밍
- 어떤 수학적 문제를 해결하는 과정에서 람다(lambda) 계산법을 발명했했는데, 이 결과에 직접적인 영향을 받아 만들어졌다.
- 람다 계산법의 기초가 되는 개념은 불변성(immutabillity)으로, 심볼(symbol)값이 변경되지 않는다는 개념
- 심볼 값은 계산의 주가 되는 변수 라고 할 수 있다.
- 이는 함수형 언어에는 할당문이 전혀 없다는 뜻이기도 하다.
- 사실 대다수의 함수형 언어가 변수 값을 변경할 수 있는 방법을 제공하기는 하지만, 굉장히 조건이 까다롭다.
- 함수형 프로그래밍은 할당문에 대해 규칙을 부과한다.
이 세가지 패러다임의 특징
- 각 패러다임은 프로그래머에게서 권한을 박탈한다.
- 어느 패러다임도 새로운 권한을 부여하지 않는다
- 패러다임은 부정적은 의도를 가지는 일종의 추가적인 규칙을 부과한다.
- 즉 패러다임은 무엇을 해야할지를 말하기 보다는 무엇을 해서는 안 되는지를 말해준다.
- 각 패러다임이 우리에게서 무엇인가를 빼앗는 사실을 인지해보자
- goto문 - 구조적 프로그래밍
- 함수 포인터 - 객체지향 프로그래밍
- 할당문 - 함수형 프로그래밍
- 등을 앗아간다.
- 세 가지 패러다임과 아키텍처의 세 가지 큰 관심사(함수, 컴포넌트 분리, 데이터관리)가 어떻게 서로 연관되는지에 주목하자.
4장 구조적 프로그래밍
다익스트라는 증명에 관한 연구를 진행하면서 goto문장이 모듈을 더 작은 단위로 재귀적으로 분해하는 과정에 방해가 되는 경우가 있다는 사실을 발견했다.
만약 모듈을 분해할 수 없다면, 합리적으로 증명할 때 필수적인 기법인 분할 정복 접근법을 사용할 수 없게 된다.
반면 goto문을 사용하더라도 모듈을 분해할 때 문제가 되지 않는 경우도 있었다. - 좋은 경우
- if / then / else / do / while 와 같은 분기와 반복이라는 단순한 제어 구조에는 좋은 경우이다
- 뵘과 야코피니가 모든 프로그램을 순차(sequence), 분기(selection), 반복 (iteration)이라는 세가지 구조만으로 표현할 수 있다는 사실을 증명했다.
- 모듈을 증명 가능하게 하는 바로 그 제어 구조가 모든 프로그램을 만들 수 있는 제어 구조의 최소 집합과 동일하다는 사실
현대 개발자는 모두 구조적 프로그래머이며이다.
- 제어흐름을 제약 없이 직접 전환할 수 있는 선택권 자체를 언어에서 제공하지 않기 때문이다.
- goto문
- 자바의 경우 이름을 붙인 break문이나 예외가 goto문과 유사하다
테스트
- 다익스트라 : "테스트는 버그가 있음을 보여줄 뿐, 버그가 없음을 보여줄 수는 없다."
- 테스트가 통과해도, 다른 버그가 있다.
- 소프트웨어는 수학보다는 과학과 같다.
- 최선을 다하더라도 올바르지 않음을 증명하는 데 있어 실패함으로써 올바름을 보여주기 때문이다
- 테스트가 잘못된 기능을 테스트 하는데 실패함으로써 잘못된 기능이 아닌, 올바른 기능이라는걸 알기 때문이다.
- 반증에 실패함으로써 올바르다는걸 증명이 가능하다
- 실패에 대한 테스트 케이스를 성공
- 즉 거짓임을 증명하려는 테스트가 실패한다면 , 이 기능들은 목표에 부합할 만큼은 참이라는것
- 최선을 다하더라도 올바르지 않음을 증명하는 데 있어 실패함으로써 올바름을 보여주기 때문이다
구조적 프로그래밍이 오늘날 까지 가치있는 이유는 프로그래밍에서 반증 가능한 단위를 만들어 낼 수 있는 능력이다.
- 단위테스트 라고 보여진다.
소프트웨어 아키텍트는 모듈, 컴포넌트, 서비스가 쉽게 반증 가능하도록 만들기 위해 분주히 노력해야 한다.
- 모듈, 컴포넌트, 서비스가 테스트하기 쉽도록 만들어야 한다.
5장 객체지향 프로그래밍
좋은 아키텍처를 만드는 일은 객체 지향 OO 설계 원칙을 이해하고 응용하는 데서 출발한다.
- OO 설계 원칙이란 무엇일까?
OO의 본질을 설명하기 위해 세 가지 주문에 기대할 수 있다.
- 캡슐화 (encapsulation)
- 상속 (inhreritance)
- 다형성 (polymorphism)
캡슐화?
캠슐화를 통해 데이터와 함수가 응집력 있께 구성된 집단을 서로 구분 짓는 선을 그을 수 있다.
구분선 바깥에서 데이터는 은닉되고, 일부 함수만이 외부에 노출된다.
이 개념들이 각 클래스의 private, public 멤버 함수로 표현된다.
- 하지만, 이 개념이 객체지향 언어들에서만 국한된게 아닌 C언어에서도 가능한 개념이란걸 알아두자 .
- 다음은 간단한 c코드이다.
// point.h . 포인트의 헤더, 선언부
struct Point;
struct Point* makePoint(double x, double y);
double distance (struct Point *p1, struct Point *p2);
// point.c . 포인트의 구현부
#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);
}
- point.h를 사용하는 측에서, Point의 멤버에 접근할 방법이 전혀 없다.
- 클라이언트가 캡슐화된 코드에 접근할 방법이 없다.
- 사용자는 makePoint() 함수와 distance() 함수를 호출할 수는 있지만 구조체의 구조와 함수가 어떻게 구현되었는지 알 수 없다.
- 헤더와 구현체를 분리함으로써 캡슐화 하고 있다고 할 수 있다.
그러나 자바와 C# 은 이 방식을 버림으로써 캡슐화를 훼손했다고 할 수 있다.
- 이들 언어에서는 클래스 선언과 정의를 구분하는게 아예 불가능하다.
- 선언과 정의를 동시에 하기 때문.
- 컴파일러 입장에서는 private을 사용해서 캡슐화하는ㄱ ㅔ맞다.
- 개발자 입장에서는 캡슐화가 맞지 않다.
- 왜? 캡슐화의 정의는 아예 못봐야 하는 것인데, 우리는 get, set 등으로 캡슐화를 위반한다.
** 이로 인해 이 책에서는 C언어의 캡슐화보다 자바 C#등의 언어를 캡슐화가 훼손되었다고 표현할 수 있다. **
캡슐화는 언제든지 깨질 수 있다. get, set 등으로 인해서.
- setter 사용을 지양하자.
상속?
OO 언어 (객체지향 언어?)가 더 나은 캡슐화는 제공하지는 못했지만 상속 만큼은 확실히 제공했따.
- 하지만 상속이란 단순히 어떤 변수와 함수를 하나의 유효 범위로 재정의 하는 일에 불과하다.
- C프로그래머도 언어의 도움 없이 상속을 구현할 수 있다.
간략히 요약하면 , 캡슐화에 대해서는 OO 언어에 점수를 줄 수 없고,
상속에 대해서는 0.5점 정도를 부여할 수 있다
다형성!?
OO 언어가 있끼 전에 다형성을 표현할 수 있는 언어가 있었다.
하지만 좀 더 불안전 하고, 편리하지 않다.
- 함수 포인터를 사용하기 때문
- 함수 포인터는 다형성을 따라할 수는 있다.
- 다만 널을 가리키거나, 의도한 기능과는 다른 함수를 가리킨다면 버그가 발생할 수 있으며 찾아내고 없애기가 힘들다.
하지만 OO 언어는 이러한 관례를 없애주어 버그를 없애는데 큰 도움이 된다.
다형성을 제공하지는 못했지만, 좀 더 안전하고 더욱 편리하게 사용할 수 있게 해준다.
그렇다면 다형성이 뭐가 그렇게 좋은가?
- 간단히 복사 프로그램 예제를 생각해보자.
- 모든 입출력 장치 드라이버가 있다고 하자.
- 콘솔, 파일, 프린트 등
- open, close, read, write, seek 라는 함수가 있다,
- 각 장치 코드마다 필요한 함수들을 정의하고 사용한다
- 다음 코드는 각 장치들을 인터페이스처럼 묶어서 작성한 것이다
struct FILE {
void (*open)(char* name, int mode);
void (*close)();
void (*read)();
void (*write)(char);
void(*seek)(long index, int mode);
};
- 하지만 복사 프로그램에서는 이 FILE 처럼 인터페이스가 아닌, 각 장치들의 구현체를 직접 구현해서 사용한다고 가정하자.
- 장치마다 코드들을 가지고 있고 구현했는데, 새로운 장치가 생겼다.
- 그러면 복사 프로그램 내에서 저 코드들을 새 장치마다 다 구현 해야 한다. 어지간히 번거로운 일이 아니다.
- 코드 교체에 많은 소모가 든다.
- 하지만 다형성을 이용하여 FILE 처럼 인터페이스를 구현해서 사용한다면?
- 복사 프로그램 내에서는 코드를 수정할 필요가 없다.
- 왜냐하면 복사 프로그램의 소스 코드는 입출력 드라이버의 소스 코드에 의존하지 않기 때문이다.
의존성 역전
전형적인 함수 호출 트리의 경우,
main 함수가 고수준 함수 호출
-> 고수준 함수는 중간 수준 함수 호출
-> 중간 수준 함수가 저서준 함수 호출
- 이런 호출 트리에서는 소스 코드 의존성의 방향은 반드시 제어흐름을 따르게 된다
- 제어흐름은 시스템의 행위에 따라 결정되며, 소스코드 의존성은 제어흐름에 따라 결정된다.
- 하지만 다형성이 끼어들면 다른일이 생긴다
- 악필은 양해 바랍니다..
- 소스코드 의존성은 진한 화살 표시, 제어 흐름은 점선 화살 표시이다.
- 위 호출 트리와 뭔가 다르다?
- 소스코드는 인터페이스를 의존하고 있고, 실제 제어 흐름은 인터페이스의 구현체인 ML1을 호출한다.
- 소스코드와 제어 흐름은 같은 방향이여야 하는데..??
즉 소스 코드 의존성(상속 관계)이 제어흐름과는 반대로 흐른다!
- 소스코드는 바뀌지 않고 언제든지 구현체를 바꿀 수 있다는 의미이다.
- 이걸 의존성 역전 (dependency inversion) 이라고 부른다.
- 개발자가 소스코드 사이에 인터페이스를 추가함으로써 프로그램의 제어 흐름을 역전 시켜 버린것이다.
OO 언어로 개발된 시스템을 다루는 소프트웨어 아키텍트는,
시스템의 소스 코드 의존성 전부에 대해 방향을 결정할 수 있는 절대적인 권한을 가진다.
- 우리는 지금껏 프로그램에게 모든 제어권을 뻇겨버리지 않았는가?
- 다형성을 사용하여 어마어마하게 큰 권한을 하나 가지고 온것이다
그럼 이 권한을 가지고 무엇을 할까?
예를 들어보자
- 업무 규칙이 데이터베이스와 사용자 인터페이스에 의존한다.
- 이렇게 되어버리면, 데이터베이스나 사용자 인터페이스가 바뀐다면 업무 규칙을 바꾸어야하는 불상사가 생긴다.
- 업무 규칙이 먼저 여야 하는것이 아닌가? 그러기 위해 기능을 개발하는 것인데.
- 이렇게 되어버리면, 데이터베이스나 사용자 인터페이스가 바뀐다면 업무 규칙을 바꾸어야하는 불상사가 생긴다.
하지만 이 다형성과 인터페이스로 시스템의 소스코드 의존성을 반대로 배치해 버리면?
- 데이터베이스와 UI가 업무 규칙에 의존하게 만들 수 있다.
- 이렇게 되어버린다면, 업무 규칙이 바뀜에 따라, 데이터베이스나 사용자 인터페이스가 바뀌게 된다.
- 즉 업무 규칙의 소스코드에서는 사용자 인터페이스나 데이터베이스를 호출하지 않고, 인터페이스를 호출한다 .
- 사용자 인터페이스나 데이터베이스가 바뀌어도 업무 규칙은 변하지 않는다.
- 배포 독립성(independent deployability) 의 이점도 가지게 된다.
- 사용자 인터페이스나 데이터베이스에서 발생한 변경사항은 업무 규칙에 영향을 미치지 않으므로 독립적으로 배포 가능.
- 특정 컴포넌트( 사용자인터페이스, 데이터베이스 등)에 소스코드가 변경되면 해당하는 컴포넌트만 다시 배포하면 된다.
결론
결론적으로, 객체지향에서의 OO란, 다형성을 이용하여 전체 시스템의 모든 소스 코드 의존성에 대한 절대적인 제어 권한을 획득할 수 있는 능력이다.
OO를 사용하면 아키텍트는 각 모듈에 대한 독립성을 보장할 수 있고, 각 모듈마다 독립적으로 개발하고 배포할 수 있다.
6장 함수형 프로그래밍
함수형 프로그래밍이라는 개념은 프로그래밍 그 자체보다 앞서 등장했다.
이 패러다임에서 핵심이 되는 기반은 람다(lambda) 계산법으로 알론조 처치가 1930년대에 발명했다.
함수형 언어는 가변 변수가 없다.
변수가 한번 초기화 되면 절대로 변하지 않는다.
- 이것이 람다의 계산법이다.
- 변하지 않는다는것은 불변성(immutabile) 인데 이것이 함수형 프로그래밍에서의 핵심이다.
불변성과 아키텍처
불변성, 변하지 않는다는 성질이다.
불변성과는 반대로 변하는 성질인 가변성이 있다.
왜 변수는 변할 수 있어서 변수라고 하는데 불변성이 중요하고, 가변성을 염려해야 하는가?
그것은 경합조건(race condition), 교착 상태(deadlock), 동시 업데이트(concurrent update) 문제 때문이다.
- 이 문제들은 모두 가변 변수로 인해 발생한다.
- 만약 어떠한 변수도 갱신되지 않는다면 경합 조건이나 동시 업데이트 문제가 일어나지 않는다.
- lock이 가변적이지 않다면, 교착상태도 발생하지 않는다.
다시 말해, 우리가 동시성 애플리케이션에서 마주치는 모든 문제들은 다수의 스레드와 프로세스로 인한 가변 변수로 인해 생기는 것이다.
- 이것이 동시성 (concurrency) 문제이다.
가변성 때문에 이러한 문제들이 생기는데,
가변성을 없애고 불변성으로 만들어 버리면 이 문제들은 사라지지만 불변성은 추가적인 자원과 프로세서의 속도가 필요하다.
- 불변성은 실현 가능하겠지만 일종의 타협이 필요하다.
- 완벽하게 불변성을 만족할 수는 없고 어느정도는 가변성이 필요하다 .
- 애플리케이션 내부의 서비스를 가변 컴포넌트와 불변 컴포넌트로 분리한다.
- 그리고 이 가변 컴포넌트의 가변 변수들을 보호하는 적절한 수단을 동원해 뒷받침 해야한다.
- 엄격한 제약
- 자바의 경우 atomicInteger, atomicLong 등
가능한 한 많은 처리를 불변 컴포넌트로 옮겨야 한다. 가변성을 최대한 줄여 동시성 문제를 해결하는 것이 중요하다.
함수형 프로그래밍의 베이스는 이러한 불변성을 이용하여 동시성 문제로부터 더욱 안전하게 프로그래밍 하는것이다.
결론
- 구조적 프로그래밍은 제어 흐름의 직접적인 전환에 부과되는 규율이다.
- 객체 지향 프로그래밍은 제어흐름의 간접적인 전환에 부과되는 규율이다.
- 함수형 프로그래밍은 변수 할당에 부과되는 규율이다 .
이들 패러다임 모두 프로그래머 에게서 하나씩 무엇가를 앗아간다.
각 패러다임은 우리가 코드를 작성하는 방식의 형태를 제한한다.
잘 생각해보면 각 프로그래밍 패러다임은 다른 패러다임에 속할 수 있다.
소프트웨어를 만들고 기능을 구현하는 방법은 무궁무진하다.
각 패러다임마다 제한적인 상황과 이득을 가져올수 있는 요소들은 다르다.
각기 다른 상황에 맞는 패러다임을 사용해서 최적의 소프트웨어를 만들어야 한다고 생각한다.
그것이 개발자의 덕목이니까.
마지막으로 한가지를 기억하자.
소프트웨어의 모든 핵심 요소들은 순차, 분기, 반복, 참조로 구성된다.
'스터디 > 클린 아키텍처- 로버트마틴' 카테고리의 다른 글
클린 아키텍처 1부. (0) | 2022.08.20 |
---|