본문 바로가기

PROGRAMMING

[Clean Architecture] 책 읽기 1장 ~ 6장

 

분명 책은 1년 전에 샀지만 그때 당시에는 함수형 프로그래밍? 그게ㅔ뭔ㄷ.. 시절이라 흥미가 가지 않던 책이었는데

 

iOS에서 클린 아키텍처를 직접 프로젝트에 적용해보니 개발시간이 단축되는 느낌이나 편리한 느낌이 있었다..

 

근데 왜 편리한지, 왜 객체 지향 설계 원칙을 준수하면서 아키텍처를 만들었는지,

 

근본적인 물음이 해결되지는 않았기 때문에 책을 읽으면서 생각을 정리해보려고 한돠,,

 

 

 

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

제대로 된 소프트웨어를 만들면 아주 적은 인력만으로도 새로운 기능을 추가하거나 유지보수할 수 있다.
변경은 단순해지고 빠르게 반영할 수 있다. 결함은 적어지고 잦아든다. 최소한의 노력으로 기능과 유연성을 최대화할 수 있다.

이런 일을 가능하게 해주는 것이 아키텍처라고 한다. 여기서 중요한 말은 유연성인 것 같다.

유연성이 좋은 소프트웨어는 새로운 기능을 추가하거나 유지보수할 때 들어가는 비용이 현저히 줄어든다.

따라서 제대로 만든 설계와 아키텍처를 사용하는 이유는 적은 인력으로 쉽게 구현하고 유지보수 할 수 있기 때문이다.

 

설계와 아키텍처 둘 사이에는 차이가 없다는 점이다. 아무런 차이가 없다.
'아키텍처'는 저수준의 세부사항과는 분리된 고수준의 무언가를 가리킬때 흔히 사용되는 반면,
'설계'는 저수준의 구조 또는 결정사항 등을 의미할 때가 많다.
하지만 실제로 이 둘을 구분 짓는 경계는 뚜렷하지 않다. 고수준에서 저수준으로 향하는 의사결정의 연속성만이 있을 뿐이다.

설계와 아키텍처를 구분짓는 뚜렷한 경계는 없다.

 

결론

어떤 경우라도 개발 조직이 할 수 있는 최고의 선택지는 조직에 스며든 과신을 인지하여 방지하고, 소프트웨어 아키텍처의 품질을 심각하게 고민하기 시작하는 것이다.

소프트웨어 아키텍처의 품질을 고민하는 것은 결국에 좋은 생산성을 위해 고민하는 것과 같다는 결론을 얻을 수 있었다.

어떻게 아키텍처를 설계해야 좋은 생산성으로 이어지는지 자세한 내용은 차차 알아가는 것으로 !

 

 

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

행위

소프트웨어의 첫 번째 가치

요구사항을 구현하고 버그를 수정한다.

 

아키텍처

소프트웨어의 두 번째 가치

부드러워야 한다. = 소프트웨어를 만든 이유는 행위를 쉽게 변경할 수 있도록 하기 위해서다.

이해관계자가 기능을 바꾸면, 이러한 변경사항을 쉽게 적용할 수 있어야한다.

 

아키텍처가 특정 형태를 다른 형태보다 선호하면 할수록, 새로운 기능을 이 구조에 맞추는 게 더 힘들어진다.
따라서 아키텍처는 형태에 독립적이어야 하고, 그럴수록 더 실용적이다.

형태에 독립적이어야 한다는 말이 잘 이해가 안간다.. 범용적이어야 한다는 것인가!

 

더 높은 가치

기능인가 아니면 아키텍처인가? 둘 중 어느 것의 가치가 높은가?

여기서 나온 예시는

- 완벽하게 동작하지만 수정이 아예 불가능한 프로그램

- 동작은 하지 않지만 변경이 쉬운 프로그램

 

이렇게 주며 기능과 아키텍처의 가치에 대해 생각해보라고 한다.

무조건 아키텍처의 가치가 더 높다기 보다는, 현재의 기능도 중요하지만 아키텍처의 가치도 현재의 기능의 가치보다 못하진 않다.

정도로 생각하고자 한다. 

 

아키텍처를 위해 투쟁하라

아키텍트는 기능을 개발하기 쉽고, 간편하게 수정할 수 있으며, 확장하기 쉬운 아키텍처를 만들어야 한다.
아키텍처가 후순위가 되면 시스템을 개발하는 비용이 더 많이 들고, 일부 또는 전체 시스템에 변경을 가하는 일이 현실적으로 불가능해진다.

좋은 아키텍처의 정의가 나온 것 같다.

기능을 개발하기 쉽고, 간편하게 수정할 수 있으며, 확장하기 쉬운 아키텍처를 만들어야 한다.

헛헛 이상적인 소프트웨어인가


프로그래밍 패러다임

3장. 패러다임 개요

패러다임이란 프로그래밍을 하는 방법으로, 대체로 언어에는 독립적이다.
패러다임은 어떤 프로그래밍 구조를 사용할지, 그리고 언제 이 구조를 사용해야 하는지를 결정한다.
현재까지 이러한 패러다임에는 세가지 종류가 있다.

여태 C, C++, Java, Python, Swift 등 언어를 사용하면서 패러다임에 관해서 동일한 이야기를 했었다.

함수형 프로그래밍, 객체지향 프로그래밍.. 

생각해보니 이런 동일한 패러다임을 적용해서 언어를 만들었기 때문에 한가지 언어를 익히면 조금 쉽게 다른 언어를 익혔던 것 같다.

 

구조적 프로그래밍

무분별한 점프 (goto) 가 프로그램 구조에 해롭기 때문에

점프를 if/then/else, do/while/until 과 같은 구조로 대체

구조적 프로그래밍은 제어흐름의 직접적인 전환에 대해 규칙을 부과한다.

 

객체 지향 프로그래밍

올레 요한 달, 크리스텐 니가드 두 명의 프로그래머가

알골 언어의 함수 호출 스택 프레임을 힙으로 옮기면,

함수 호출이 반환된 이후에도 함수에서 선언된 지역 변수가 오랫동안 유지될 수 있음을 발견했다.

 

함수 -> 클래스의 생성자

지역 변수 -> 인스턴스 변수

중첩 함수 -> 메서드

 

그리고 함수 포인터를 특정 규칙에 따라 사용하는 과정을 통해 필연적으로 다형성이 등장하게 되었다.

객체 지향 프로그래밍은 제어흐름의 간접적인 전환에 대해 규칙을 부과한다.

메모리 구조를 배울 때 클래스는 힙에 저장되고, 함수 호출 프레임은 스택에 저장된다고 배웠는데

클래스도 원래 초기엔 함수 호출 프레임이었고 이를 힙에 저장한 것을 클래스라고 부른 것 같다.

 

함수형 프로그래밍

최근에서야 겨우 도입되기 시작했지만, 세가지 패러다임 중 가장 먼저 만들어졌다.

람다 계산법에 영향을 받아서 만들어진 패러다임

람다 계산법의 기초 개념 : 불변성

 = 함수형 언어에는 할당문이 전혀 없다는 뜻! (굉장히 까다로운 조건에서만 가능)

함수형 프로그래밍은 할당문에 대해 규칙을 부과한다.

 

각 패러다임은 프로그래머에게서 권한을 박탈한다. 어느 패러다임도 새로운 권한을 부여하지 않는다.
즉, 패러다임은 무엇을 해야 할지를 말하기 보다는 무엇을 해서는 안 되는지를 말해준다.

단순히 프로그래밍을 하는 방법을 패러다임이라고 생각했는데

무엇을 하면 안되는지를 알려주는 것도 패러다임이구나..

 

구조적 프로그래밍 - goto문 (1학년때 c? c++ 하면서 보고 써보진 않았던)

객체 지향 프로그래밍 - 함수 포인터 (이걸 빼앗기 보다는 잘 활용한다..?는 것 같은데)

함수형 프로그래밍 - 할당문

 

저자는 앞으로 최소한 부정적인 의도를 가진 프로그래밍 패러다임은 딱 세 가지밖에 없을 것이라고 말한다.

 

사실 잘 모르겠지만!

적어도 몇십년 동안 이 세가지 패러다임 외에 새롭게 등장한 패러다임이 없다는 사실이 이를 아직까지 증명할 수 있는 것 같다.

 

결론

세 가지의 패러다임과 아키텍처의 관심사는 연관되어 있다. (함수, 컴포넌트 분리, 데이터 관리)

어떻게 연관되는지는 뒤에서!

 

 

4장. 구조적 프로그래밍

(책에서는 데이크스트라 이지만.. 다익스트라가 너무 익숙하다 ㅎ.ㅎ)

증명

다익스트라는 프로그래머가 입증된 구조를 이용하고, 이들 구조를 코드와 결합시키며, 
그래서 코드가 올바르다는 사실을 증명하고자 했다.

hmm.. 다익스트라가 프로그래밍은 어렵고, 프로그래머가 프로그래밍을 잘하지 못하는 문제가 있었기 때문에!

아주 작은 세부사항을 간과해서 실패를 피하기 위해 코드가 올바르다는 것을 그 자체로 증명하게 하려는 연구를 했다.

고 이해했다. 음! ㅇㅅㅇ?

 

이 연구를 진행하다 goto 문장이 모듈을 더 작은 단위로 재귀적으로 분해하는 과정에 방해가 되는 경우가 있다는 사실을 발견했다.

연구를 위해 모듈을 분해하는 분할 정복을 사용해야 했는데, goto 문장이 이 모듈 분해를 방해했다고 한다.

-> 무분별한 goto 사용이 아닌 if/then/else, do/while 같은 분기와 반복이라는 단순한 제어 구조를 사용해야한다.

그래야 증명 가능한 단위로 모듈 분해가 가능하다.

 

뵘, 야코피니 :
모든 프로그램을 순차 sequence, 분기 selection, 반복 iteration 이라는 세 가지 구조만으로 표현할 수 있다는 사실을 증명했다.

 

현재 우리 모두는 구조적 프로그래머라고 한다!

break 문도 goto문이 아니냐 -> 제어 흐름을 아무 제약 없이 전환할 수 있던 goto 문과는 달리 제약이 있다.

 

구조적 프로그래밍을 사용하면 프로그래머는 대규모 시스템을 모듈과 컴포넌트로 나눌 수 있다. 

 

=> 여기 까지의 결론은

 

goto 문장을 사용하지 않는 구조적 프로그래밍을 통해

대규모 시스템을 모듈과 컴포넌트로 나눌 수 있고,

더 나아가 모듈과 컴포넌트를 입증할 수 있는 더 작은 기능들로 세분화 해서 모듈과 컴포넌트를 입증할 수 있다.

 

테스트

"테스트는 버그가 있음을 보여줄 뿐, 버그가 없음을 보여줄 수는 없다". 고 말한 적이 있다.
테스트에 충분한 노력을 들였다면 테스트가 보장할 수 있는 것은 프로그램이 목표에 부합할 만큼은 충분히 참이라고 여길 수 있게 해주는 것이 전부다.

당연하고 맞는 말인데 뭔가,, 생각지 못한 부분이었다.

테스트를 하는 이유가 테스트를 했던 목표 부분 만큼은 올바르게 동작한다는 것을 증명할 수 있다는 것이었다. 

(물론 모든 케이스를 테스트 할 수 있을지도 보장할 수 없다)

 

결론

구조적 프로그래밍이 오늘날까지 가치 있는 이유는 프로그래밍에서 반증 가능한 단위를 만들어 낼 수 있는 이 능력 때문이다.
뿐만 아니라 아키텍처 관점에서는 기능적 분해를 최고의 실천법 중 하나로 여기는 이유이기도 하다.
소프트웨어 아키텍트는 모듈, 컴포넌트, 서비스가 쉽게 반증 가능하도록 (테스트하기 쉽도록) 만들기 위해 분주히 노력해야 한다.

구조적 프로그래밍의 가치는?

- 코드를 테스트 가능한 단위로 나눌 수 있다.

- 하지만 goto 문장은 이를 방해하기 때문에 단순한 분기와 반복문을 사용해야 한다.

 

테스트 가능한 단위로 나누기 위해서는?

- 테스트 가능한 단위로 나누기 위해서는 구조적 프로그래밍과 유사한 제한적인 규칙을 받아들여 사용해야한다.

- 제한적인 규칙은 뒤에!

 

 

5장. 객체 지향 프로그래밍 Object-Oriented Programming

객체 지향 Object-Oriented 는 무엇일까?

- 객체 지향 언어는 캡슐화 encapsulation, 상속 inheritance, 다형성 polymorphism 최소한 세 가지 요소를 반드시 지원해야 한다고 한다.

 

캡슐화 ? (물음표인 이유는..)

객체 지향 언어는 데이터와 함수를 쉽고 효과적으로 캡슐화하는 방법을 제공한다.

이렇게 캡슐화 된 데이터와 함수를 집단으로 나눠서 분리할 수 있다.

 

데이터는 은닉, (public)

일부 함수만이 노출 (private)

 

그렇다면 객체 지향 언어만이 캡슐화가 가능할까?

 

아니다. 객체 지향 언어가 아닌 C 언어에서도 완벽하게 캡슐화가 가능하다.

// C언어
// point.h

// point.h 를 사용할때 makePoint, distance 함수를 호출할 순 있지만 Point의 x, y에 접근할 방법이 없다.
struct Point;
struct Point* makePoint(double x, double y);
double distance(struct Point *p1, struct Point *p2);

// point.c

#inlcude "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++ 언어가 등장하면서 C++ 컴파일러는 클래스의 인스턴스 크기를 알 수 있어야 하기 때문에

완벽한 캡슐화가 불가능해졌다.

// C++
// point.h 
// 헤더 파일에서 클래스를 정의해줘야 한다.

class Point {
public:
    Point(double x, double y);
    double distance(struct Point *p1, struct Point *p2);
private:
    double x;
    double y;
};

헤더 파일을 사용하면 x, y에 접근할 수는 없지만 변수가 존재한다는 사실을 알게 된다.

 

x, y라는 이름이 바뀐다면 다시 컴파일 해야하기 때문에 완벽한 캡슐화는 깨지게 된다.

 

-> 보완?

public, private, protected 키워드를 도입했지만 이는 임시방편이다.

 

자바, C#, 현재 내가 사용하는 Swift는 헤더와 구현체 분리를 하지 않기 때문에 캡슐화는 더욱 손상되었다고 한다.

 

당연히 private, protected 키워드들이 캡슐화를 보장하는 것이라고 생각했는데 C언어의 코드를 보니까

완벽한 캡슐화란 다시 컴파일 할 필요도 없이 내부를 은닉하는 것이구나.. 새로운 관점 ㅇ0ㅇ

 

상속?

객체 지향 언어가 더 나은 캡슐화를 제공하지는 못했지만, 상속만큼은 확실히 제공해준다..!?

 

이전의 C 프로그램 뿐만 아니라 대다수의 언어에서도 상속을 구현할 수 있었다.

 

상속처럼 구현하는 예시)

// namedPoint.h

struct NamedPoint;

struct NamedPoint* makeNamedPoint (double x, double y, char* name) ;
void setName (struct NamedPoint* np, char* name) ;
char* getName(struct NamedPoint* p);

// namedPoint. c

#include "namedPoint. h"
#include <stdlib.h>

struct NamedPoint {
    double x, y;
    char* name:
};

struct NamedPoint* makeNamedPoint (double x, double y, char* name) {
    struct NamedPoint* p = malloc(sizeof (struct NamedPoint));
    p->× = x;
    p->y = y;
    p->name = name;
    return p;
}

void setName (struct NamedPoint* np, char* name) {
    np->name = name;
}

char* getName (struct NamedPoint* np) {
    return np->name;
}

// main.c

#include "point.h"
#include "namedPoint.h"
#include <stdio.h>

int main(int ac, char** av) {
    struct NamedPoint* origin = makeNamedPoint (0.0, 0.0, "origin");
    struct NamedPoint* upperRight = makeNamedPoint(1.0, 1.0, "upperRight");
    printf("distance='f\n",
    distance (
        (struct Point*) origin, // NamePoint에서 Point로 UpCasting
        (struct Point*) upperRight)
    );
}

 

NamePoint의 데이터 구조와 Point의 데이터 구조 순서가 동일하기 때문에 가능한 일이다

-> 실제 C++은 이 방법을 사용해서 단일 상속을 구현했다.

 

하지만 다중 상속을 구현하는 것은 훨씬 어려운 일! 

 

그러고 보니 Swift는 다중상속을 지원하지 않는다. 이유는 추후에 찾아봐야지..

 

아무튼,

 

상속이란 새로운 개념을 객체 지향 언어가 만든 것은 아니고 편리한 방식으로 제공했다는 결론!

 

 

다형성 ?

다형성도 객체 지향 언어 이전에 표현할 수 있었다고 한다. (이쯤되면 객체 지향이 짬뽕언어인가..싶다)

 

#include <stdio. h>

void copy() {
    int c;
    while ((c = getchar()) != EOF)
        putchar(c);
}

 

getchar() 함수는 STDIN 에서 문자를 읽는다.

putchar() 함수는 STDOUT 으로 문자를 쓴다.

 

문자를 읽고 쓰는 행위는 STDIN과 STDOUT에 의존한다. (이것은 JAVA 형식의 인터페이스)

 

하지만 C에는 인터페이스가 없다.

-> UNIX 운영체제의 경우 입출력 장치 드라이버가 open, close, read, write, seek 표준 함수를 제공할 것을 요구한다.

 

결국엔 getchar() 함수가 STDIN의 FILE 데이터 구조의 read() 포인터가 가르키는 함수를 호출한다.!

(운영체제가 제공하는 read 함수를 단순히 호출만 할 뿐이다.)

 

이러한 기법이 다형성의 근간이 된다고 한다.

 

C++ 에서는 클래스의 모든 가상 함수 virtual function 는 vtable 이라는 테이블에 포인터를 가지고 있고,
모든 가상 함수 호출은 이 테이블을 거치게 된다.

 

따라서 다형성 - 함수 포인터를 응용한 것

 

다형성이 뭐가 좋은가?

" 프로그램이 장치 독립적으로 작동할 수 있게 해준다. "

 

예를들어 위에서 운영체제가 장치 드라이버에게 open, close, read, write, seek 표준 함수들을 제공하라고 요구하기 때문에!!

다른 장치 드라이버가 들어와도 별도의 변경 없이 바로 사용할 수 있는 것이다.

- 키보드를 연결만하면 바로 사용할 수 있는 것처럼!

 

 

의존성 역전

다형성을 안전하고 편리하게 적용할 수 있는 매커니즘이 등장하기 전 
소프트웨어 소스 코드 의존성의 방향은 반드시 제어흐름을 따르게 되어있었다.

다형성을 안전하고 편리하게 적용할 수 있다

-> 의존성 방향을 어디서나 역전 시킬 수 있다. (의존성 역전!)

 

HL1 모듈

ML1 모듈 - Function()

ML1의 인터페이스 - Function()

 

HL1 모듈이 ML1 모듈의 Function()을 호출한다고 해보자.

HL1 모듈은 인터페이스를 통해 Function()을 호출한다.

이 인터페이스는 런타임에 존재하지 않는다!

인터페이스를 통해 ML1 모듈의 Function()을 호출하는 것이다.

 

하지만 ML1과 인터페이스 사이의 제어 흐름은 반대이다.

 

그림이 없지만..!

 

원래 

HL1  -->  ML1

 

이렇게 제어흐름이었다면

 

HL1  -->  인터페이스  <--의존성 역전!--  ML1

 

이렇게 의존성 역전이 된 것이다.

 

결론은 의존성을 원하는 방향으로 설정할 수 있다는 것이다.

이것이 객체 지향이 제공하고 지향하는 관점이다.

 

 

 

그래서 이걸로 뭘 할 수 있는데?!

 

->  직접적으로 다른 컴포넌트에 의존하지 않기 때문에 독립적으로 배포가 가능하다.

->  독립적으로 배포가 가능하다면 각 모듈을 독립적으로 개발할 수 있다.

 

 

아키텍트 관점에서 가장 중요한 개념이 다형성인 것 같다.

모듈을 독립적인 컴포넌트로 만들기 위해서 의존성 역전이 필요하고, 의존성 역전을 어디서나 적용할 수 있게 해주는 것이

객체 지향이 제공하는 다형성 덕분이다.

 

 

 

6장. 함수형 프로그래밍

패러다임의 핵심이 되는 기반 ! - 람다 lambda

파이썬, 자바, Swift 등.. 많은 언어에서 사용되는 것이 함수형 프로그래밍이다. 살펴보자!

 

함수형 언어에서 변수는 변경되지 않는다.

처음부터 끝까지 변수는 변경되지 않는다는 내용이 전부다. ㅎ.ㅎ

 

변수가 변경되지 않는다 = 불변성

불변성이 중요한 이유는?

 

1. race condition

2. deadlock

3. concurrent update

 

이러한 동시성 문제들이 발생하는 이유가 가변 변수이기 때문이다.

오..!

 

lock 이나 모든 변수가 불변이라면 이런 문제들이 발생할 일도 없다.

-> 실제로 실현 가능할까?

 

저장 공간이 무한하고 프로세서의 속도가 무한히 빠르다면 가능하다. (불가능하다는 말 아닌가?)

 

 

가능하게 하는 방법들은 뭐가 있을까?

- transaction

- atom

 

등이 있다. 하지만 적당히 타협을 해야한다.

 

애플리케이션을 구조화 하려면 변수를 변경하는 컴포넌트변경하지 않는 컴포넌트를 분리해야 한다는 것이다.
그리고 이렇게 변경하려면 가변 변수들을 보호하는 적절한 수단 (transaction, atom)을 동원해 뒷받침해야한다.

트랜잭션을 사용하려면 애플리케이션 수명 주기 동안만 트랜잭션을 저장할 공간과 처리 능력만 있으면 충분 할 것이다.

이벤트 소싱의 기본 발상이 요것이다. https://learn.microsoft.com/ko-kr/azure/architecture/patterns/event-sourcing

데이터가 수행되는 전체 작업을 모두 저장한다.!

 

-> 저장 공간이 많이 필요할 것이다. why? 작업을 새로 저장하고 읽기 때문에

-> CRUD 작업에서 CR 만 수행한다.

-> 동시 업데이트 문제가 절대 발생하지 않는다.

(소스 코드 버전 관리 시스템이 이러한 방식으로 동작한다.)

 

 

 


세 가지 패러다임의 정리 끝

 

 

 

https://www.google.co.kr/books/edition/%ED%81%B4%EB%A6%B0_%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98_%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4_%EA%B5%AC/xk9MyAEACAAJ?hl=ko 

 

클린 아키텍처: 소프트웨어 구조와 설계의 원칙

저자 : 로버트 C. 마틴 (Robert C. Martin) “밥 아저씨(Uncle Bob)”로 불리기도 한다. 1970년부터 프로그래머로 활동했다. 전 세계 콘퍼런스에서 호평받는 연사이며, 《클린 코드》, 《UML 실전에서는 이

books.google.co.kr

 

'PROGRAMMING' 카테고리의 다른 글

[Clean Architecture] 책 읽기 7장 ~ 11장  (0) 2023.04.03
LG ThinQ 오픈소스 목록  (0) 2021.08.19
API  (0) 2021.08.13
REST API  (0) 2021.03.23
react native 관해서 끄적  (0) 2021.02.20