컴퓨터 프로그래밍 패러다임을 분류하는 여러 기준이 있다. 구조와 비구조, 값과 함수, 흐름과 이벤트, 스칼라와 배열 등 현재 필자도 모르는 다양한 기준 속에서 해당 기준을 선택한 이유는 최종 목적지가 Reactive이기 때문이다. 목적지를 이해하기 위한 수단으로 공부를 이어가는 중이니 다른 분류는 나~중에 다루는 것으로 하겠다.
그리고 이런 프로그래밍 패러다임이 무자르듯, N극과 S극처럼 명확하게 분리되는 경험이 아니다. 명령형을 선언형처럼 사용하는 법이 있고 선언형에는 명령형으로 작성된 베이스가 있다. 절차적 패러다임에는 함수형의 모습이 들어있고 또 구분되는 특징이 있기도 하다. 객체지향의 내부에는 프로시저가 있으며 또 명령형으로 분류되지만 선언형으로 구현할 수 있다. 그리고 이에 따라 다양한 패러다임이 섞여서 존재하게 된다. 패러다임을 공부하는 이유는 전의 글처럼 생각하는 방식과 관점을 배우기 위해서다. 무엇이 어디에 들어가느냐 보다 각 패러다임이 중점적으로 제안하는 관점이 무엇인지에 집중하며 공부를 이어가자.
대부분의 컴퓨터 하드웨어는 명령형으로 구현된다. 거의 모든 하드웨어들이 컴퓨터 고유 언어인 기계어를 실행하도록 되어 있는데, 이것이 명령형으로 쓰여 있다. low-level에서 프로그램의 상태는 메모리의 내용으로 정의되고 구문은 기계어의 명령어로 정의된다. 따라서 컴퓨터 프로그래밍에서 명령형 프로그래밍을 제외할 수 없는 기본적이고 전통적인 방식이라 할 수 있다.
요리 레시피를 찾아보자. 각 순서마다 재료(상태)를 어떤 조리법(구문)으로 어떻게 만들어 낼지(변경)를 단계별로 명시해놓았다. 이 과정을 그대로 완수했을 때 하나의 완성된 요리를 얻을 수 있다. 공정도 마찬가지다. 각각의 단계에 지시 사항들이 있고 그 단게를 지날 때마다 상태는 현실 세계에 반영된다. 지하철에서 외국인에게 길을 찾아주는 경우도 생각해보자. 어디로 갈지, 어디서 환승할지를 단계별로 알려준다. 각 단계별로 새로운 상태가 업데이트되고 그 상태에서 새로운 작업을 하게 된다.
func double(arr: [Int]) {
var results = []
for i in 0..<arr.count {
results.append(arr[i] * 2)
}
return results
}
우리는 arr의 모든 요소를 더블링하는 함수에서 result라는 상태(위에서 언급했듯 상태는 메모리의 내용이다. high-level에 가면 변수가 한편 상태를 일부 표현할 수 있다)가 반복문이라는 구문에 의해 변경되는 연산 과정을 보고 있다. 어떻게 변경되는지를 보여주며, 컴퓨터가 수행할 명령들이 나열되어 있다. 이것을 명령형 프로그래밍이라고 한다.
어떻게 보면 아주 간단한 방식의 프로그래밍 패러다임이다. 1도 모르는 컴퓨터에게 일을 시키는 방식은 그 친구가 뭘해야 하는지 하나하나 알려주는 것만큼 좋은게 없기 때문이다. 아주 low-level에서는 필수불가결한 요소이고 우리가 제작할 프로그램에서도 반드시 필요한 방식이다.
하지만 분명한 한계가 존재한다. 먼저 직관성이 떨어진다. 간단한 것과 직관적인 것은 구분해야 한다. 위의 반복문의 경우 너무 간단하기 때문에 직관적이지만 저 단순한 명령을 내밷는 작업이 많아지면, 직관적이지 않다. 이는 코드의 가독성 뿐만 아니라 기술부채 전체를 늘리는 꼴이 된다.
두 번째는 상태를 계속 변경시키기 때문에 추적이 용이하지 않다. results라는 변수가 프로그램을 완성시키기 위한 다른 작업에도 같이 이용되어야 한다면 그리고 그 이용하는 작업이 굉장히 많아진다면 과연 누가 results가 올바르게 작동한다고 확신할 수 있을까? 상태를 컨텍스트에 독립적으로 유지하지 않고 컨텍스트에 종속되게 만드는 것이 명령형의 큰 단점이다. 이 역시 쓰면 쓸수록 기술부채를 늘린다.
이 문제를 해결하는 방법은 사실 아주 간단하다. 바로 '추상화'하는 것이다. 이는 다음 선언형 프로그래밍 패러다임을 이해하는 초석이 된다.