본문 바로가기
Books/클린 아키텍쳐

프로그래밍 패러다임 - 구조적, 객체 지향, 함수형 프로그래밍

by softserve 2023. 4. 27.
반응형

프로그래밍 패러다임

 

은 어떻게 코드를 작성할 것인가? 에 대한 답이라고 할 수 있습니다. 시대의 흐름에 따라 이 질문에 대한 대답은 계속 달라지고 있고, 그에 따라 새로운 언어와 도구가 인기를 끌고 대세로 등극하거나 점차 쇠퇴해 가기도 합니다. 프로그래밍 패러다임을 다루는 위키 문서에는 수십 가지의 패러다임을 소개하고 있고, 구글에 검색을 해보면 복잡한 계층도 그림이 제일 먼저 눈에 들어옵니다.

간단히 소개해 보자면 옛날에는 코드가 순차적으로 실행되는 절차 지향 프로그래밍으로 개발을 했지만 JAVA와 C++ 등과 함께 객체 지향이란 개념이 등장하면서 크게 판도가 바뀌었습니다. 이 절차 지향이니 객체 지향이니 하는 것은 '어떻게' 문제를 해결할 것이냐에 초점을 맞춘 명령형 프로그래밍이고, 나는 관점 자체를 '무엇'을 해결할 것이냐 로 달리하겠다고 선언한 것이 선언형 프로그래밍입니다. 이런 선언형 프로그래밍 패러다임의 한 부분이 함수형 프로그래밍입니다. 함수형 프로그래밍이 최근에 등장한 개념은 아니지만 몇 년 전부터 자바 등의 언어에서 람다식을 지원하기 시작하고, 함수형 프로그래밍을 사용하는 리액트 같은 것들이 많이 활용되면서 많이 주목을 받고 있는 것 같습니다.

이것들이 상충되는 개념은 아니고 언어별로 확연하게 구분이 되는 것도 아닙니다. 특별히 의식하지 않더라도 우리가 오늘 개발한 프로그램은 구조적이면서 객체 지향이기도 하고 어떤 면에서는 함수형 프로그래밍이기도 합니다. 패러다임이 시대에 따라 변하는 것은 맞지만 정답이 있는 것은 아니기에 상황에 맞게 적절히 사용하는 것이 필요합니다. 패러다임이란 것은 쉽게 말하면 개발 과정에서 어떤 문제점을 발견하고 그 해결책을 제시한 것이라고 할 수 있고 이 문제점과 해결 방법이라는 본질에 집중할 필요가 있다는 생각이 듭니다.

『클린 아키텍쳐』에서는 구조적 프로그래밍, 객체 지향 프로그래밍, 함수형 프로그래밍 세 가지만을 소개하고 있습니다. 또 이 3가지 외에 새로운 패러다임이 등장하지는 않을 것이라고 합니다. 흥미롭게도, 패러다임이란 곧 제약이기 때문입니다. 구조적 프로그래밍은 프로그래머에게서 goto문을 앗아갔고, 객체 지향 프로그래밍은 포인터를, 함수형 프로그래밍은 할당문을 빼앗아갔는데 이제 더 이상 빼앗길 것이 남지 않았다는 것이죠.

갑자기 패러다임을 소개하는 이유는 아키텍처의 관점에서 이런 패러다임이 시사하는 바가 있기 때문입니다. 그럼 지금부터 이 세 가지 패러다임에 대해서 살펴보도록 하겠습니다.

 

구조적 프로그래밍

 

프로그래밍을 할 때 가장 중요한 것은 실행의 흐름을 제어하는 것입니다. 지금이야 if문을 이용해 조건에 따라 분기 처리를 하고 for문과 같은 반복문을 통해서 특정 조건을 만족하는 동안 반복해서 실행을 하도록 하지만 옛날에는 goto문을 이용해서 프로그래머가 직접 몇 번째 줄을 실행해라 라는 명령을 내렸습니다. goto문의 사용은 당연하게도 많은 문제점을 가지고 있었는데 코드가 난잡해지고, 사람이 직접 흐름을 제어하다 보니 예상치 못한 오류가 자주 발생할 수 있다는 것이었죠. 이 goto문을 없애고 순차, 분기, 반복이라는 세 가지 제어구조만으로 개발하도록 한 것이 바로 구조적 프로그래밍입니다.

다익스트라 알고리즘으로 유명한 데이크스트라(Dijkstra)는 goto문의 해악을 주장하며 새로운 패러다임을 만들어낸 컴퓨터 과학자입니다. 데이크스트라는 프로그램의 완전성을 증명하고자 노력을 했는데, 이를 위해선 프로그램을 작은 단위로 분해할 수 있어야 했습니다. 하지만 goto문으로 만들어진 프로그램은 이렇게 분해하는 것이 힘들었죠. 단, 순차, 분기, 반복이라는 형태로 만들어진 프로그램은 기능적 분해가 가능했습니다. 사실 이 세 가지 제어구조만으로 모든 프로그램을 표현할 수 있었던 것이죠. 그리고 결과적으로 goto문은 사라지게 되었습니다.

비록 프로그램을 수학적으로 증명하는 것에는 실패했지만, 소프트웨어 개발은 수학적인 시도처럼 보일지라도 사실은 과학에 가깝다는 점에서 데이크스트라의 시도는 의미가 있습니다. 과학 이론은 옳다는 것을 입증할 수는 없으나 틀렸다는 반례를 들 수 없으면 받아들일 수밖에 없기 때문이죠.

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

테스트를 통해서 반증에 실패한다면 그 프로그램은 소기의 목적을 달성할 수 있다고 볼 수 있습니다. 그리고 이러한 증명은 틀렸음을 입증할 수 있는 프로그램에 대해서만 가능합니다. 구조적 프로그래밍은 프로그램을 증명 가능한 작은 단위로 분해할 수 있기 때문에 결과적으로 입증할 수 있는 프로그램을 만들게 됩니다. 우리는 구조적 프로그래밍의 규칙과 제약을 따라서 쉽게 분해하고 테스트할 수 있는 프로그램을 만들어야 하고 그러면 결과적으로 좋은 아키텍처를 가진 프로그램을 만들 수가 있습니다.

 

객체 지향 프로그래밍

 

객체 지향이 무엇이냐는 질문은 면접장에서도 자주 접할 수 있는 흔한 질문이지만, 그에 대한 답으로 널리 알려져 있는 것들ㅡ이를테면 '현실 세계의 객체를 모델링한 것'이라거나 '데이터와 메서드의 조합'이라고 하는 것들ㅡ에 대해 저자는 볼멘소리를 쏟아냅니다. 너무 모호하고 저런 설명을 들어도 객체 지향이 무엇인지 여전히 알 수 없다는 것이죠. (그러면서 정답이 뭔지는 알려주지 않습니다.)

다만, 객체 지향은 캡슐화와 상속, 다형성이라는 객체 지향의 특징을 만족하는 방법이라고 할 수 있습니다. 이런 것들은 객체 지향 언어가 등장하기 전까지 불가능했던 것도 아니고 객체 지향이 새롭게 만들어낸 개념도 아닙니다. C언어로는 오히려 더 완벽한 캡슐화가 가능했습니다. 객체 지향으로 상속이 보다 편리해진 것은 맞지만 높은 점수를 주기는 어렵습니다. 다형성 역시 함수 포인터가 제공해 주던 것에 불과하지만 객체 지향을 통해 더 안전하고 쉽게 사용할 수 있게 되었다는 점에서는 의미가 있습니다. 아키텍처의 관점에서 볼 때 가장 중요한 특성은 다형성, 그중에서도 의존성 역전입니다.

예를 들어 A 클래스에서 B 클래스 또는 인터페이스를 사용할 경우 A는 B에 의존한다고 표현하고 이걸 소스 코드 의존성(상속)이라고 합니다. B가 없으면 A도 작동을 하지 않기 때문입니다. 기존에는 제어 흐름과 소스 코드 의존성이 정확히 일치했습니다. main 함수에서 고수준 함수를 호출하고 각 함수에서 다시 저수준 함수를 호출하는 제어 흐름 속에서 main 함수는 고수준 함수에 의존하고, 고수준 함수는 다시 저수준 함수에 의존하게 됩니다.

그런데 이 사이에 다형성이 끼어들면 의존성 역전 dependency inversion이 발생하게 됩니다.

아래 그림과 같이 상위 모듈에서 하위 모듈의 함수를 호출하는 제어 흐름에서 소스 코드는 인터페이스를 통해 함수를 호출하기 때문에 의존성이 제어 흐름과는 반대로 되는 것이죠.

 

 

다형성은 의존성을 어디에서나 역전시킬 수 있는 권한을 부여합니다. 의존성에 대한 제어 권한을 가지게 되면, 각 모듈을 독립적으로 개발하고 배포할 수 있게 됩니다. 바로 이것이 아키텍트의 관점에서 봤을 때의 객체 지향의 이점이라고 할 수 있습니다.

 

함수형 프로그래밍

 

함수형 언어의 중요한 특징 중의 하나는 변수의 값이 변하지 않는다는 것입니다.

아래는 1부터 5까지의 정수의 제곱을 출력하는 프로그램입니다.

function square() {
    var i;
    for(i = 1; i <= 5; i++) {
        console.log(i*i);
    }
}

square();

가변 변수 i의 값이 1부터 5까지 변하면서 i를 제곱한 결과를 출력합니다.

똑같은 프로그램을 함수형으로 만들면 이렇게 됩니다.

const range = (i) => Array(i).fill().map((v,i) => i+1)
const square = (v) => { console.log(v*v) }

let arr = range(5)
arr.map(square)

가변 변수를 이용해 반복하는 대신 연속해서 증가하는 숫자 배열을 만드는 range() 함수와 제곱을 출력하는 square() 함수, 그리고 map() 을 이용해서 결과를 출력합니다.

가변 변수는 동시성과 관련하여 경합 조건, 교착 상태, 동시 업데이트 등 많은 문제를 초래합니다. 하지만 완벽한 불변성을 실현하는 것은 어려운 일이므로 약간의 타협이 필요합니다.

하나는 가변 컴포넌트와 불변 컴포넌트를 분리하는 방법입니다.

다른 하나는 이벤트 소싱입니다. 이벤트 소싱이란 상태를 변화시키는 트랜잭션 또는 이벤트를 모두 저장해두었다가 상태가 필요할 때 저장된 트랜잭션들을 모두 실행하여 상태를 얻는 방법을 말합니다. 물론 트랜잭션의 양이 늘어날수록 저장 공간과 처리 능력의 한계를 경험할 가능성이 높아지겠지만 관련 기술의 발전 속도를 생각하면 이것도 유용한 방법이 될 수 있습니다.

 

구조적 프로그래밍은 제어 흐름의 직접적 전환에 부과되는 규율이다.
객체 지향 프로그램은 제어 흐름이 간접적 전환에 부과되는 규율이다.
함수형 프로그래밍은 변수 할당에 부과되는 규율이다.
...
소프트웨어는 예나 지금이나 순차, 분기, 반복, 참조로 구성될 뿐 그 이상도 그 이하도 아니다.
- 본문

 

 

 

반응형

'Books > 클린 아키텍쳐' 카테고리의 다른 글

클린 아키텍쳐 - 소개  (1) 2023.04.06

댓글