Eternity's Chit-Chat

aeternum.egloos.com



행위 매개변수화(Behavior Parameterization) - 3부 Software Design

언어의 추상화 레이어

map() 함수를 기반으로 map-square() 함수와 map-succ() 함수를 구현하는 방법은 다양한 추상화 수준의 계층(layer)에 따라 프로그램의 구성 요소를 배치하는 방법을 잘 보여준다.  Paul Graham은 “Programming Bottom-Up”이라는 글에서 프로그래밍 언어에 가까운 하위 수준의 추상화에서 출발해 문제 영역을 반영하는 상위 수준의 추상화를 구축하는 방식의 장점을 잘 설명하고 있다.

경험 많은 Lisp 프로그래머는 프로그램을 다른 방식으로 분할한다. 하향식(top-down) 설계 방식과 더불어 상향식(bottom-up) 설계 방식이라고 불릴 수 있는 원칙을 따른다. 상향식 설계 방식에서는 현재 해결하려고 하는 문제에 적합하도록 언어를 변경한다. Lisp에서는 프로그래밍 언어에 가깝게 프로그램을 작성하는 것이 아니라 문제에 가깝게 프로그램을 작성한다. 프로그램을 작성하면서 “Lisp이 이러이러한 연산자를 가졌으면 좋겠는데”라고 생각한다. 그리고는 원하는 연산자를 작성한다. 나중에 새로운 연산자를 사용함으로써 다른 부분의 설계가 단순해졌다는 것을 느끼게 된다. 언어와 프로그램은 함께 성숙한다. 분쟁 지역의 국경처럼 언어와 프로그램 간의 경계는 최종적으로 산과 강, 그리고 프로그램의 자연스러운 국경을 따르게 될 때까지 계속해서 다시 그려진다. 결국 프로그램을 위해 언어가 설계된 것처럼 보이게 될 것이다. 언어와 프로그램이 조화를 이룰 때 작은 크기의 명확하고 효율적인 코드를 얻게 된다.

- Paul Graham, Programming Bottom-Up

Steve McConnell 역시 “Code Complete”에서 언어 안에서의(in) 프로그래밍이 아닌 언어를 향한 (into) 프로그래밍 방식을 따를 것을 권하고 있다.

Sapir-Whorf 가설에 따르면 ‘특정한 개념을 생각할 수 있는 능력은 그러한 개념을 표현할 수 있는 단어를 알고 있는 지에 달려 있다’고 한다. 만약 그러한 단어들을 모른다면, 개념을 표현할 수 없을 뿐만 아니라 공식화할 수도 없을 것이다.

프로그래머는 이와 유사하게 언어에 의해서 영향을 받는다. 프로그래밍 개념을 표현하기 위해서 사용할 수 있는 프로그래밍 언어에서의 단어(명령어)가 프로그래밍 개념을 어떻게 표현할 것인지를 결정할 뿐만 아니라, 심지어는 어떠한 개념을 표현할 수 있는지 까지 결정할 것이다.

David Gries가 지적했듯이, 프로그래밍 도구가 프로그래밍에 대한 사고를 결정해서는 안 된다. Gries는 언어 안에서의(in) 프로그래밍과 언어를 향한(into) 프로그래밍을 구분하고 있다. 언어에서의(in) 프로그램을 작성하는 프로그래머는 자신의 사고를 언어가 지원하는 기능들로 제한해 버린다. 만약 언어 도구가 원시적이면 프로그래머의 생각도 원시적일 것이다.

언어를 향한(into) 프로그래밍을 작성하는 프로그래머는 표현하고자 하는 개념이 무엇인지 결정하고 난 다음, 언어가 제공하는 툴을 사용하여 그런 개념들을 어떻게 표현할 것인지 결정한다.

-  Steve McConnell, Code Complete 2nd Edition

Sapir-Whorf 의 말처럼 ‘특정한 개념을 생각할 수 있는 능력이 그러한 개념을 표현할 수 있는 단어를 알고 있는 지에 달려 있다’고 한다면 프로그램 상에서 필요한 개념을 표현하기 위해 먼저 그 개념을 표현할 수 있는 단어들을 구축하는 것이 우선일 것이다. 예제에서 map()은 단어를 구성하고 map-square()와 map-succ()는 상위 수준의 개념을 표현한다.

map() 함수는 리스트를 다른 리스트로 변환하기 위한 기본 빌딩 블록을 제공한다. map-square() 함수와 map-succ() 함수는 map()의 세부적인 구현 방법을 모르더라도 원하는 방식으로 리스트의 항목을 변환할 수 있다.

비록 여기에서 사용된 두 map 함수의 예는 개념으로부터 단어를 추출하는 방법을 따르고 있지만 빌딩 블록의 구축 방향과 무관하게 핵심적인 내용은 동일하다. 단어 수준의 재사용 가능한(그리고 중복을 제거한) 하위 수준의 추상화를 구축하고 이를 기반으로 좀 더 상위 수준의 개념을 구축함으로써 문제 영역의 언어를 향해 프로그래밍하는 것이다.

<그림 1> 추상화 수준에 따른 빌딩 블록의 계층(layer)

<그림 1>에서 하위 레이어의 map() 함수가 상위 레이어에서 전달된 익명 함수를 역으로 호출한다는 점에 주목하라. 이것은 map() 함수가 사용되는 컨텍스트에 종속적인 정보(여기에서는 리스트 항목의 변환 로직)를 상위 레이어로부터 하위 레이어로 전달함으로써 map() 함수를 특정한 상황으로 부터 분리한다. 즉, map() 함수를 다양한 컨텍스트에서 재사용 가능한 범용적인 함수로 만든다.

이처럼 하위 레이어의 빌딩 블록을 상위 레이어에 특화된 요소로부터 분리하는 것은 하위 레이어의 컨텍스트 독립성(context independence)을 보장함으로써 재사용성을 향상시킨다. 상위 레이어가 하위 레이어를 호출하고 다시 하위 레이어 내부에서 상위 레이어에서 전달된 프로시저를 호출하는 방법을 ‘콜백(callback)’이라고 부른다. 콜백은 고차 함수의 특성을 이용해(고차 함수가 지원되지 않는 경우에는 이를 모방해서) 행위를 매개변수화하는 일반적인 메커니즘을 가리킨다.

요약하면 map() 함수 예제를 통해 알 수 있는 것처럼 변하는 부분과 변하지 않는 부분을 분리하고 중복 코드를 제거함으로써 재사용 가능하고 유연한 코드를 낳을 뿐만 아니라 프로그램을 추상화의 경계에 따라 레이어 방식으로 구축할 수 있는 토대를 마련할 수 있다.

변경의 축에 따라 행위를 분리하는 것은 언어와 무관하게 모든 애플리케이션 설계 시에 적용 되어야 하는 기본 원칙이다. 따라서 절차적 언어나 객체지향 언어와 같이 다른 패러다임에 기반을 두고 있는 언어들 역시 고차-함수를 직접 지원하거나 고차-함수의 개념을 모방할 수 있는 유사한 기법들을 제공하고 있다. 이제 몇 가지 프로그래밍 언어를 살펴 보면서 고차-함수의 개념이 비함수형 패러다임 언어에서 어떤 형태로 지원되고 있는 지 알아 보기로 하자.