[독서] Clean Architecture - 2부
2부_ 프로그래밍 패러다임
패러다임이란, 프로그래밍을 하는 방법으로 보통 언어에 독립적이다.
패러다임은 언제 어떤 프로그래밍 구조를 사용할지 결정하며 세가지 종류가 있다.
3장 : 패러다임 개요
패러다임은 다음의 세 가지가 존재한다.
- 구조적 프로그래밍
- 객체지향 프로그래밍
- 함수형 프로그래밍
구조적 프로그래밍
제어흐름의 직접적인 전환에 대해 규칙을 부과한다.
goto등의 무분별한 점프문을 if/then/else, do/while/until 등의 익숙한 구조로 대체했다.
객체 지향 프로그래밍
제어프름의 간접적인 전환에 대해 규칙을 부과한다.
알골 언어의 함수 호출 스택 프레임을 힙으로 옮기면 함수 호출이 반환된 이후에도 함수 내부의 지역변수가 오랫동안 유지될 수 있음을 발견했고, 이러한 함수는 클래스의 생성자가 되었다.
지역변수 -> 인스턴스 변수, 중첩 함수 -> 메서드가 되었다.
함수 포인터를 특정 규칙에 따라 사용하는 과정으로 인해 다형성이 등장한다.
함수형 프로그래밍
할당문에 대해 규칙을 부과한다.
람다계산법에 영향을 받아 만들어진 것으로, 람다계산법은 불변성을 기초로 한다. 즉, 심볼의 값이 변경되지 않는다는 개념이다.
즉, 함수형 언어에는 할당문이 전혀 없고, 할당하기 위해서는 까다로운 조건을 만족해야 한다.
각 패러다임은 프로그래머에게서 권한을 박탈한다. 즉, 무엇을 해서는 안되는 지를 말해준다.
세 패러다임은 모두 아키텍쳐와 관계가 있다.
아키텍처 경계를 넘나들기 위해 다형성을 이용한다. 함수형 프로그래밍을 통해 데이터의 위치와 접근 방법에 대해 규칙을 부과하며, 모듈의 기반 알고리즘으로 구조적 프로그래밍을 사용한다.
아키텍쳐의 세가지 관심사 - 함수, 컴포넌트 분리, 데이터 관리
4장 : 구조적 프로그래밍
모듈을 증명 가능하게 하는 제어구조는 모든 프로그램을 만들 수 있는 제어 구조의 최소 집합과 동일하다. 즉, 순자, 분기, 반복이라는 세 가지 구조이다.
열거법을 통해 순차 구문을 입증하는 기법은 순차 구문의 입력을 출력까지 수학적으로 출력한다.
분기 : 열거법을 재적용해 분기를 통한 각 경로를 열거하고 결과가 올바르다면 증명에 성공한다.
반복 : 귀납법을 사용해 1의 경우가 올바를 때 N의 경우가 올바름을 가정하고 N+1의 올바름을 증명한다.
현재 goto문은 거의 사용되지 않는다. 이는 제어 흐름을 제약 없이 직접 전환할 수 있는 선택권이 사라졌음을 의미한다. 우리는 모두 구조적 프로그래머이다.
구조적 프로그래밍을 통해 우리는 모듈을 기능적으로 분해할 수 있게 되었다. 프로그래머는 대규모 시스템을 모듈과 컴포넌트로 나눌 수 있고, 각각을 다시 작은 기능으로 세분화 할 수 있다.
그러나 수학적 증명은 너무 힘들기 때문에 과학적 방법을 사용한다. 서술된 내용이 사실임을 증명하는 것이 아니라 서술이 틀렸음을 증명하는 방식으로 동작한다. 즉, 테스트를 통해서 프로그램이 맞다는 것을 증명할 수는 없다. 목표에 부합할 만큼은 참이라고 보장할 뿐이다.
구조적 프로그래밍은 프로그램을 증명 가능한 세부 기능 집합으로 재귀적으로 분해하도록 한다. 그리고 테스트를 통해 기능들이 거짓임을 증명하려 시도한다.
소프트웨어 아키텍트는 모듈, 컴포넌트, 서비스가 테스트하기 쉽도록 만들어야 한다. 이를 위해서는 제한적인 규칙들을 받아들여야 한다.
5장 : 객체 지향 프로그래밍
OO란 무엇인가?
캡슐화
OO는 데이터와 함수를 쉽고 효과적으로 캡슐화 하는 방법을 제공한다. 그러나 C언어에서도 완벽한 캡슐화가 가능하다. 오히려 많은 OO에서는 완벽한 캡슐화가 불가능하다. 사용자가 멤버변수의 형태를 알게되면 캡슐화가 깨진다. 예를 들어, 멤버변수의 이름이 바뀌면 파일을 다시 컴파일해야 한다.
대부분의 OO는 캡슐화를 강제하지 않는다.
OO 프로그래밍은 프로그래머가 캡슐화된 데이터를 위회해서 사용하지 않을 것이라는 믿음을 기반으로 한다.
상속
상속이란 어떤 변수와 함수를 하나의 유효범위로 묶어 재정의하는 것으로, 이는 C언어에서도 손수 구현할 수 있다. 그러나 OO는 상속을 더 편리하게 한다.
다형성
C에서도 다형성을 표현할 수 있다. 다형성은 포인터를 응용한 것이다.
하지만 OO는 다형성을 더 안전하고 편리하게 사용할 수 있도록 해준다.
프로그래머가 수동으로 지켜야 하는 관례를 없애 실수 위험을 줄여주었다. 즉, 제어흐름을 간접적으로 전환하는 규칙을 부과한다.
다형성을 사용하면 함수의 세부 소스코드를 알지 못한채 함수를 사용할 수 있다. 즉 연결된 드라이버만 바꿔주면 사용자는 소스코드를 변경할 필요가 전혀 없다. 이러한 형태는 플러그인 아키텍쳐이다. 즉, 프로그램은 장치 독립적이어야 한다.
의존성 역전
전형적 호출트리에서 소스코드 의존성의 방향은 반드시 제어 흐름을 따른다. 그러나 다형성이 적용된다면 의존성이 제어흐름에 반대로 작용할 수 있다. 이를 의존성 역전이라고 부른다.
OO언어가 다형성을 안전하고 편리하게 제공한다는 것은 소스코드 의존성을 어디에서든 역전시킬 수 있다는 뜻이기도 하다.
즉, 어디서든 시스템의 소스 코드 의존성 방향을 원하는 방향으로 설정 가능하다.
예를 들어 UI/데이터 베이스가 업무 규칙의 플러그인이 된다는 것은 업무 규칙의 소스코드에서는 UI/데이터베이스를 호출하지 않는다는 뜻이다. 따라서 업무규칙, UI, 데이터베이스는 세가지로 분리된 단위로 컴파일할 수 있고 이 단위들의 의존성은 소스코드 사이의 의존성과 같다. 즉, 업무규칙 컴포넌트는 UI/데이터베이스의 컴포넌트에 독립적이다. 이는 UI/데이터베이스에 변경이 일어나도 업무규칙에는 영향을 미치지 않는다는 것을 의미한다. 이것이 배포독립성이다.
배포독립성은 각 모듈을 독립적으로 개발할 수 있게 만든다. 이것이 바로 개발 독립성이다.
OO를 사용하면 고수준의 정책을 포함하는 모듈은 저수준의 세부사항을 포함하는 모듈에 대해 독립성을 보장할 수 있다. 저수준의 세부사항은 플러그인 모듈로 중요도가 낮게 만들고, 고수준의 정책을 포함하는 모듈과 독립적으로 개발/배포할 수 있다.
6장 : 함수형 프로그래밍
함수형 프로그램은 람다 계산법을 핵심으로 한다.
자바 프로그램에서는 가변 변수를 사용하며, 가변 변수는 프로그램의 실행 중에 상태가 변할 수 있다.
하지만 클로저 프로그램에서는 변수가 한 번 초기화 되면 절대로 변하지 않는다.
즉, 함수형 언어에서 변수는 변경되지 않는다.
변수의 가변성이 사라진다는 것은 무엇을 의미하는가?
경합조건(race condition), 교착상태 조건(deadlock), 동시 업데이트(concurrent update) 문제들은 모두 가변 변수로 인해 발생하기 때문이다.
(사족 : 최근 공부하는 verilog에서 다루는 얘기가 여기서 모두 나온것이 놀랍다. 가만 생각해보면 거기서도 값을 assign하려고 할 때 이러한 문제가 발생했던 것 같다..! 왠지 역시 어떤 일이든 동작을 작성한다는 점에서 비슷한 문제가 일어나게 되는 것 같다. 신기하다.)
즉, 우리가 동시성 어플리케이션, 다중 스레드와 프로세스를 사용하는 어플리케이션에서 마주치는 모든 문제는 가변 변수가 없다면 절대로 생기지 않는다.
아키텍트라면 동시성(concurrency)문제에 지대한 관심을 가져야만 한다.
자원이 무한대가 아니라면 불변성은 약간의 타협을 해야 실현 가능하다.
가변성의 분리
어플리케이션 혹은 어플리케이션 내의 서비스를 가변 컴포넌트와 불변 컴포넌트로 분리해야 한다.
불변 컴포넌트에서는 순수하게 함수형 방식으로만 작업이 처리되고, 가변 변수는 전혀 사용되지 않는다.
불변 컴포넌트는 변수의 상태 변경이 가능한 다른 컴포넌트와 통신한다.
상태 변경을 통해 일어나는 다양한 동시성 문제를 방지하기 위해 흔히 사용하는 실천법이 트랜잭션 메모리다.
트랜잭션 메모리는 트랜잭션을 사용하거나, 재시도 기법을 통해 이들 변수를 보호한다.
예시로 클로저의 atom 기능이 있다.
1
2
(def counter (atom 0)) ; counter를 0으로 초기화한다.
(swap! counter inc) ; counter를 안전하게 증가시킨다.
이 코드에서 atom의 값을 변경하기 위해서는 반드시 swap!함수를 사용해야 한다는 제약이 있다.
swap은 변경할 atom변수와 저장할 값을 계산할 함수를 인자로 받아 atom변수를 계산한 값으로 변경시킨다.
… 불변 컴포넌트인 계산 로직을 inc로 분리함으로써 가변변수인 atom을 보호했다고 말하고 싶은 것 같다.
가능한 한 많은 처리를 불변 컴포넌트로 옮기고 가변 컴포넌트에는 최소한의 코드만 남겨놓아야 한다.
이벤트 소싱
고객의 계좌 정보를 관리하는 어플리케이션을 예로 들면, 입금 트랜잭션, 출금 트랜잭션이 존재할 것이다.
이 때 고객의 잔고를 직접 변경하지 않고 잔고 조회 요청마다 지금까지 있었던 모든 트랜잭션결과를 더해 반환하는 방식으로 구현한다면 가변변수는 하나도 필요하지 않다.
이러한 전략을 어플리케이션의 수명 주기 동안만 문제없이 동작할 정도의 자원을 사용하도록 한다면?
이런 발상에서 나온 것이 바로 이벤트 소싱(event sourcing)이다.
이벤트 소싱은 상태가 아닌 트랜잭션을 저장한다. 상태가 필요해지면 상태의 시작점부토의 모든 트랜잭션을 처리한다. 혹은, 일정 주기마다 상태를 계산해 저장하는 방법을 사용할 수도 있다. 예를 들어, 매일 자정마다 상태를 저장한다거나.
이벤트 소싱 방식을 사용하면 CRUD에서 CR만을 사용한다. 즉, 데이터 저장소에서 삭제되거나 변경되는 것이 없다. 따라서 저장공간이 충분하다는 전제하에 완전한 함수형으로 만들 수 있다.
소스코드 버전관리 시스템이 이런 방식을 사용하고 있다.
요약
- 구조적 프로그래밍 : 제어 흐름의 직접적인 전환에 부과되는 규율
- 객체 지향 프로그래밍 : 제어흐름의 간접적인 전환에 부과되는 규율
- 함수형 프로그래밍 : 변수 할당에 부과되는 규율 소프트웨어, 컴퓨터 프로그램은 순차, 분기, 반복, 참조로 구성된다.
Comments powered by Disqus.