클린아키텍처 2부: 프로그래밍 패러다임

Jihyun·2021년 12월 5일
0

이 글은 한달 한권 챌린지의 두번째 책, 클린아키텍처를 읽고 정리한 내용이다.


3장. 패러다임 개요


구조적 프로그래밍

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

최초로 적용된 패러다임으로, 1968년 에츠허르 비버 데이크스트라가 발견했다.

데이크스트라는 무분별한 점프(goto)는 프로그램 구조에 해롭다는 사실을 제시했다. 데이크스트라는 이러한 점프들을 분기(if, then, else)와 반복(do, while, until)과 같이 더 익숙한 구조로 대체했다.


객체 지향 프로그래밍

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

두 번째로 도입된 패러다임은 구조적 프로그래밍보다 사실 2년 앞선 1966년, 올레 요한 달과 크리스텐 니가드에 의해 등장했다.

이들 두 프로그래머는 알골(ALGOL) 언어의 함수 호출 스택 프레임(stack frame)을 힙(heap)으로 옮기면 함수 호출이 반환된 이후에도 함수에서 선언된 지역 변수가 오랫동안 유지될 수 있음을 발견했다. 바로 이러한 함수가 클래스의 생성자가 되었고, 지역 변수는 인스턴스 변수, 그리고 중첩 함수는 메서드가 되었다. 함수 포인터를 특정 규칙에 따라 사용하는 과정을 통해 필연적으로 다형성이 등장하게 되었다.


함수형 프로그래밍

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

세 번째 패러다임은 최근에 들어서야 겨우 도입되기 시작했지만, 세 패러다임 중 가장 먼저 만들어졌다. 사실 함수형 프로그래밍은 컴퓨터 프로그래밍 자체보다 먼저 등장했다. 알론조 처치는 어떤 수학적 문제를 해결하는 과정에서 람다(lambda) 계산법을 발명했는데, 함수형 프로그래밍은 이러한 연구 결과에 직접적인 영향을 받아 만들어졌다.

람다 계산법의 기초가 되는 개념은 불변성(immutability)으로, 심볼(symbol)의 값이 변경되지 않는다는 개념이다. 이는 함수형 언어에는 할당문이 전혀 없다는 뜻이기도 하다.


결론

패러다임은 무엇을 해야할지를 말하기보다는 무엇을 해서는 안 되는지를 말해준다.

세 가지 패러다임 각각은 우리에게서 goto문, 함수 포인터, 할당문을 빼앗아갔다. 우리에게서 더 빼앗을 수 있는 것은 없을 것이며, 최소한 부정적인 의도를 가진 패러다임으로는 이 세 가지가 전부일 것이다.

우리는 아키텍처 경계를 넘나들기 위한 메커니즘으로 다형성을 이용한다. 우리는 함수형 프로그래밍을 이용하여 데이터의 위치와 접근 방법에 대해 규칙을 부과한다. 우리는 모듈의 기반 알고리즘으로 구조적 프로그래밍을 사용한다.

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



4장. 구조적 프로그래밍


증명

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

데이크스트라는 수학자가 유클리드 계층구조를 사용하는 방식을 프로그래머도 사용할 수 있다고 믿었고, 단순한 알고리즘에 대해 기본 적인 증명을 작성할 수 있는 기법을 보여줘야 한다는 사실을 깨달았다. 이 연구를 진행하면서 goto 문장이 모듈을 더 작은 단위 로 재귀적으로 분해하는 과정에 방해가 되는 경우가 있다는 사실을 발견했으며, 증명이라는 수학적인 원리를 적용하여 순차 실행 및 분기, 반복이라는 두가지 제어구조가 올바르다는 것을 입증했다.

모듈을 증명 가능하게 하는 바로 그 제어구조가 모든 프로그램을 만들 수 있는 제어구조의 최소집합과 동일하다는 사실이다. 구조적 프로그래밍은 이렇게 탄생했다.


기능적 분해

구조적 프로그래밍을 통해 모듈을 증명 가능한 더 작은 단위로 재귀적으로 분해할 수 있게 되었고, 이는 결국 모듈을 기능적으로 분해할 수 있음을 뜻했다.

즉, 거대한 문제 기술서를 받더라도 문제를 고수준의 기능들로 분해할 수 있다. 그리고 이들 각 기능은 다시 저수준의 함수들로 분해할 수 있고, 이러한 분해 과정을 끝없이 반복할 수 있다. 게다가 이렇게 분해한 기능들은 구조적 프로그래밍의 제한된 제어 구조를 이용하여 표현할 수 있다.


테스트

테스트는 버그가 있음을 보여줄 뿐, 버그가 없음을 보여줄 수는 없다

프로그램이 잘못되었음을 테스트를 통해 증명할 수는 있지만, 프로그램이 맞다고 증명할 수는 없다. 테스트에 충분한 노력을 들였다면 테스트가 보장할 수 있는 것은 프로그램이 목표에 부합할 만큼은 충분히 참이라고 여길 수 있게 해주는 것이 전부다.


결론

구조적 프로그래밍이 오늘날까지 가치 있는 이유는 프로그래밍에서 반증가능한 단위를 만들어 낼 수 있는 바로 이 능력 때문이다. 또한 흔히 현대적 언어가 아무런 제약 없는 goto 문장은 지원하지 않는 이유이기도하다. 뿐만 아니라 아키텍처 관점에서는 기능적 분해를 최고의 실천법 중 하나로 여기는 이유이기도 하다.

가장 작은 기능에서부터 가장 큰 컴포넌트에 이르기까지 모든 수준에서 소프트웨어는 과학과 같고, 따라서 반증가능성에 의해 주도된다. 소프트웨어 아키텍트는 모듈, 컴포넌트, 서비스가 쉽게 반증 가능하도록(테스트하기 쉽도록) 만들기 위해 분주히 노력해야 한다. 이를 위해 구조적 프로그래밍과 유사한 제한적인 규칙들을 받아들여 활용해야 한다.



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

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

OO의 본질을 설명하기 위해 캡슐화(encapsulation) 상속(inheritance), 다형성(polymorphism)에 기대는 부류도 있는데, 이들은 OO가 이 세 가지 개념을 적절하게 조합한 것이거나, 또는 OO 언어는 최소한 세 가지 요소를 반드시 지원해야 한다고 말한다.


캡슐화

OO를 정의하는 요소 중 하나로 캡슐화를 언급하는 이유는 데이터와 함수를 쉽고 효과적으로 캡슐화하는 방법을 OO 언어가 제공하기 때문이다. 그리고 이를 통해 데이터와 함수가 응집력 있게 구성된 집단을 서로 구분 짓는 선을 그을 수 있다. 구분선 바깥에서 데이터는 은닉되고,일부 함수만이 외부에 노출된다. 이 개념들이 실제 OO 언어에서는 각각 클래스의 private 멤버 변수와 public 멤버 함수로 표현된다.

JAVA와 C#은 헤더와 구현체를 분리하는 방식을 모두 버렸고, 이로 인해 캡슐화는 더욱 심하게 훼손되었다. 이들 언어에서는 클래스 선언과 정의를 구분하는 게 아예 불가능하다. 이 때문에 OO가 강력한 캡슐화에 의존한다는 정의는 받아들이기 힘들다. 실제로 많은 OO 언어가 캡슐화를 거의 강제하지 않는다.

OO 프로그래밍은 프로그래머가 충분히 올바르게 행동함으로써 캡슐화된 데이터를 우회해서 사용하지 않을 거라는 믿음을 기반으로 한다. 하지만 OO를 제공한다고 주장한 언어들이 실제로는 C 언어에서 누렸던 완벽한 캡슐화를 약화시켜 온 것은 틀림없다.


상속

상속이란 단순히 어떤 변수와 함수를 하나의 유효범위로 묶어서 재정의하는 일에 불과하다. 사실상 OO언어가 있기 훨씬 이전에도 C 프로그래머는 언어의 도움 없이 상속을 흉내 내는 요령을 구현할 수있었다. 따라서 OO언어가 완전히 새로운 개념을 만들지는 못했지만, 데이터 구조에 가면을 씌우는 일을 상당히 편리한 방식으로 제공했다고 볼 수는 있다.


다형성

함수를 가리키는 포인터를 응용한 것이 다형성이다. OO 언어는 다형성을 제공하지는 못했지만,다형성을 좀 더 안전하고 더욱 편리하게 사용할 수 있게 해준다. OO 언어를 사용하면 다형성은 대수롭지 않은 일이 된다. 이러한 이유로 OO는 제어흐름을 간접적으로 전환하는 규칙을 부과한다고 결론지을 수 있다.

플러그인 아키텍처는 입출력 장치 독립성을 지원하기 위해 만들어졌고. 등장 이후 거의 모든 운영체제에서 구현되었다. 그럼에도 대다수의 프로그래머는 직접 작성하는 프로그램에서는 이러한 개념을 확장하여 적용하지 않았는데,함수를 가리키는 포인터를 사용하면 위험을 수반하기 때문이었다. OO의 등장으로 언제 어디서든 플러그인 아키텍처를 적용할 수 있게 되 었다.

OO언어가 다형성을 안전하고 편리하게 제공한다는 사실은 소스코드 의존성을 어디에서든 역전시킬 수 있다는 뜻이기도 하다.

이러한 접근법을 사용한다면, OO언어로 개발된 시스템을 다루는 소프트웨어 아키텍트는 시스템의 소스 코드 의존성 전부에 대해 방향을 결정할 수있는 절대적인 권한을 갖는다. 즉, 소스 코드 의존성이 제어흐름의 방향과 일치되도록 제한되지 않는다. 호출하는 모듈이든 아니면 호출 받는 모듈이든 관계없이 소프트웨어 아키텍트는 소스코드 의존성을 원하는 방향으로 설정 할 수 있다.


결론

OO 란 무엇인가? 소프트웨어 아키텍트 관점에서 정답은 명백하다. OO란 다형성을 이용하여 전체 시스템의 모든 소스 코드 의존성에 대한 절대적인 제어 권한 을 획득할 수 있는 능력이다.

OO를 사용하면 아키텍트는 플러그인 아키텍처를 구성할 수 있고, 이를 통해 고수준의 정책을 포함하는 모듈은 저수준의 세부사항을 포함하는 모듈에 대해 독립성을 보장할 수 있다. 저수준의 세부사항은 중요도가 낮은 플러그인 모듈로 만들 수 있고, 고수준의 정책을 포함하는 모듈과는 독립적으로 개발하고 배포할 수 있다.


6장. 함수형 프로그래밍

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


불변성과 아키텍처

아키텍처를 고려할 때 이러한 내용이 왜 중요한가? 아키텍트는 왜 변수의 가변성을 염려하는가?

경합(race)조건, 교착상태(deadlock) 조건, 동시 업데이트(concurrent update) 문제가 모두 가변 변수로 인해 발생하기 때문이다. 만약 어떠한 변수도 갱신되지 않는다면 경합 조건이나 동시 업데이트 문제가 일어나지 않는다. 락(lock)이 가변적이지 않다면 교착상태도 일어나지 않는다.

우리가 동시성 애플리케이션에서 마주치는 모든 문제, 즉 다수의 스레드와 프로세스를 사용하는 애플리케이션에서 마주치는 모든 문제는 가변 변수가 없다면 절대로 생기지 않는다.

아키텍트라면 동시성(concurrency)문제에 지대한 관심을 가져야만 한다. 우리는 스레드와 프로세스가 여러 개인 상황에서도 설계한 시스템이 여전히 강건하기를 바란다. 그렇다면 이제 불변성이 정말로 실현 가능한지를 스스로에게 반드시 물어봐야 한다.


가변성의 분리

불변성과 관련하여 가장 주요한 타협 중 하나는 애플리케이션, 또는 애플리케이션 내부의 서비스를 가변 컴포넌트와 불변 컴포넌트로 분리하는 일이다. 불변 컴포넌트에서는 순수하게 함수형 방식으로만 작업이 처리되며, 어떤 가변 변수도 사용되지 않는다. 불변 컴포넌트는 변수의 상태를 변경할 수 있는 하나 이상의 다른 컴포넌트와 서로 통신한다. 상태 변경은 컴포넌트를 갖가지 동시성 문제에 노출하는 꼴이므로, 흔히 트랜잭션 메모리(transactional memory)와 같은 실천법을 사용하여 동시 업데이트와 경합 조건 문제로부터 가변 변수를 보호한다.

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

현명한 아키텍트라면 가능한 한 많은 처리를 불변 컴포넌트로 옮겨야 하고, 가변 컴포넌트에서는 가능한 한 많은 코드를 빼내야 한다.

0개의 댓글