Posts [클린아키텍처] 1 ~ 6장
Post
Cancel

[클린아키텍처] 1 ~ 6장

clean-architecture-book

‘클린 아키텍처’ 기술 서적에 대해 학습했던 내용을 정리하기 위한 목적의 TIL 포스팅입니다.🙆‍♂️

1부 - 소개

1장 - 설계와 아키텍처란?

목표는?

  • 소프트웨어 아키텍처의 목표는 필요한 시스템을 만들고 유지보수하는데 투입되는 인력을 최소화하는데 있다.
  • 새로운 기능을 출시할 때 마다 비용이 증가한다면 나쁜 설계라 볼수 있다.

사례 연구 ~ 무엇이 잘못되었나?

  • 한 소프트웨어 개발 회사 몇년 동안 계속 개발 인력을 기하급수적으로 늘려가는데, 그에 비해 생산성 및 비용 그리고 매출이 현저하게 떨어진 사례가 있다.
  • 위 사례의 근본적 원인은 개발자들이 두 가지 거짓말에 속기 때문이다.
    • “코드는 나중에 정리하면 돼. 당장은 시장에 출시하는게 먼저야”
    • “지저분한 코드를 작성하면 단기간에는 빠르게 갈 수 있고, 장기적으로 볼때만 생산성이 낮아진다.”(더 중요한)
  • 여기서 핵심은 엉망으로 개발하면 깔끔하게 유지할 때보다 항상 더 느리다는 것이다.

결론

  • 소프트웨어 아키텍처를 심각하게 고려할 수 있으려면 좋은 소프트웨어 아키텍처가 무엇인지 이해해야 한다.
  • 비용은 최소화하고 생산성은 최대화할 수 있는 설계와 아키텍처를 가진 시스템을 만들려면, 이러한 결과로 이끌어 줄 시스템 아키텍처가 지닌 속성을 알고 있어야 한다.

2장 - 두 가지 가치에 대한 이야기

  • 소프트웨어 시스템은 이해관계자에게 서로 다른 두 가지 가치(행위와 구조)를 제공한다.

행위

  • 단순하게 동작하는 기능을 개발하는 것을 뜻한다.

아키텍처

  • ‘소프트웨어’ 라는 단어는 ‘부드러운(soft)’과 ‘제품(ware)’라는 단어의 합성어이다.
  • 이처럼 소프트웨어는 ‘부드러움을 지니도록’ 만들어졌다.
  • 소프트웨어를 만든 이유는 기계의 행위를 쉽게 변경할 수 있도록 하기 위해서다.
    • 만약 기계의 행위를 변경하는게 어렵게 만들었다면 하드웨어라 불렀을 것이다…
  • 즉, 핵심은 소프트웨어는 변경하기 쉬워야 한다.
    • 변경사항을 적용하는데 드느 어려움은 변경되는 범위(scope)에 비레해야 하며, 변경사항의 형태와는 관련 없어야 한다.
  • 이러한 변경하기 쉬운 소프트웨어를 만드려면 ‘시스템 아키텍처’ 가 중요하다.
    • 아키텍처가 특정 형태를 다른 형태보다 선호하면 할수록, 새로운 기능을 이 구조에 맞추는게 더 힘들어진다.
    • 따라서 아키텍처는 형태에 독립적이어야 하고, 그럴수록 더 실용적이다.

더 높은 가치

  • 필자는 위에서 언급한 ‘행위’와 ‘아키텍처’ 중에 ‘아키텍처’ 라고 주장하는데 그 이유는 다음과 같다.
    • 완벽하게 동작하지만 수정이 아예 불가능한 프로그램은 요구사항 변경시 동작하지 않을 것이고 결국 프로그램이 돌아가도록 만들수 없게 된다.
    • 동작은 하지만 변경이 쉬운 프로그램은 돌아가도록 만들면되고, 변경사항이 발생해도 유지보수 가능하다. 앞으로도 계속 유용할 것이다.
  • 수정이 현실적으로 불가능한 시스템은 존재하기 마련인데, 변경에 드는 비용이 변경으로 창출되는 수익을 초과하는 경우다..

아이젠하워 매트리스

image https://velog.io/@joosing/2-%EB%91%90-%EA%B0%80%EC%A7%80-%EA%B0%80%EC%B9%98%EC%97%90-%EB%8C%80%ED%95%9C-%EC%9D%B4%EC%95%BC%EA%B8%B0-Clean-Architecture-u45rjz6p

  • 긴급한 문제는 중요하지 않으며, 중요한 문제는 절대 긴급하지 않다는게 핵심이다.
  • 소프트웨어의 첫 번째 가치인 행위는 긴급하지만 매번 높은 중요도를 가지는 것은 아니다.
  • 소프트웨어의 두 번째 가치인 아키텍처는 중요하지만 즉각적인 긴급성을 필요로 하는 경우는 절대 없다.
  • 위 네 가지중 우선순위는 다음과 같다.
    • 1)긴급하고 중요한(아키텍처, 동작)
    • 2)긴급하지는 않지만 중요한(아키텍처)
    • 3)긴급하지만 중요치 않은(동작)
    • 4)긴급하지도 않고 중요치도 않은
  • 아키텍처, 즉 중요한 일은 가장 높은 두 순위를 차지하는 반면, 행위는 첫번째와 세번째에 위치한다는 점을 주목해야 한다.
  • 업무 관리자와 개발자가 흔히 저지르는 실수는 세번째 위치한 항목을 첫 번째로 격상시키는 것이다. 그러다 실패로 이어지게 된다..
    • 소프트웨어 개발자를 고용하는 이유는 바로 중요도가 높은 아키텍처를 업무 관리자에게 설득시키기 책임지기 위해서다.
    • 따라서 기능의 긴급성이 아닌 아키텍처의 중요성을 설득하는 일은 소프트웨어 개발팀이 마땅히 책임져야 한다.

아키텍처를 위해 투쟁하라

  • 효율적인 소프트웨어 개발팀은 투쟁에서 정면으로 맞서 싸운다.
    • 개발자는 소프트웨어를 안전하게 보호해야 할 책임이 있으며, 아키텍처를 위해 적극적으로 투쟁해야 한다.
  • 아키텍트는 기능을 개발하기 쉽고, 간편하게 수정 가능하며, 확장이 쉬운 아키텍처를 만들어야 한다.

2부 - 벽돌부터 시작하기: 프로그래밍 패러다임

3장 - 프로그래밍 패러다임 개요

구조적 프로그래밍

  • 간략히 요약하면 제어흐름의 직접적인 전환에 대해 규칙을 부과한다.

객체 지향 프로그래밍

  • 간략히 요약하면 제어흐름의 간접적인 전화에 대해 규칙을 부과한다.

함수형 프로그래밍

  • 간략히 요약하면 할당문에 대해 규칙을 부과한다.

생각할 거리

  • 각 패러다임은 프로그래머에게 권한을 박탈한다.
    • 즉, 패러다임은 무엇을 해야 할지 말하기보단 무엇을 해선 안되는지를 말해준다.</b
  • 세 가지 패러다임 각각은 우리에게서 goto문, 함수 포인터, 할당문을 앗아간다.
  • 우리에게 더 가져갈 수 있는 건 없을 것이다. 따라서 프로그래밍 패러다임은 아프오롣 딱 세가지 밖에 없을 것이다.
    • 중요한건 아니지만 이들 패러다임은 1958~1968년 총 10 년 동안 모두 만들어졌다. 이후 수십년이 지났지만 새롭게 등장한 패러다임은 없다..

결론

  • 세 가지 패러다임과 아키텍처의 세 가지 큰 관심사(함수, 컴포넌트 분리, 데이터 관리)가 어떻게 서로 연관되는지에 주목하자.

4장 - 구조적 프로그래밍

  • 1930년에 태어난 데이스크라는 1957년 결혼을 하는데, 그 당시 네덜란드에선 결혼 의식의 하나로 자신의 직업을 기입해야 했다.
  • 하지만 네덜란드에선 그의 직업인 ‘프로그래머’ 를 인정하지 않으려했고, 프로그래밍의 원리나 과학을 증명해내고자 했다.

증명

  • 데이스크라는 goto 문장이 모듈을 더 작은 단위로 재귀적으로 분해하는 과정에 방해가 되는 경우가 있다는 사실을 발견했다.
  • 반면 goto 문장을 사용하더라도 모듈을 분해할 때 문제 되지 않는 경우도 있었다.
  • 데이스크라는 이런 goto 문의 ‘좋은’ 사용 방식은 if/then/sle 와 do/while 과 같은 분기와 반복이라는 단순한 제어구조에 해당한다는 사실을 발견했다.
    • 모듈이 이러한 종류의 제어 구조만을 사용한다면 증명 가능한 단위로까지 모듈을 재귀적으로 세분화하는 것이 가능해 보였다.
  • 그는 이러한 제어 구조는 순차 실행과 결합했을 때 특별하다는 사실을 깨달았다.
  • 즉 모든 프로그램을 순차, 분기, 반복 이라는 세 가지 구조만으로 표현할 수 있다는 사실을 알게 되었다. (사실 2년 앞서 뵘과 야코피니가 발견했다)

해로운 성명서

  • 1968 년 데이스크라는 CACM 편집자에게 증명해낸 사실을 편지로 보냈고 같은해 3월호에 실렸다.
  • 하지만 비판하는자와 지지하는자들 사이에서 논란이 많았다.
  • 이러한 10년 이상의 논란을 데이스크라는 마침내 이겨냈다.
    • 컴퓨터 언어가 진화하면서 goto 문장은 계속 뒤편으로 밀려났고, 마침내 거의 사라졌다.
    • 대다수 현대적 언어는 goto 문장을 포함하지 않으며, 당연히 LISP 에선 첨부터 없었다.
  • 현재 우리 모두는 구조적 프로그래머이며, 여기엔 선택의 여지가 없다.
    • 제어 흐름을 제약 없이 직접 전화할 수 없는 선택권 자체를 언어에서 제공하지 않기 떄문이다.
    • 자바의 경우 break 문이나 예외가 goto 문과 유사하다볼 수 있찌만, 그래도 이들 구조는 제한적인 부분이 있다.
    • goto 키워드를 지원하는 언어에서도 goto 문의 목적지 범위를 현재 함수 안으로 한정시키는 편이다.

기능적 분해

  • 구조적 프로그래밍을 통해 모듈을 증명 가능한 더 작은 단위로 재귀적으로 분해할 수 있게 되었고, 이는 결국 모듈을 기능적으로 분해할 수 있음을 뜻했다.
  • 즉, 거대한 문제 기술서를 받더라도 문제를 고수준의 기능들로 분해할 수 있다.
  • 그리고 이들 각 기능은 다시 저수준의 함수들로 분해 가능하고, 이러한 분해 과정을 끝없이 반복할 수 있다.
  • 게다가 이렇게 분해한 기능들은 구조적 프로그래밍의 제한된 제어구조를 이용하여 표현할 수 있다.
  • 1970~80년대 몇몇 사람들이 구조적 분석이나 구조적 설계와 같은 기법을 더 디벨롭하여 널리알렸다.
  • 이들 기법을 사용하면 프로그래머는 대규모 시스템을 모듈과 컴포넌트로 나눌 수 있고, 더 나아가 모듈과 컴포넌트는 입증할 수 있는 아주 작은 기능들로 세분화할 수 있다.

엄밀한 증명은 없다.

  • 하지만 끝내 유클리드 계층 구조는 만들어지지 않았고 증명해내지 못했다.
  • 하지만 수학적인 증명만이 있는 것은 아니고 과학적 방법도 있었다.

과학이 구출하다.

  • 과학은 서술된 내용이 사실임을 증명하는 방식이 아니라 서술이 틀렸음을 증명하는 방식으로 동작한다.
  • 각고의 노력으로도 반례를 들 수 없는 서술이 있다면 목표에 부합할만큼은 참이라고 본다.
  • 결론적으로 수학은 증명 가능한 서술이 참임을 입증하는 원리라 볼 수 있는데, 과학은 증명 가능한 서술이 거짓임을 입증하는 원리라 볼 수 있다.

테스트

  • 데이크스트라는 “테스트는 버그가 있음을 보여줄 뿐, 버그가 없음을 보여줄 순 없다” 고 말한 적 있다.
    • 다시 말해 프로그램이 잘못되었음을 테스트를 통해 증명 가능하지만, 맞다고는 증명할 수 없다.
    • 테스트에 충분한 노력을 들였따면 테스트가 보장하는 것은 프로그램이 목표에 부합할만큼은 충분히 참이라고 여길 수 있게 해주는 것이 전부다.
  • 이 같은 사실은 소프트웨어는 수학적인 시도가 아니라 과학임을 알려준다. 최선을 다하더라도 올바르지 않음을 증명하는데 실패함으로써 올바름을 보여주기 때문이다.
  • 구조적 프로그래밍은 프로그램을 증명 가능한 세부 기능 집합으로 재귀적으로 분해할 것을 강요한다.
  • 그러고 나서 테스트를 통해 증명 가능한 세부 기능들이 거짓인지를 증명하려 시도한다.
  • 이처럼 거짓임을 증명하려는 테스트가 실패한다면, 이 기능들은 목표에 부합할 만큼은 충분히 참이라 여기게 된다.

결론

  • 구조적 프로그래밍이 오늘날까지 가치 있는 이유는 프로그래밍에서 반증 가능한 단위를 만들어 낼 수 있는 바로 이 능력 때문이다.
  • 또한 흔히 현대적 언어가 아무런 제약 없는 goto 문을 지원하지 않는 이유이기도 하다.
  • 뿐만 아니라 아키텍처 관점에서 기능적 분해를 최고의 실천법 중 하나로 여기는 이유기도 하다.
  • 가장 작은 기능에서부터 가장 큰 컴포넌트에 이르기까지 모든 수준에서 소프트웨어는 과학과 같고, 따라서 반증 가능성에 의해 주도된다.
  • 소프트웨어 아키텍트는 모듈, 컴포넌트, 서비스가 쉽게 반증 가능하도록(테스트하기 쉽도록) 만들기 위해 분주히 노력해야 한다.
  • 이를 위해 구조적 프로그래밍과 유사한 제한적인 규칙들을 받아들여 활용해야 한다.

5장 - 객체 지향 프로그래밍

  • 좋은 아키텍처를 만드는일은 객체 지향 OO(Object-Oriented) 설계 원칙을 이해하고 응용하는 데서 출발한다. 그렇다면 대체 OO 은 무엇일까?
  • OO 의 본질을 설명하기 위해 세 가지 주문에 기대는 부류도 있는데, 캡슐화, 상속, 다형성이 바로 그 주문이다.
  • 이들은 OO가 이 세 가지 개념을 적절하게 조합한 것이거나, 또는 OO 언어는 최소한 세 가지 요소를 반드시 지원해야 한다고 말한다.

캡슐화?

  • 캡슐화를 통해 데이터와 함수가 응집력 있게 구성된 집단을 서로 구분 짓는 선을 그을 수 있다.
  • 구분선 바깥에서 데이터는 은닉하고, 일부 함수만이 외부에 노출된다.
  • C언어과 같은 OO 가 아닌 언어에서도 충분히 완전한 캡슐화는 가능하다.
  • 다음 c 코드를 보자.

  • point.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 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를 사용하는 측에서 struct Point의 멤버에 접근할 방법이 전혀 없다.
  • 사용자는 makePoint() 함수와 distance() 함수를 호출할 수는 있지만, Point 구조체의 데이터 구조체가 어떻게 구현되어 있는지 알 수 없다. 이것이 완벽한 캡슐화이다.

  • 하지만 이후 C++ 이라는 형태로 OO가 등장했고, C가 제공하는 완전한 캡슐화는 깨지게 되었다.
  • C++ 에 public, private, protected 키워드를 도입함으로써 불완전한 캡슐화를 사실상 어느 정도 보완하기는 했다.
  • 하지만 이는 컴파일러가 헤더 파일에서 멤버변수를 볼 수 있어야 했기 때문에 조치한 임시방편일 뿐이다.
  • 자바와 C#은 헤더와 구현체를 분리하는 방식을 모두 버렸고, 이로 인해 캡슐화는 더욱 심하게 훼손되었다.
  • 이 때문에 OO가 강력한 캡슐화에 의존한다는 정의는 받아들이기 힘들다.
    • 실제로 많은 OO 언어가 캡슐화를 거의 강제하지 않는다.
    • OO 프로그래밍은 프로그래머가 충분히 올바르게 행동함으로써 캡슐화된 데이터를 우회해서 사용하지 않을거라는 믿음을 기반으로 한다.
    • 하지만 OO를 제공한다고 주창한 언어들이 실제로 C언어에서 누렸던 완벽한 캡슐화를 약화시켜 온 것은 틀림 없다.

상속?

  • OO 언어가 더 나은 캡슐화를 제공하진 못했으나, 상속만큼은 OO 언어가 확실히 제공했다.
  • 사실상 OO 언어가 있기 훨씬 이전에도 C프로그래머는 언어의 도움 없이 손수 이러한 방식으로 구현할 수 있었다.
  • OO 언어가 고안되기 훨씬 이전에도 상속과 비슷한 기법이 사용되었다고 볼 수 있는데, 이렇게 말하는데는 어폐가 있다.
    • 상속을 흉내내는 요령은 있었지만, 사실상 상속만큼 편리한 방식은 절대 아니기 때문이다.
    • 게다가 이 기법을 이용해서 다중 상속을 구현하기란 훨씬 더 어려운 일이었다.
  • OO 언어가 완전히 새로운 개념을 만들진 못했지만, 데이터 구조에 가면을 씌우는 일을 상당히 편리한 방식으로 제공했다고 볼 수는 있다.
  • 간략히 요약하면, 캡슐화에 대해선 OO에 점수를 줄 수 없고, 상속에 대해서만 0.5 점 정도를 부여할 수 있다.

다형성?

  • OO 언어가 있기 이전에 다형성을 표현할 수 있는 언어가 있었다. C로도 충분했다.
  • c 로 작성된 아래 복사 프로그램을 살펴보자.
1
2
3
4
5
6
7
#include <strdio.h>

void copy() {
	int c;
    while ((c=getchar()) != EOF)
    	putchar(c);
}
  • getchar() 함수는 STDIN에서 문자를 읽는다. 그러면 DTDIN은 어떤 장치인가?
  • putchar() 함수는 STDOUT으로 문자를 쓴다. 그런데 STDOUT은 또 어떤 장치인가?
  • 이러한 함수는 다형적이다. 즉 행위가 STDIN과 STDOUT의 타입에 의존한다.
  • STDIN과 STDOUT은 사실상 자바 형식의 인터페이스로, 자바에서는 각 장치별로 구현체가 있다.
  • C에서는 그렇다면 어떤 방식으로 문자를 읽는 장치 드라이버를 호출할 수 있는가?
    • 유닉스에서는 모든 입출력 장치 드라이버가 다섯 가지 표준 함수를 제공할 것을 요구한다.
    • 열기(open), 닫기(close), 읽기(read), 쓰기(write), 탐색(seek)이 바로 이 표준 함수들이다.
    • FILE 데이터 구조는 이들 다섯 함수를 가리키는 포인터들을 포함한다. 이 예제의 경우라면 다음과 같을 것이다.
1
2
3
4
5
6
7
struct FILE {
	void(*open)(char* name, int mode);
	void(*close)();
	int(*read)();
	void(*write)(char);
	void(*seek)(long index, int mode);
};

콘솔용 입출력 드라이버에서는 이들 함수를 아래와 같이 정의하며, FILE 데이터 구조를 함수에 대한 주소와 함께 로드할 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
 #include "file.h"

void open(char *name, int mode) {/*...*/ }
void close() {/*...*/ }
int read() {
	int c;
	/*...*/
	return c;
}
void write(char c) {/*...*/ }
void seek(long index, int mode} {/*...*/}

struct FILE console = { open, close, read, write, seek };

이제 STDIN을 FILE*로 선언하면, STDIN은 콘솔 데이터 구조를 가리키므로, getchar()는 아래와 같은 방식으로 구현할 수 있다.

1
2
3
4
5
extern struct FILE* STDIN;

int getchar() {
	return STDIN->read();
}
  • 다시 말해 getchar()는 STDIN으로 참조되는 FILE 데이터 구조의 read 포인터가 가리키는 함수를 단순히 호출할 뿐이다.
  • 이처럼 단순한 기법이 모든 OO가 지닌 다형성의 근간이 된다.
  • 예를 들어 C++에서는 클래스의 모든 가상 함수는 vtable이라는 테이블에 포인터를 가지고 있고, 모든 가상 함수 호출은 이 테이블을 거치게 된다.
  • 파생 클래스의 생성자는 생성하려는 객체의 vtable을 단순히 자신의 함수들로 덮어 쓸 뿐이다.

함수를 가리키는 포인터를 응용한 것이 다형성의 핵섬이다.

  • 1940년대 후반 폰 노이만 아키텍처가 처음 구현된 이후 프로그래머는 다형적 행위를 수행하기 위해 함수를 가리키는 포인터를 사용해 왔다. 따라서 OO가 새롭게 만든 것은 전혀 없다.
  • 하지만 이 말이 완전히 옳은 말은 아니다.
  • OO 언어는 다형성을 제공하지는 못했지만, 다형성을 좀 더 안전하고 더욱 편리하게 사용할 수 있게 해준다.
  • 함수에 대한 포인터를 직접 사용하여 다형적 행위를 만드는 이 방식에는 문제가 있는데, 함수 포인터는 위험하다는 사실이다.
    • 프로그래머는 이들 포인터를 초기화하는 관례를 준수해야 한다.
    • 또한 이들 포인터를 통해 모든 함수를 호출하는 관례를 지켜야 한다.
    • 프로그래머가 위 관례들을 망각하게 되면 버그가 발생하고, 이러한 버그는 수정이 어렵다.
  • OO 언어는 이러한 관례를 없애주며, 따라서 실수할 위험이 없다.
  • OO 언어를 사용하면 다형성은 대수럽지 않은 일이 된다.
  • OO 언어는 과거 C 프로그래머가 꿈에서야 볼 수 있던 강력한 능력을 제공한다.
  • 이러한 이유로 OO는 제어흐름을 간접적으로 전환하는 규칙을 부과한다고 결론 지을 수 있다.

다형성이 가진 힘

  • 이전 복사 프로그램의 새로운 입출력 장치가 생긴다면 프로그램엔 아무런 변경이 필요치 않다!!
    • 심지어 다시 복사 프로그램을 다시 커파일할 필요 조차 없다.
  • 왜일까? 복사 프로그램 소스 코드는 입출력 드라이버의 소스 코드에 의존하지 않기 때문이다.
  • 입출력 드라이버가 FILE 에 정의된 다섯 가지 표준함수를 구현한다면, 복사 프로그램에선 이 입출력 드라이버를 얼마든지 사용할 수 있다.
  • 다시말해 입출력 드라이버가 복사 프로그램의 플러그인이 된 것이다.
  • 왜 유닉스 운영체제는 입출력 장치들을 플러그인 형태로 만들었는가?
    • 프로그램이 장치 독립적이어야 하기 때문이다.
    • 프로그램에 다른 장치에서도 동일하게 동작할 수 있도록 하는 것이 우리가 진정 바랐던 일임을 깨달았기 때문이다.
  • 위와 같은 플러그인 아키텍처(Plugin Architecture)는 이처럼 입출력 장치 독립성을 지원하기 위해 만들어졌고, 등장이 후 거의 모든 운영체제에서 구현되었다.
  • 그럼에도 대다수의 프로래머는 직접 작성하는 프로그램에선 이러한 개념을 확장하여 적용치 않았는데, 함수를 가리키는 포인터를 사용하면 위험을 수반하기 때문이었다.
  • OO 의 등장으로 언제 어디서든 플러그인 아키텍처를 적용할 수 있게 되었다.

의존성 역전

image 출처: https://wedonttalknemore.tistory.com/10

  • 위 전형적인 호출 트리는 main 함수가 고수준 함수를 호출하고, 고수준 함수는 다시 중간 수준 함수를 호출하며, 중간 수준 함수는 다시 저수준 함수를 호출한다.
  • 이러한 호출트리에서 소스 코드 의존성 방향은 반드시 제어흐름을 따르게 된다.
  • 즉, 제어 흐름은 시스템의 행위에 따라 결정되며, 소스 코드 의존성은 제어흐름에 따라 결정된다.
  • 하지만 다형성이 끼어들면 얘기가 다르다.

image 출처: https://wedonttalknemore.tistory.com/10

  • 위 이미지에서 HL1 모듈은 ML1 모듈의 F() 함수를 호출한다.
  • 소스코드에서는 HL1 모듈은 인터페이스를 통해 F() 함수를 호출한다.
  • 이 인터페이스는 런타임에는 존재하지 않는다.
  • HL1은 단순히 ML1 모듈의 함수 F()를 호출할 뿐이다.

  • 하지만 ML1과 I 인터페이스 사이의 소스코드 의존성(상속 관계)이 제어 흐름과는 반대이다.
  • 이를 의존성 역전이라 부른다.
  • OO 언어가 다형성을 안전하고 편리하게 제공한다는 것은 소스 코드 의존성을 어디에서든 역전시킬 수 있다는 뜻이기도 하다.
    • 소스 코드 의존성은 소스 코드 사이에 인터페이스를 추가함으로써 방향을 역전시킬 수 있다.
    • OO언어로 개발된 시스템은 소스 코드 의존성 전부에 대해 방향을 결정할 수 있는 절대적인 권한을 갖는다.
    • 즉, 소스 코드 의존성이 제어흐름의 반향과 일치되도록 제한되지 않는다.
  • 아래 이미지처럼 업무 규칙(Business Rules)이 데이터베이스와 사용자 인터페이스에 의존하는 대신에, 시스템의 소스 코드 의존성을 반대로 배치하여 데이터베이스와 UI가 업무 규칙에 의존하게 만들 수 있다.

image 출처: https://wedonttalknemore.tistory.com/10

  • 즉, UI와 데이터베이스가 업무 규칙의 플러그인이 된다는 뜻이다.
  • 다시 말해, 업무 규칙의 소스 코드에서는 UI나 데이터베이스를 호출하지 않는다.
  • 결과적으로 업무 규칙(비즈니스 로직), UI, 데이터베이스는 세 가지로 분리된 컴포넌트 또는 배포 가능한 단위(jar, DLL, Gem 등)로 컴파일 할 수 있고, 이 배포 단위들의 의존성 역시 소스 코드 사이의 의존성과 같다.
  • 따라서 업무 규칙을 UI와 데이터베이스와는 독립적으로 배포할 수 있다.
  • UI나 데이터베이스에서 발생한 변경사항은 업무 규칙에 일절 영향을 미치지 않는다. 즉 이들 컴포넌트는 독립적으로 배포 가능하다.

  • 배포 독립성 - 컴포넌트의 소스 코드가 변경되면, 해당 코드가 포함된 컴포넌트만 다시 배포하면 된다.
  • 개발 독립성 - 배포 독립성이 있으면,서로 다른 팀에서 각 모듈을 독립적으로 개발할 수 있다.

나의 생각

  • 일반적으로 표현 영역 - 응용 서비스 영역 - 도메인 영역 - 인프라 영역으로 구성된 레이어드 아키텍처에서 Repository 인터페이스를 도메인 영역에 두고 인프라 영역에 이에 대한 구현체를 구현하는 것이 위의 한 예시라 볼 수 있을 것 같다.
    • 만약 다른 영역과 관련없는 도메인 영역의 특정 로직만 수정하게 될 경우 해당 모듈만 독립적으로 배포하면 되도록 하는 것이 위에서 설명한 배포 독립성이 될 것 같다.

결론

  • OO란 다형성을 이용하여 전체 시스템의 모든 소스 코드 의존성에 대한 절대적인 제어 권한을 획득할 수 있는 능력이다.
  • OO를 사용하면 아키텍트는 플러그인 아키텍처를 구성할 수 있고, 이를 통해 고수준의 정책을 포함하는 모듈은 저수준의 세부사항을 포함하는 모듈에 대해 독립성을 보장할 수 있다.
  • 저수준의 세부사항은 중요도가 낮은 플러그인 모듈로 만들 수 있고, 고수준의 정책을 포함하는 모듈과는 독립적으로 개발하고 배포할 수 있다.

나의 생각

  • 인프라스트럭쳐 영역과 같은 실제 구현기술을 다루는 저수준 모듈은 플러그인 모듈로서 쉽게 다른 것으로 바꿀 수 있도록 하는게 좋다는 것을 말하는 것 같다.

스크린샷 2023-07-09 오후 8 03 42

  • 만약 위 이미지에서 인프라 영역이 DB 를 통해 데이터를 조회하는 Repository 구현체를 가지고 있다고 했을때,
  • 이를 만약 파일 시스템으로부터 조회하도록 변경하고자 한다면 파일 시스템으로부터 조회해오는 인프라 모듈만 새로 구현에서 교체하면 되도록 독립적으로 개발 후 배포하면 될 것이다.

6장 - 함수형 프로그래밍

  • 함수형 프로그래밍이라는 개념은 프로그래밍 그 자체보다 앞서 등장했다.
  • 이 패러다임에서 핵심이 되는 기반은 람다 계산법으로 알로조 처치가 1930년대에 발명했다.

정수를 제곱하기

  • 아래는 25까지의 정수의 제곱을 출력하는 자바 언어의 예시이다.
1
2
3
4
5
6
public class Squint {
	public static void main(String args[]) {
    	for (int i=0; i<25; i++)
        	System.out.println(i*1);
    }
}
  • 리스프에서 파생한 클로저(Clojure)는 함수형 언어로, 클로저를 이용하면 같은 프로그램을 다음과 같이 구현할 수 있다.
1
2
3
4
(println ;___출력한다. 
	(take 25 ;___ 처음부터 25까지
    	(map (fn [x] (* x x)) ;__ 제곱을 
        	(range)))) ;___ 정수의
  • 리스프에선 함수를 괄호안에 넣는 방식으로 호출한다. (range) 는 range 함수를 호출한다.
  • 표현식 (fn [x] (* x x)) 는 익명 함수로, 곱셈 함수를 호출하면서 입력 인자를 두 번 전달한다. 즉, 입력의 제곱을 계산한다.
  • 전체 코드를 다시 해석해보자.
    • 1)range 함수는 0부터 시작해서 끝이 없는 정수 리스트를 반환한다.
    • 2)반환된 정수 리스트는 map 함수로 전달되고, 각 정수에 대해 제곱을 계산하는 익명 함수를 호출하여, 모든 정수의 제곱에 대해 끝이 없는 리스트를 생성한다.
    • 3)제곱된 리스트는 take 함수로 전달되고, 이 함수는 앞의 25개까지의 항목으로 구성된 새로운 리스트를 반환한다.
    • 4)println 함수는 입력 값을 출력하는데, 이 경우 입력은 앞의 25개의 정수에 대한 제곱 값으로 구성된 리스트다.

클로저(함수형 언어)와 자바의 극단적 차이

  • 자바는 가변 변수(mutable variable)를 사용하는데, 가변 변수는 프로그램 실행중에 상태가 변할 수 있다.(위의 예시에서 반복문을 제어하는 i 변수)
  • 클로저는 가변 변수가 전혀 없다. x 와 같은 변수가 한 번 초기화되면 절대 변하지 않는다.
    • 이는 우리에게 놀라운 사실을 알려준다. 함수형 언어에선 변수는 변경되지 않는 다는 것이다.

불변성과 아키텍처

  • 아키텍트는 왜 변수의 가변성을 고려하는가?
  • 가변 변수로 인해 발생하는 문제는 다음과 같다.
    • 경합(race) 조건
    • 교착상태(deadlock) 조건
    • 동시 업데이트(concurrent update)
  • 다시 말해 우리가 동시성 애플리케이션에서 마주치는 모든 문제, 즉 다수의 스레드와 프로세스가 여러 개인 상황에서도 설계한 시스템이 여전히 강건하길 바란다.
  • 그렇다면 이제 불변성이 정말 실현 가능한지를 스스로에게 반드시 물어봐야 한다.
    • 저장 공간이 무한하고 프로세서 속도가 무한 빠르다고 전제한다면 위 질문에 대한 대답을 긍정적일 것이다./
    • 반대로 자원이 무한대가 아니라면 대답은 조금 미묘할 것이다.
    • 위처럼 불변성은 실현 가능하겠지만, 이처럼 타협이 조금 필요한데 어떤 타협이 필요한지 살펴보자.

가변성의 분리

  • 불변성과 관련하여 가장 주요한 타협 중 하나는 애플리케이션, 또는 애플리케이션 내부의 서비스를 가변 컴포넌트와 불변 컴포넌트로 분리하는 일이다.
  • 불변 컴포넌트에서는 순수하게 함수형 방식으로만 작업이 처리되며, 가변 변수는 사용되지 않는다.
  • 불변 컴포넌트는 변수의 상태를 변경할 수 있는 순수 함수형 컴포넌트가 아닌 하나 이상의 다른 컴포넌트와 서로 통신한다.

image 출처: https://wedonttalknemore.tistory.com/11

  • 상태 변경은 컴포넌트를 갖가지 동시성 문제에 노출하는 꼴이므로, 흔히 트랜잭션 메모리와 같은 실천법을 사용하여 동시 업데이트와 경합 조건 문제로부터 가변 변수를 보호한다.
  • 트랜젝션 메모리는 데이터베이스가 디스크의 레코드를 다루는 방식과 동일한 방식으로 메모리의 변수를 처리한다.
    • 즉, 트랜잭션을 사용하거나 또는 재시도 기법을 통해 이들 변수를 보호한다.
  • 이러한 접근법의 예시로 클로저의 atom 기능을 들 수 있다.
    • 클로저의 atom 기능과 관련된 예시는 여기를 참고바란다.
  • 말하고자하는 요지는, 애플리케이션을 제대로 구조화하려면 변수를 변경하는 컴포넌트와 변경하지 않는 컴포넌트를 분리해야 한다는 것이다.
    • 그리고 이렇게 분리하려면 가변 변수들을 보호하는 적절한 수단을 동원해 뒷받침해야 한다.
  • 현명한 아키텍트라면 가능한 한 많은 처리를 불변 컴포넌트로 옮겨야 하고, 가변 컴포넌트에서는 가능한 한 많은 코드를 빼내야 한다.

이벤트 소싱

  • 기술의 발전으로 인해 저장 공간과 처리 능력의 한계는 우리 시야에서 급격히 사라지고 있다.
  • 더 많은 메모리를 확보할수록, 기계가 더 빨라질수록 필요한 가변 상태는 더 적어진다.
  • 간단한 예로, 고객 계좌를 관리하는 은행 어플리케이션을 생각해보자.
    • 이 애플리케이션에서는 입금 트랜잭션과 출금 트랜잭션이 실행되면 잔고를 변경해야 한다.
    • 여기서 계좌 잔고를 변경하는 대신 트랜잭션 자체를 저장한다고 생각해보자.
    • 누군가 잔고 조회를 요청할 때마다 계좌 개설 시점부터 발생한 모든 트랜잭션을 단순히 더한다.
    • 이 전략에서는 가변 변수가 하나도 필요 없다.
  • 당연하게 위와 같은 접근법은 터무니 없다.
    • 시간이 지날수록 트랜잭션 수는 끝없이 증가하고, 잔고계산에 필요한 컴퓨팅 자원은 걷잡을 수 없이 커진다.
    • 따라서 이 전략이 영원히 실현 가능하려면 무한한 저장 공간과 무한한 처리 능력이 필요하다.
  • 하지만 이 전략이 영원히 동작하게 만들 필요는 없다.
    • 아마도 애플리케이션의 생명주기 동안만 문제없이 동작할 정도의 저장공간과 처리 능력만 있으면 충분할 것이다.
  • 이벤트 소싱에 깔려 있는 기본 발상이 바로 이것이다.
    • 이벤트 소싱은 상태가 아닌 트랜잭션을 저장하자는 전략이다.
    • 상태가 필요해지면 단순히 상태의 시작점부터 모든 트랜잭션을 처리한다.
    • 물론 지름길을 택할 순 있다. 매일 자정에 상태 계산 후 저장한다. 그 후 상태 정보가 필요해지면 자정 이후의 트랜잭션만을 처리하면 된다.
  • 위 전략은 저장 공간을 많이 필요할 것인데 우리 시대는 저장 곤간을 이제 충분히 확보 가능하다.
  • 더 중요한 점은 데이터 저장소에서 삭제되거나 변경되는 것은 하나도 없다는 사실이다. 데이터의 CRUD 가 아닌 CR 만 있는 것이다.
  • 또한 데이터 저장소에서 변경과 삭제가 전혀 발생하지 않으므로 동시 업데이트 문제 또한 일어나지 않는다.
  • 저장 공간과 처리 능력이 충분하면 애플리케이션이 완전한 불변성을 갖도록 만들 수 있고, 따라서 완전한 함수형으로 만들 수 있다.
  • 위 이야기가 여전히 터무늬없게 들린다면, 소스 코드 버전 관리 시스템(ex. git)이 정확히 이 방식으로 동작한다는 사실을 떠올려 보면 도움이 될 것이다.

결론

  • 프로그래밍 패러다임에 대해 요약하면 다음과 같다.
    • 구조적 프로그래밍은 제어흐름의 직접적인 전환에 부과되는 규율이다.
    • 객체 지향 프로그래밍은 제어흐름의 간접적인 전환에 부과되는 규율이다.
    • 함수형 프로그래밍은 변수 할당에 부과되는 규율이다.
  • 세 패러다임 모두 우리에게서 무언가를 앗아갔다.
    • 각 패러다임은 코드를 작성하는 방식의 형태를 한정시킨다.
    • 어떤 패러다임도 우리의 권한이나 능력에 무언가를 보태지는 않는다.
  • 지난 반세기 동안 배운 것은 해서는 안되는 것에 대해서다.
    • 이 사실을 깨닫는다면 소프트웨어는 급격히 발전하는 기술이 아니라는 진실을 마주하게 된다.
    • 1946년 앨런 튜링이 전자식 컴퓨터에서 실행할 거의 최초의 코드를 작성할 때 사용한 소프트웨어 규칙과 지금의 소프트웨어 규칙은 조금도 다르지 않다.
    • 도구, 하드웨어 모두 많이 발전했지만, 소프트웨어의 핵심은 여전히 그대로다.
  • 소프트웨어는 순차(sequence), 분기(selection), 반복(iteration), 참조(indirection)로 구성되며, 그 이상 그 이하도 아니다.

Reference

This post is licensed under CC BY 4.0 by the author.

[Java] HashSet은 어떠한 이유로 순서를 보장하지 않을까

[클린아키텍처] 7 ~ 11장