본문 바로가기
IT 서적/Clean Architecture

[클린 아키텍처] 2부. 벽돌부터 시작하기: 프로그래밍 패러다임 (구조적/객체지향/ 함수형 프로그래밍)

by Crystal.k 2022. 2. 27.

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

3장. 패러다임 개요

  • 구조적 프로그래밍
    • 최초로 적용된 패러다임
    • 하지만 최초로 만들어진 패러다임은 아니다.
    • 1968년 에츠허르 비버 데이크스트라가 발견했다.
    • 구조적 프로그래밍은 제어 흐름의 직접적인 전환에 대해 규칙을 부과한다
  • 객체 지향 프로그래밍
    • 두 번째로 도입된 패러다임
    • 구조적 프로그래밍 보다 2년 앞선 1966년에 올레 요한 달과 크리스텐 니가드에 의해 등장했다.
    • 알골(ALGOL)언어의 함수 호출 스택 프래임을 힙으로 옮기면 함수 호출이 반환된 이후에도 함수에서 선언된 지역변수가 오랫동안 유지될 수 있음을 발견했다.
    • 객체 지향 프로그래밍은 제어흐름의 간접적인 전환에 대해 규칙을 부과한다.
  • 함수형 프로그래밍
    • 최근에 들어서야 도입되기 시작했다.
    • 만들어진 것은 세 패러다임중 가장 먼저 만들어졌다.
    • 알론조 처치는 수학적문제를 해결하는 과정에서 람다 계산법을 발명했고, 함수형 프로그래밍은 람다계산법에 직접적인 영향을 받아 만들어졌다.
    • 1958년에 존 매카시가 만든 LISP언어의 근간이 되는 개념이 바로 람다 계산법이다.
    • 람다계산법의 기초가 되는 개념은 불변성으로, 심벌의 값이 변경되지 않는다는 개념이다. 이는 함수형 언어에는 할당 문이 전혀 없다는 뜻이기도 하다.
    • 함수형 프로그래밍은 할당문에 대해 규칙을 부과한다.

생각할 거리

  • 각 패러다임은 프로그래머에게서 권한을 박탈한다.
  • 즉, 무엇을 하면 안되는지를 말해준다.
  • goto문, 함수 포인터, 할당 문을 앗아간다.
  • 이 밖에 새롭게 등장할 패러다임은 없을 것.

결론

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

4장 구조적 프로그래밍

증명

프로그래밍은 어렵고, 프로그래머는 프로그래밍을 잘하지 못한다. 단순해 보이지만 작은 세부사항이라도 간과하면 결국 예상외의 방식으로 실패했다.

이 문제를 “증명”을 적용해서 해결하고자했다.

유클리드 계층구조를 프로그램에도 도입할 수 있지 않을까?

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

모든 프로그램을 순차(Sequence), 분기(selection), 반복(iteration)이라는 세 가지 구조만으로 표현할 수 있다는 사실을 증명했다.

  • 순차 구문
    • 올바름을 입증할 수 있다는 사실을 보여주었다.
    • 순차 구문의 입력을 순차 구문의 출력까지 수학적으로 추적한다.
    • 이 접근법은 일반적인 수학적 증명 방식과 다를 바 없다.
  • 분기
    • 데이크스트라는 열거법을 재적용하는 방식으로 처리했다.
    • 분기를 통한 각 경로를 열거했다. 결과적으로 두 경로가 수학적으로 적절한 결과를 만들어낸다면 증명은 신뢰할 수 있게 된다.
  • 반복
    • 반복이 올바름을 증명하기 위해 데이크스트라는 귀납법을 사용했다.
    • 열거법에 따라 1의 경우가 올바름을 증명했다. 그리고 N의 경우가 올바르다고 가정할 때 N+1의 경우도 올바름을 증명하여 이 경우에도 열거법을 사용했다.
    • 또한 반복의 시작 조건과 종료 조건도 열거법을 통해 증명했다.

이 증명은 고되고 복작했지만 증명은 증명이었다. 증명을 해냄으로써 프로그램에서도 정리에 대한 유클리드 계층구조를 만들 수 있을 거라는 생각이 실제로 이루어질 듯 보였다.

해로운 서명서

데이크스트라는 “goto문의 해로움”이라는 글에서 데이크스트라는 세 가지 제어 구조에 대한 자신의 의견을 피력했고 그 후 전쟁이 시작됐다. 논쟁은 10년 이상 지속되었고 마침내 데이크스트라가 승리했다. 대다수의 현대적 언어는 goto문장을 포함하지 않는다.

현재 우리 모두는 구조적 프로그래머이다. 제어 프름을 제약 없이 직접 전환할 수 있는 선택권 자체를 언어에서 제공하지 않기 때문이다.

기능적 분해

구조적 프로그래밍을 통해 모듈을 증명 가능한 더 작은 단위로 재귀적으로 분해할 수 있게 되었고 결국 모듈을 기능적으로 분해 가능해졌다. 거대한 문제 기술서를 받더라도 저수준의 함수들로 분해 가능하다.

1970~1980년대에 구조적 분석(Structured analysis)이나 구조적 설계(structured design)와 같은 기법이 인기를 끌었다.

프로그래머는 대규모 시스템을 모듈과 컴포넌트로 나눌 수 있고, 더 나아가 모듈과 컴포넌트를 입증할 수 있는 아주 작은 기능들로 세분화할 수 있다.

엄밀한 증명은 없었다.

프로그램 관점에서 정리에 대한 유클리드 계층구조는 끝내 만들어지지 않았다. 대개의 프로그래머들은 세세한 기능 하나하나 엄밀히 증명하는 고된 작업에서 이득을 얻으리라고는 생각하지 않았다.

올바른지를 입증할 때 사용하는 전략에는 유클리드 방식만 있는 것은 아니다. 과학적 방법(scientific method)도 있다.

과학이 구출하다.

과학 이론과 법칙은 그 올바름을 절대 증명할 수 없다. 뉴턴의 운동 제2법칙인 F=ma를 옳다고 증명할 수 없다. 다만 시연하거나 정확도를 측정할 수는 있다.

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

모든 서술이 증명 가능한 것은 아니다.

수학은 증명 가능한 서술이 참임을 입증하는 원리.

과학은 증명 가능한 서술이 거짓임을 입증하는 원리.

테스트

데이크스트라 ”테스트는 버그가 있음을 보여줄 뿐, 버그가 없음을 보여줄 수는 없다.”

즉 프로그램이 잘못되었음을 증명할 수는 있지만, 프로그램이 맞다고 증명할 수는 없다.

소프트웨어 개발은 수학적인 시도가 아니라 오히려 과학과 같다.

최선을 다하더라도 올바르지 않음을 증명하는 데 실패함으로써 올바름을 보여주기 때문이다.

결론

구조적 프로그래밍에서 반증 가능한 단위를 만들어 낼 수 있는 능력 때문에 오늘날까지 가치가 있다.

아키텍처 관점에서는 기능적 분해를 최고의 실천법 중 하나로 여기는 이유기도 하다.

모든 수준에서 소프트웨어는 과학과 같고 따라서 반증 가능성에 의해 주도된다.

소프트웨어 아키텍트는 모듈, 컴포넌트, 서비스가 쉽게 반증 가능하도록(테스트하기 쉽도록) 만들기 위해 노력해야 한다.

 

 

👩🏻‍💻 구조적 프로그래밍 장을 읽으면서 증명으로 프로그램을 증명하려고 시도했다는 대목을 읽었을 때는 와? 너무 힘들 것 같은데? 가능은 한 건가? 싶었는데... 입증을 해내는 방법에 수학만이 아니라 과학이 있어서 다행이라는 생각을 하게 될 줄이야... 그리고 테스트가 왜 중요한지 이런 식으로 깨닫게 될 줄이야.... 과학적인 접근으로 인해 테스트를 많이 할 수밖에 없는 구조로 흘러가고 있는 프로그래밍 세상이 신선하고 흥미로웠다. 수학적으로 입증이 가능했다면 테스트를 안 할 수도 있었겠네^^ 버그가 없는 프로그래밍 가능해지는 건가^,,^

 

5장 객체 지향 프로그래밍

좋은 아키텍처를 만드는 일은 객체지향설계원칙을 이해하고 응용하는 데서 출발한다.

OO란 무엇인가?

데이터와 함수의 조합

실제 세계를 모델링하는 새로운 방법

OO는 캡슐화, 상속, 다형성 세 가지 개념을 적절하게 조합한 것, 또는 최소한 세 가지 요소를 반드시 지원해야 한다.

캡슐화

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

캡슐화를 통해 데이터와 함수가 응집력 있게 구성된 집단을 서로 구분 짓는 선을 그을 수 있다. 따라서 구분선 바깥에서는 데이터가 은닉되고, 일부 함수만이 외부에 노출된다.

하지만, 캡슐화 개념이 OO만 가능한 것은 아니다. C언어에서도 완벽한 캡슐화가 가능하다. 데이터 구조와 함수를 헤더 파일에 선언하고, 구현파일에서 이들을 구현한다. 그리고 프로그램 사용자는 구현 파일에 작성된 항목에 대해서는 접근할 수 없기 때문에 캡슐화가 가능하다.

OO가 강력한 캡슐화에 의존한다는 것은 옳지 못하다.

심지어 C언어에서 누려왔던 완벽한 캡슐화를 OO언어는 약화시켜 온 것은 틀림없는 사실이다.

상속

OO 언어가 상속만큼은 확실히 제공했다?

상속이란 단순히 어떤 변수와 함수를 하나의 유효 범위로 묶어서 재정의하는 일에 불과하다.

사실상 OO언어가 있기 훨씬 이전에도 C 프로그래머는 손수 구현할 수 있었다.

struct Point{
	double x, y;
};

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

struct NamedPoint* org;
struct NamedPoint* upperR;

printf("distance=%f\\n",distance((struct Point*)org, (struct Point*) upperR);

눈속임처럼 보이는 이 방식은 OO가 출현하기 이전에 흔히 사용하던 기법이며, 실제로 C++에서는 이 방법을 이용해 단일 상속을 구현했다.

따라서, OO언어가 완전히 새로운 개념을 만들진 못했지만, 데이터 구조에 가면을 씌우는 일을 편리한 방식으로 제공했다고 볼 수 있다.

다형성

OO언어가 나오기 이전부터 함수를 가리키는 포인터를 응용하여 다형성을 사용했었다.

OO언어는 실수할 위험이 적게 다형성을 사용할 수 있게 제공한다.

OO는 제어흐름을 간접적으로 전환하는 규칙을 부과한다고 결론 지을 수 있다.

다형성이 가진 힘

OO의 등장으로 언제 어디서든 플러그인 아키텍처를 적용할 수 있게 되었다. (서로 의존하지 않음)

의존성 역전

소스 코드 의존성의 방향은 반드시 제어 프름을 따르게 된다. 하지만, 다형성이 끼어들면 의존성역전이 일어난다. 소스 의존성(상속관계)이 제어흐름과는 반대가 되게 된다.

이러한 접근법을 사용하여, 소프트웨어 아키텍트는 시스템의 소스 코드 의존성 전부에 대해 방향을 결정할 수 있는 절대적인 권한을 갖는다. 즉 소스코드 의존성이 제어 흐름의 방향과 일치되도록 제한되지 않는다.

소스코드의존성을 원하는 방향으로 설정할 수 있다.

이것이 바로 OO가 제공하는 힘이다.

👩🏻‍💻 똑똑한 C 프로그래머는 모든 할 수 있겠다는 생각이 든다. 모든 oop의 개념을 C로 구현할 수 있다는 것이 일차적으로 놀랐고 oop에서는 쉽고 단순하게 제공한다는 사실이 다행임을 한번 더 느낀다.

 

결론

OO란, 다형성을 이용하여 전체 시스템의 모든 소스코드 의존성에 대한 절대적인 제어 권한을 획득할 수 있는 능력이다.

모듈의 독립성을 보장할 수 있다.

 

6장 함수형 프로그래밍

정수를 제곱하기

public class Squint{
	public static void main(String args[]){
		for(int i=0; i<25; i++)
			System.out.println(i*i)
	}
}
(println (take 25 (map (fn [x] (* x x)) (range))))

자바 프로그램은 가변 변수(mutable variable)를 사용하지만, 함수형 언어에서 변수는 변경되지 않는다.

불변성 아키텍처

경합(race)조건, 교착상태(deadlock)조건, 동시 업데이트(concurrent update)문제가 모두 가변 변수로 인해 발생하는 문제들이다. 즉 변수가 갱신되지 않으면 위의 문제가 일어나지 않는다. 락(Lack)이 가변적이지 않다면 교착상태도 잃어나지 않는다.

불변성이 정말로 실현 가능할까?

가변성의 분리

불변성과 관련한 가장 주요한 타협중 하나는 애플리케이션, 또는 애플리케이션 내부의 서비스를 가변 컴포넌트와 불변 컴포넌트로 분리하는 일이다.

트랜잭션 메모리와 같은 실천법을 사용해 동시업데이트와 경합 조건 문제로부터 가변 변수를 보호한다.

이벤트 소싱

이벤트 소싱은 상태가 아닌 트랜젝션을 저장하는 전략이다. 상태가 필요해지면 단순히 상태의 시작점부터 모든 트랜잭션을 처리한다.

저장공간이 많이 필요하겠지만, 테라바이트도 작다고 여기는 시대인데 저장공간은 문제가 아니다.

데이터 저장소에서 삭제되거나 변경되는 것이 하나도 없다는 사실이 더 중요하다. 따라서 업데이트 문제도 없다.

저장공간과 처리 능력이 충분하다면 어플리케이션에 완전한 불변성을 갖도록 만들 수 있고, 따라서 완전한 함수형으로 만들 수 있다.

결론

  • 구조적 프로그래밍은 제어 흐름의 직접적인 전환에 부과되는 규율이다.
  • 객체 지향 프로그래밍은 제어 흐름의 간접적인 전환에 부과되는 규율이다.
  • 함수형 프로그래밍은 변수 할당에 부과되는 규율이다.

세 패러다임 모두 우리에게서 무엇인가를 앗아갔다. 코드를 작성하는 방식의 형태를 한정시킨다. 우리의 권한이나 능력에 무언가를 보태지는 않는다.

지난 반세기 동안 우리가 배운 것은 해서는 안되는 것에 대해서이다.

소프트웨어, 즉 컴퓨터 프로그램은 순차, 분기, 반복, 참조로 구성된다. 그 이상도 이하도 아니다.

 

👩🏻‍💻 언어를 익숙하게 사용하려고만 하였지, 왜 생겨났고 어떤 패러다임을 가지고 있는지를 깊이있게 생각해본적이 없었는데, 생각하지 못한 관점에서 개념을 잡아줘서 좋았다.

반응형

댓글