제공 :
한빛 네트워크
저자 : Brian MacDonald
역자 : 박정근
원문 :
Should You Start Programming with a Procedural Language?
절차적 언어에서 함수형 언어로 넘어가는 것은 왜 외발자전거를 배우는 것과 비슷할까?
나는 독자들에게 "
어떤 언어를 먼저 배워야 할까?" 에 대해 의견을 물어보았다. 내 스스로도 추천 의견을 남겨보았고, 사람들도 댓글을 통해 열띤 토론을 벌였다. 여러 가지 프로그래밍 언어를 접해본 사람이라면, 언어가 가진 프로그래밍 스타일이나 패러다임의 차이에 대해 들어봤을 것이다. 이러한 용어는 헷갈릴 수 있는데, 더군다나 프로그램을 처음 접하는 사람들에겐 이해하기가 어려운 개념일 것이다. 혼란스러운 것은 많은 언어가 딱 하나의 패러다임에 속하는 것도 아니고, 때로는 패러다임 자체가 바뀌거나, 서로 넘나들 수도 있다는 것이다.
순서와 제어: 절차적 프로그래밍
절차적 프로그래밍은 명령형 프로그래밍이라고도 불리며, 프로그램의 시작부터 종료까지 수행할 내용을 순서대로 나열하는 특징이 있다. 이 프로그래밍은 반복해서 돌려야 하는 개별 작업에 특히 알맞으며, 특정한 기능의 집합을 수행하는 함수에 제어권을 넘겨 수행하고, 그 결과를 받아오게 된다. 절차적 언어에는 여러 가지가 있는데 BASIC, Pascal, C언어가 대표적이다.
아래는 C언어로 된 간단한 코드 예제이다.
int add(int a, int b);
int main(){
int firstNum = 6;
int secondNum = 15;
int sum;
sum = add(firstNum,secondNum);
printf("sum= ",sum);
return 0;
}
int add(int a,int b){
int result;
result = a + b;
return result;
}
예제에는 firstNum, secondNum 두 개의 변수가 있고, 각 변수는 숫자형으로 선언되고 할당되어 있다. 다른 변수로는 sum이 있는데, add() 함수의 호출 결과가 할당된다. 프로그램의 제어 흐름은 add() 함수로 넘어가서 두 개의 숫자가 합산되어 그 결과가 반환되고, 다시 원래의 위치로 돌아와서 결과를 출력한 후 종료된다. 예제에는 다른 내용도 있지만, 여기서는 전체적인 개념만 잡기 바란다.
초보자에게 있어 절차적 프로그래밍의 장점은 약간의 구문만 알면, 코드를 따라가면서 어떤 내용이 수행되는지 파악할 수 있다는 것이다.
데이터가 프로그램이다: 함수형 프로그래밍
함수형 프로그래밍은 이름이 내포하듯, 함수를 강조하는데, 절차적 프로그래밍보다 함수에 중점을 두는 방식이다. 함수형 프로그래밍에서 각 함수의 출력 값은 다음 함수의 입력 값으로 연결되어, 함수의 작동은 입력 값에 의해 결정된다. 절차적 프로그래밍이 "어떻게" 처리하고 싶은지를 기술하는 것이라면, 기능적 프로그램은 "무엇"을 처리하고 싶은지를 기술하는 것이라고들 한다. 기능적 프로그래밍에서 데이터(종종 프로그램의 상태로 정의되기도 함)는 바뀌지 않는다. 변수도 없으며 오로지 인풋과 아웃풋만이 존재할 뿐이다. 프로그램의 상태 변화는 부수적 효과로 여겨지는데, 이것이 프로그램에 해가 되는지 아니면 일을 할 때 유용한지에 대해서는 답을 하는 사람에 따라 의견이 분분하다. 함수형 프로그래밍 언어로는 Common LISP(리스프), Haskell(하스켈), Erlang(얼랑), F# 언어가 있다.
함수형 언어의 장점이 드러나는 분야는 리스트 처리인데, 재귀 호출(recursion)이 탁월하기 때문이다. 아래는 얼랑(Erlang) 언어로 리스트에 들어있는 항목을 정렬하는 방법을 보여준다. 리스트 정렬에는 여러 가지 기법이 있는데, 여기에 제시된 방법은 퀵 소트(QuickSort)라고 하는 것이다. 퀵 소트 자체는 이 글에서는 그리 중요하진 않다.
sort([Pivot|T]) ->
sort([ X || X <- T, X < Pivot]) ++
[Pivot] ++
sort([ X || X = Pivot]);
sort([]) -> [].
이 함수에서 X는 리스트를 의미한다. 꼭 숫자로 된 리스트일 필요는 없다. 단어든 가격이든 동물이든, 프로그래머가 리스트에 포함된 항목을 다른 항목과 어떻게 비교할 것인지만 정의할 수 있으면 된다. 퀵 소트 알고리즘에서는 리스트 내 한 개의 항목을 기준 값(Pivot)으로 정하게 되는데, 예제 코드에서는 첫 번째 항목이 기준 값이 된다. [Pivot|T]는 첫 번 째 항목이 기준 값이며, 그 뒤에 따라오는 리스트의 나머지 부분이 T라는 뜻이다. 함수의 나머지 부분에서는 기준 값보다 작은 모든 항목의 정렬된 서브 리스트, 그 뒤에는 기준 값, 기준 값보다 크거나 같은 요소의 정렬된 서브 리스트를 출력 값으로 정의하고 있다. 여기까지는 이해하기 순조로운데, 그렇다면 서브 리스트는 어떻게 정렬할까? 간단하다. 정렬 함수를 또 호출하면 된다. 그러면, 서브 리스트는 다시 서브-서브-리스트들로 나눠지고, 서브 리스트에 속한 요소가 하나가 남을 때까지 각 리스트를 차례로 정렬 함수로 돌리면 된다. 이렇게 함수에서 함수를 호출하는 과정이 모두 수행되면, 완전히 정렬된 리스트가 결과물로 나오는데, 이 과정을 재귀 호출이라고 부른다. 함수형 언어의 다른 특징으로는 함수에서 다른 함수를 정의할 수 있다는 것인데, 재귀 호출은 함수 내부에 그 함수 자체가 정의되어 있는 특수한 형태이다.
함수형 프로그래밍 예제는 코드가 매우 짧다. 하지만, 절차적 프로그래밍 예제에 비해 상당히 복잡한 일을 하고 있으며, 사람이 읽고 이해하기가 다소 어려워 보인다. 사실, 방금 설명한 재귀호출에 대한 설명을 읽고 머리가 아파온다면, 그리 이상한 일은 아니다. 함수형 프로그래밍을 익히기 위해서는, 많은 훈련이 필요하며, 절차적 프로그래밍과는 다른 사고방식이 필요하다. 절차적 프로그래밍이 일직선으로 나아가는 방식이라면, 함수형 프로그래밍은 나선형으로 진행되는 방식이다.
부품 조립: 객체 지향 프로그래밍
객체 지향 언어는 객체를 사용하여 현실 세계의 모델링을 추구한다. 객체(Object)는 "데이터"와 "함수" 두 부분으로 구성되는데, 이들은 각각, 멤버와 메소드라고도 불린다. 멤버와 메소드를 구분하는 고전적인 방법은 멤버는 "객체가 가지고 있는 것", 메소드는 "객체가 행하는 것"으로 구분하는 것이다. 예를 들어, "고양이"라는 객체가 있다면, 색깔이나 크기는 데이터 멤버가 되며, "소리를 내다 - purr()"와 "먹다 - eat()"는 고양이 객체의 메소드가 된다.
객체 지향 프로그래밍의 주된 차이점은 데이터와 메소드가 동일한 객체에서 본질적으로 서로 묶여서 프로그램 전반에 걸쳐 사용된다는 것이다. 어떤 객체는 다른 객체를 입력 값으로 받아들여, 서로 다른 객체 상에서도 본연의 기능이 작동될 수 있다. 객체 지향 프로그램의 제어 흐름을 읽어나가는 것은 보통 직관적이지는 않다. 객체 지향 언어의 예로는 C++, Java, C# 언어를 들 수 있다.
아래는 C++로 작성된 간단한 예제이다.
class Cat
{
public:
Cat(int initialAge);
int GetAge() const;
void SetAge(int age);
void Meow();
private:
int catAge;
char * catName;
};
Cat::Cat(int initialAge)
{
catAge = initialAge;
catName = new char[10];
}
int Cat::GetAge()
{
return catAge;
}
void Cat::SetAge(int newAge)
{
catAge = newAge;
}
void Cat::Meow()
{
cout << "Meow!n";
}
int main()
{
Cat Fluffy(5);
Fluffy.Meow();
cout << "Fluffy is " << Fluffy.GetAge() << " years old.n";
return 0;
}
이 간단한 코드만 보더라도, 앞의 두 예제보다 훨씬 많은 내용이 코딩되어 있다. 어떻게 보면, 이는 객체지향의 본질이다. 객체 지향 언어는 모델링이 필요한데, 모델링이 빠진다면 마치 절차적 언어처럼 보일 것이다. 예제 코드의 대부분은 Cat 클래스의 모델을 정의하는데 사용되었다. 객체 지향 프로그래밍을 하게 되면, 종종 다른 사람이 작성한 클래스를 라이브러리로 사용할 수 있어서, 작성자 자신의 클래스를 굳이 만들지 않아도 된다. 사실, 대체로 라이브러리 내부에서 코드가 어떻게 돌아가는지를 정확히 몰라도, 접근하고자 하는 함수와 데이터만 알면, 라이브러리 사용이 가능하다.
이 예제가 수행하는 모든 내용은 main() 함수에 들어있는데, 이 함수에서는 Fluffy라고 명명된 cat 객체 하나가 생성되고, Meow() 함수가 호출된 다음, 다시 멤버 변수 접근을 위해 GetAge() 함수가 사용되었다. 이 코드의 흐름을 따라가려면, main() 함수와 클래스 정의부에 있는 함수, 그리고 그 함수가 실제로 접근하는 데이터를 찾아, 소스 코드 안에서 앞으로 갔다, 뒤로 갔다를 반복하게 된다.
여기에 내가 제시한 세 개의 샘플 코드는 각자 다른 내용을 수행하고 있어서, 상호 비교하기가 어려울 것이다. 프로그래밍 언어는 애초에 서로 다른 목적으로 위해 만들어졌고, 그래서 원래의 용도를 벗어난 곳에 사용하게 되면, 어색한 코드가 나올 수 있다. 함수형 언어로 간단한 덧셈 함수를 작성할 수는 있지만, 작성된 코드는 딱히 함수형 언어 같이 보이지는 않을 것이다. 또, 퀵소트 알고리즘을 절차적 언어 혹은 객체 지향 언어로 구현할 수 있겠지만, 코드의 양이 상당히 증가할 뿐 아니라, 함수형 언어로 구현한 코드보다 깔끔해 보이지도 않을 것이다. 이것은 마치 망치와 모종삽을 비교하는 것과 같다. 둘 중 어떤 도구를 쓰든, 구덩이를 팔 수는 있지만, 둘 중에서는 모종삽이 훨씬 더 나을 것이다. 왜냐하면, 삽은 애초에 흙에 구덩이를 파기 위해 만들어졌기 때문이다.
어떤 언어로 시작해야 할까?
자, 그렇다면, 언어를 처음 배우는 사람에겐 어떤 종류의 언어가 제일 좋을까? 이 질문에 정답이 딱 하나만 있는 건 아니다. 모든 언어엔 각각의 장점과 단점이 있다. 많은 이들이 자신이 가장 좋아하는 언어가 제일 좋다고 목소리를 높이거나, 스스로에게 가장 익숙한 프로그래밍 스타일이 제일 좋다고 주장할 것이다. 이러한 논쟁은 인터넷에서 많이 벌어지고 있는데, 여기서 가장 많이 나오는 이야기는 대부분의 언어가 순수하게 딱 하나의 스타일만 가지고 있지 않다는 것이다. C++언어가 C언어로부터 출발한 것처럼, 대체로 많은 객체 지향 언어가 절차적 언어에서 진화했다. 많은 객체 지향 언어는 이제, 함수가 다른 함수의 인풋으로 사용되는 람다(lambda) 함수라고 하는 함수형 언어의 특징도 내포하고 있다. 그리고 함수형 언어도 절차적 프로그래밍 언어에 익숙한 사람들이 코드를 쉽게 읽을 수 있도록 도와주는 몇몇 기능(syntactic sugar)을 가지고 있다.
무슨 일을 하는지 알고 있다면?
만약 특정한 스타일의 프로그래밍이 대세인 분야에서 일을 하거나, 취업을 하고자 한다면, 그 분야에서 가장 많이 사용하는 언어를 배워야 할 것이다. 예를 들어, 이론 수학 분야에서는 함수형 언어를 배워야 할 것이다.
원점에서 출발
그런데, 만약 선택 사항이 많거나, 과거에 한 번도 프로그래밍 언어를 배운 적이 없는 사람들은 어떻게 해야 할까? 이럴 때 나는, 절차적 언어 또는 C++나 JavaScript 처럼 적어도 절차적 언어에 뿌리를 둔 객체 지향 언어를 먼저 배울 것을 권고한다. 누구나 처음 배우는 언어에 많은 영향을 받게 된다. 언어를 처음 배운 상태에서, 다른 언어를 배우고자 한다면 두 가지 선택권이 있다. 첫째, 새로 배우는 언어가 과거에 배웠던 언어와 비슷해서, 이미 알고 있는 내용을 새 언어에 모두 연관지을 수 있든지, 아니면 예전에 배웠던 내용을 다 잊어버리고, 새 언어가 지닌 개념들로 바꾸어야 할 것이다.
과거의 지식을 잊고, 새로운 지식을 받아들이는 것은 머리를 단련시키기는 좋은 훈련이며, 우리 모두 좀 더 많이 시도해야 할 가치가 있지만, 이를 위해 시간과 노력이 소요된다. 절차적 언어 프로그래밍은 그 자체로 많은 것들을 습득할 수 없을지는 몰라도, 다른 언어를 배우기 위해 좋은 출발점이다. 절차적 프로그래밍을 배우면, 함수의 기본인 반복문(Loops), 변수(Variables)와 더불어 제어 흐름(Control Flow)을 습득할 수 있으며, 객체를 구성하기 위한 기본 요소인 데이터 구조를 배울 수 있다. 이러한 기초 지식을 배우고 나면, 배운 것을 토대로 다른 패러다임의 언어로 확장해 나갈 수 있을 것이다.
일단 감을 잡으면, 추진력은 저절로 생긴다
절차적 언어를 먼저 배운다는 것은 보조 바퀴를 달고 자전거를 배우는 것과 꽤 비슷하다. 만약 너무 오랫동안 보조 바퀴를 달고 자전거를 타면, 보조 바퀴 없이 자전거를 타는 것을 못 배울 것이다. 절차적 언어에서 객체 지향 언어로 나아가는 것은 보조 바퀴를 떼는 것과 비슷하다. 처음에는 이상해서 몇 번 넘어질 수 밖에 없겠지만, 절차적 언어에서 배운 내용을 바탕으로 새 언어에 적응해 나갈 수 있다. 절차적 언어를 배운 후 함수형 언어를 배우는 것은 마치 외발자전거를 배우는 것과 비슷하다. 외부에서 보면 같은 것일지 몰라도, 외발자전거를 타기 위해서는 완전히 다른 사고 방식이 요구된다.
자전거를 탈 때 보조 바퀴를 사용하여 단계적으로 배워나가는 것은 좋지 않은 방법이며, 배우고자 하는 것을 곧바로 시작하는 게 좋다고 생각하는 사람들도 많다. 물론 이해가 되는 말이며, 그렇게 주장하는 사람들은 어쩌면 나보다 더 똑똑하거나 새로운 스킬을 더 잘 배울지도 모르겠다. 하지만, 나로서는 새로운 기술을 조금씩, 단계적으로 배우는 게 좋다. 그래서 절차적 프로그래밍 언어를 먼저 배운 것을 다행으로 생각한다.