본문 바로가기

Book

프로그래밍 패러다임

클린 아키텍처를 읽다가 2부에서 프로그래밍 패러다임들에 대한 간략한 설명이 나오는데 근본적인 물음에 대한 답들을 해주는 부분이 있어서 정리를 했다.

3장 패러다임 개요

크게 3가지 패러다임을 간략하게 소개함. 구조적 프로그래밍, 객체 지향 프로그래밍, 함수형 프로그래밍. 이 외의 패러다임은 없고 다시는 나오지 않을 거라 함. 각 패러다임을 한 줄로 정리하면 다음과 같음

  • 구조적 프로그래밍: 제어흐름의 직접적인 전환에 대해 규칙을 부과한다
  • 객체지향 프로그래밍: 제어흐름의 간접적인 전환에 대해 규칙을 부과한다
  • 함수형 프로그래밍: 할당문에 대해 규칙을 부과한다.
    구체적으로는 다음 장들에서 하나씩 다룸. 그럼 왜 이 패러다임들 외에는 다른건 나오지 않을 거라고 했나? 각 패러다임을 보면 프로그래머에게 제약을 걸면서 패러다임을 만듦. 구조적 프로그래밍은 goto 문을 제한하고, 객체 지향은 함수 포인터, 함수형 프로그래밍은 할당문을 앗아감. 이거 말고 더 제한할 게 있나? 없음. 그래서 이후에 나올 수도 없고 있다고 해도 이 패러다임을 기반으로 크게 바뀔 수 없음. 그리고 강조할 점은. 패러다임은 뭔가를 추가하는 게 아니라. 프로그래머에게 제약을 두는 방식으로 탄생했다는 것임.

4장 구조적 프로그래밍

이 패러다임은 다익스트라가 고안해낸 방식임. 처음 생겨난 이유는 다익스트라가 작성한 모든 프로그램을 수학적인 방식으로 증명하는 과정에서 생겨남. 공리, 정리, 따름 정리, 보조정리로 구성되는 유클리드 계층구조를 토대로 같은 방식으로 증명하려 했음. 근데 하는 과정에서 goto 문장이 모듈을 더 작은 단위로 재귀적으로 분해하는 과정에 방해가 되는 경우가 있다는 사실을 발견함. 좋은 사용방식은 우리가 현재 쓰는 if/then/else 와 do/while 문 같은 분기와 반복이라는 단순한 제어 구조에서 사용되는 경우였음. 그래서 goto를 금지하고 위의 방식만으로 사용함. 이러한 제어 구조는 순차 실행과 결합했을 때 특별함. 모듈을 증명 가능하게 하는 바로 그 제어 구조가 모든 프로그램을 만들 수 있는 제어 구조의 최소 집합과 동일하다는 사실이었음. 아무튼 이런 내용을 주장하고. 추후에 많은 언어에 받아들여짐. 다만 모듈 하나하나를 수학적으로 증명하려는 시도는 현실에서 필수처럼 받아들여지지 않음. 생산성 때문이라고 봄. 대신 과학적 방법으로 프로그램의 올바름을 입증하는 방식을 따르게 됨.

과학이 구출하다

과학은 근본적으로 수학과는 다른점이, 올바름을 절대 증명할 수 없다는 것에 있음. 그니까 우리가 아는 뉴턴 법칙도 특정계에서만 통함. 현재 양자역학에서는 다르게 적용됨. 이거처럼 과학적 이론은 언젠가는 잘못되었음이 밝혀질 가능성이 항상 열려있음.
과학은 서술된 내용이 사실임을 증명하는 방식이 아니라 서술이 틀렸음을 증명하는 방식으로 동작함.

각고의 노력으로도 반례를 들 수 없는 서술이 있다면 목표에 부합할 만큼은 참이라고 본다.


결론적으로 수학은 증명 가능한 서술이 참임을 입증하는 원리라고 볼 수 있지만, 과학은 증명 가능한 서술이 거짓임을 입증하는 원리라고 볼 수 있음.

테스트

위의 내용을 보면서 생각난게 있었는데, 그게 '테스트' 였음. 여기서도 이걸 이야기함. 다익스트라는 다음과 같이 말함 '테스트는 버그가 있음을 보여줄 뿐, 버그가 없음을 보여줄 수는 없다.' 그니까 잘못됨을 증명할 수는 있지만 맞다고 증명하진 않음.

사실 정말 우리가 모르는 예외는 존재할 수 있다. 테스트가 보장하는 것은 프로그램이 목표에 부합할 만큼은 충분히 참이라고 여길 수 있게 해주는 것이 전부임


바로 위 문장을 읽었을때 소름이 돋았다. 사실 별거 아니라고 할 수 있지만, 테스트를 작성할 때나 예외를 생각할 때 위의 문장을 계속 되새긴다면 훨씬 유용한 테스트를 작성할 수 있을 거 같다. 또한 Via negativa가 생각이 났다. 결국 세상 일들은 다 비슷하게 원칙에 의해 돌아가는 거 같다.

결론

구조적 프로그래밍이 가치 있는 이유는 프로그래밍에서 반증 가능한 단위를 만들어 낼 수 있는 능력때문이라고 한다.

작은 기능에서부터 가장 큰 컴포넌트에 이르기까지 모든 수준에서 소프트웨어는 과학과 같고, 따라서 반증 가능성에 의해 주도된다고 한다

. 그래서 결론적으로 아키텍트는 모듈,컴포넌트, 서비스가 쉽게 반증 가능하도록(테스트하기 쉽도록) 만들기 위해 분주한 노력을 해야 한다고 한다.

테스트 작성에 대한 근본적인 이유를 알게 된거 같기도 하고, 구조적 프로그래밍과 프로그래밍의 근본에 과학적 검증이 있다는 게 흥미로웠음.

5장 객체지향 프로그래밍

보통 OO이 무엇인가라는 질문에, 여러 답변이 있다. '데이터와 함수의 조합' , '실제 세계를 모델링하는 새로운 방법' , '캡슐화, 상속, 다형성 , 3가지 개념을 잘 구현하고 조합한 것'. 앞의 두 답변은 오히려 더 추상적이고, 마지막 답변은 왜 사용하는지에 대한 답변을 하지는 못한다. 그리고 이 파트에서 캡슐화, 상속, 다형성을 객체지향 언어들이 잘 구현하는지를 보여준다.

캡슐화

먼저 캡슐화 부분인데, 결론적으로는 내가 잘못알고 있었다. 오히려 현대 객체 지향 언어들은 캡슐화를 위반한다. c언어 같은 경우 헤더와 소스코드를 구분해서 헤더에 전방선언을 통해 이 소스를 사용하는 다른 모듈에서 내부의 다른 요소를 알지 못한 채로 사용하게 됨. 근데 오히려 현대 언어들은 아예 클래서 내부에 private 방식으로 클래스선언과 정의를 같이 작성함. 이게 사실 건들지 못하게 강제를 하지만. 외부에서 알 수는 있음. 엄밀한 캡슐화는 아니라고 함.

상속

상속 또한 구조체를 인터페이스 처럼 구현해서 c에서도 구현 가능함. 다만 이 부분은 확실히 객체지향 언어들이 편하게 제공한다고 함.

다형성

마지막으로 다형성은 저자가 주요하게 보는 부분임. 다형성 역시 c언어로 충분히 구현 가능함. 책 예시에서 입출력 드라이버에 대한 내용이 나옴. 유닉스 운영체제의 경우 모든 입출력 장치 드라이버가 다섯 가지 표준 함수를 제공할 것을 요구함. open, close, read, write, seek, 이 있음. FILE 데이터 구조는 이들 함수를 가리키는 포인터들을 포함함. 그래서 내부적으로 STDIN 이 단순히 저 함수를 정의한 구조체 형태로 선언해서 함수 포인터가 가리키는 함수를 호출하면 됨. 말하려는 요지는 함수를 가리키는 포인터를 응용한 것이 다형성이라는 것이다. 근데 이 함수에 대한 포인터를 직접 사용하는 게 위험함. 준수해야 하는 관례가 있는데, 이런 부분은 문제 생길 위험이 있음. 따라서 이런 부분 없이 편하게 사용하게 해 준 게 OO가 제공하는 다형성임.

다형성이 가진 힘

복사 프로그램 예제를 들어보면, 새로운 입출력 장치가 생긴다면, 기존 소스코드를 변경할 필요가 없음.이유는 복사 프로그램의 소스 코드가 입출력 드라이버의 소스코드에 직접 의존하지 않음. 그니까 아까 이야기한 FILE 인터페이스에 정의된 5가지 함수를 입출력 드라이버가 구현하면. 복사 프로그램은 어차피 같은 인터페이스를 사용하니 그냥 사용하 됨. 이게 의존성 역전 부분임. 플러그인처럼 사용됨. 이걸 플러그인 아키텍처라고 한다고 함. 예전에 이런 거 없이 개발자들이 작성했다고 함. 근데 기술이 발전하면서 이전 코드를 계속 다시 사용할 수 없게 되니까. 이렇게 각 입출력 장치의 독립성을 지원하는 방향으로 변경됨. (예전 천공카드부터 자기 테이프에 도달하면서 이 장치들에 의존하지 않으면서 코드들을 사용할 수 있게 구조를 바꾸는 게 좋다고 생각함)

의존성 역전

위의 얘기가 결국 의존성 역전임. 원래 전형적인 흐름은 main이 고수준 함수를 호출하고 다시 그게 저수준 함수들을 호출하고, 이렇게 의존성이 제어흐름을 따르게 됨. 하지만 인터페이스를 사용하게 되면 직접적으로 호출하지 않으니까. 제어흐름의 방향과 일치되도록 제한하지 않음.
저자는 이 부분이 OO의 힘이고 지향하는 것이라고 한다. 결론적으로 소스코드를 이런식으로 독립성을 보장하고, 시스템의 컴포넌트들도 독립성이 보장되면 배포도 독립성이 보장할 수 있고, 팀단위로 독립적으로 개발이 가능해짐.

결론

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

6장 함수형 프로그래밍

함수형 프로그래밍이라는 개념은 프로그래밍 그 자체보다 앞서 등장함.

불변성과 아키텍쳐

아키텍처를 고려할 때 변수의 불변성은 고려 대상일 수밖에 없음. race condition, dead lock, concurrent update 문제등이 모두 가변 변수로 인해 발생하기 때문. 어떠한 변수도 갱신되지 않는 다면 경합 조건이나 동시 업데이트 문제가 일어나지 않음.
다시 말해서 우리가 동시성 애플리케이션에서 마주치는 모든 문제, 즉 다수의 스레드와 프로세스를 사용하는 애플리케이션에서 마주치는 모든 문제는 가변 변수가 없다면 절대로 생기지 않음.
그렇다면 불변성이 정말로 실행가능한가? 저장공간이 무한하고 프로세서의 속도가 무한히 빠르다면 가능함. 자원이 무한대가 아니라면 약간의 타협이 필요함.

가변성의 분리

불변성과 관련하여 가장 주요한 타협 중 하나는 애플리케이션 또는 애플리케이션 내부의 가변 컴포넌트와 불변 컴포넌트로 분리하는 일임. 불변 컴포넌트가 순수 함수형 컴포넌트가 아닌 다른 컴포넌트와 통신함.
상태 변경은 컴포넌트를 갖가지 동시성 문제를 노출하는 꼴이므로, 흔히 트랜잭션 메모리와 같은 실천법을 사용하여 동시 업데이트와 경합 조건 문제로부터 가변 변수를 보호함. 여기서는 atom 변수를 사용해서 업데이트하는 클로저 언어 예시를 들었음.
요지는 애플리케이션을 제대로 구조화하려면 변수를 변경하는 컴포넌트와 변경하지 않는 컴포넌트를 분리해야 한다는 것임. 이렇게 분리하려면 가변 변수들을 보호하는 적절한 수단을 동원해 뒷받침해야 함.

이벤트 소싱

저장 공간과 처리 능력의 한계는 급격히 사라지고 있다고 함. 메모리가 더 커질수록, 기계가 더 빨라질수록 필요한 가변 상태는 더 적어짐.
간단한 예시로 고객의 계좌 잔고를 관리하는 은행 애플리케이션을 생각해 보면, 원래 트랜잭션처리를 함. 근데 만약에 계좌 잔고를 변경하는 대신 트랜잭션 자체를 저장한다고 가정해 봄, 누군가 잔고 조회를 하면 그때마다 계좌 개설 시점부터 발생한 모든 트랜잭션을 단순히 더함. 이렇게 하면 가변 변수가 필요가 없음.
당연히 이건 현실적이진 않음. 시간이 지날수록 트랜잭션의 수는 끝없이 증가하고, 컴퓨팅 자원은 늘어날 거임. 하지만 이 전략이 영원히 동작하도록 만들 필요는 없음. 애플리케이션의 수명주기 동안만 문제없이 동작할 정도의 저장공간과 처리 능력만 있으면 충분함.
이벤트 소싱에 깔려 있는 기본 발상이 이것이라고 함. 이벤트 소싱은 상태가 아닌 트랜잭션을 저장하자는 전력임. 상태가 필요해지면 단순히 상태의 시작점부터 모든 트랜잭션을 처리함.
중요한 점은 데이터 저장소에서 삭제되거나 변경되는 것이 하나도 없다는 사실임. 결과적으로 애플리케이션은 CRUD가 아니라 그저 CR만 수행함. 또한 변경 삭제 전혀 발생하지 않으니 동시 업데이트 문제도 없음. 저장 공간과 처리 능력이 충분하면 애플리케이션이 완전한 불변성을 갖도록 만들 수 있고, 완전한 함수형으로 만들 수 있음.
우리가 아는 버전관리 시스템이 이걸 기반으로 동작함. 그리고 생각해 보면 로그도 그대로 전부 남기는데, 자원만 많으면 좋다고 생각이 든다.

결론

패러다임은 우리에게 뭔가를 추가하기보단, 앗아갔음. 결론적으로 앨런튜링이 최초로 코드를 작성할 때 사용한 소프트웨어 규칙과 지금의 소프트웨어 규칙은 조금도 다르지 않다고 한다.
소프트웨어, 즉 컴퓨터 프로그램은 순차, 분기, 반복, 참초로 구성된다.

 

참고 및 출처: <클린 아키텍처> (로버트 C.마틴지음, 송준이옮김, 인사이트 , 2019)

'Book' 카테고리의 다른 글

정책과 수준 그리고 업무 규칙  (0) 2024.01.19
컴포넌트 응집도, 결합  (1) 2023.12.31
계층형 설계 1,2  (2) 2023.12.04
의존성 주입(그리고 부트스트래핑)  (0) 2023.11.16
명령-질의 책임 분리(CQRS)  (0) 2023.11.03