동적바인딩과 정적바인딩
프로그래밍언어론 카테고리를 새로 만든 기념으로 가상함수 포스팅에서 잠깐 다루었던 동적바인딩에 대해 좀더 자세하게 정리를 해 보려고 한다. 사실 정적바인딩과 동적바인딩은 프로그래밍언어론 과목의 가장 중요한(그리고 어려운) 주제 중 하나다. 언어의 개념과 구현을 아우를 수 있는 주제라서 개인적으로 좋아하는 주제이기도 하다.
먼저 바인딩이 무엇인지 정의해 보자. 바인딩은 사전적 의미로는 무언가 두 가지가 결합되는 것을 의미하기도 하고 미정(undefined, undetermined) 상태의 어떤 것이 정의 또는 결정되는 것을 얘기하기도 한다. 프로그래밍언어론에서는 이 여러 가지 의미를 다 사용하고 있지만 일단은 뒤의 개념처럼 컴파일과 실행 과정에서 어떤 것이 결정되는 것을 바인딩이라고 할 수 있다.
좀더 넓은 의미로 우리가 소프트웨어를 만들 때 언어를 결정하고 개발환경(컴파일러)을 결정하고 실행될 타겟을 결정하는 것도 바인딩이라고 볼 수 있다. 그 단계마다 어떤 것들이 결정된다. 언어에 따라 사용할 수 있는 라이브러리나 타입, 함수들이 달라지고 컴파일러에 따라 어떤 것은 오류가 나기도 하고 안 나기도 한다. 또한 기계어 코드가 나오는 방식도 달라진다. 또한 타겟 머신에 따라 어떤 기계어 코드가 나올 것인가가 결정된다. 타겟 머신은 주소 방식, 명령어의 배치 방식, 함수 호출 방식, 프로그램 실행 방식 등을 결정한다. 이렇게 최종 소프트웨어가 설치되고 운용되기까지 많은 것들이 결정되어 가는 과정을 바인딩으로 설명할 수도 있다.
그러나 여기서는 좁은 의미로 소스 프로그램이 실행되기까지의 과정에서 코드의 의미와 동작이 결정되는 과정을 바인딩이라고 보자. 그랬을 때 우리는 큰 틀에서 컴파일 과정에서 결정되는 것들과 실행하면서 결정되는 것으로 나누어 볼 수 있고 그것이 정적바인딩과 동적바인딩이 나타내는 개념이다. 보통은 컴파일과 링크 단계까지 거쳐서 컴퓨터가 바로 실행가능한 상태의 명령문 코드로 바뀌기 까지를 정적(static)이라고 하고 프로그램의 실행단계(로드, 시작, 연산, 함수호출, 입출력 등)에서 정해지는 것을 동적(dynamic)이라고 한다.
정적바인딩은 컴파일하면서 결정되는 것인데, 컴파일러는 우리가 짜준 코드를 기계어 코드로 바꾸면서 많은 것을 결정한다. 변수의 주소, 데이터의 타입과 그에 따른 연산, 명령문의 종류 등이 결정된다. 즉 프로그램이 의미하는 바를 해석하여 컴퓨터의 관점에서 이해할 수 있는 방식으로 바꾸어간다. 이것은 마치 우리가 일을 할 때 업무 지시를 해석하여 어떤 순서로 어떻게 일을 처리할지를 결정하는 것과 비슷하다. 지시 사항에 정확히 명시되지 않은 것들을 나름으로 구체화하고(실행자의 재량이라고 판단되는 것) 협의나 결과를 제공할 대상이 분명치 않으면 하나의 대상으로 결정하고 대드라인 안에서 어떤 순서와 일정으로 진행할지를 결정할 것이다. 컴파일러는 우리 프로그램을 작업지시서로 보고 그것을 컴퓨터가 이해하고 작업할 수 있는 능력에 맞게 해석하여 애매하거나 정해지지 않은 부분(컴파일러의 재량이라고 판단되는 것)을 나름의 방식으로 결정하고자 노력한다. 이것이 정적바인딩이다.
여기서 컴파일러는 결정되지 못한 것들이 있으면 두 가지 방식으로 처리한다. 우선 반드시 정해져야 일을 할 수 있는 것들이 미정상태라면 오류를 일으킨다. 또는 지시사항이 앞뒤 모순이 있거나 틀린 부분이 있어도 오류를 일으켜 문제가 무엇인지를 자기가 이해한 선에서 정리하여 알려준다. 이것이 컴파일 오류다. 한편 지시사항에 분명하게 정해지지 않았으나 나중에 결정되어야 할 것들이라면 진행 도중에 그것을 결정하고(사용자의 입력에 의해서든 실행환경에 의해서든) 그에 따라 동작할 수 있게 기계어 코드를 생성해야 한다. 이 과정이 동적바인딩이라고 할 수 있다. 즉 나중에 결정될 수 밖에 없는 일이라면 실행하면서 결정 또는 확인하여 그에 맞게 실행할 수 있게 코드가 만들어져야 한다.
그럼 구체적으로 어떤 것이 동적바인딩되는지 먼저 살펴보자. 컴파일러가 정할 수 없는 것 중에는 많은 부분이 실행시에 사용자의 입력에 의해 결정되는 것들이다. 예를 들어 변수의 값이 대표적인 것이다. 입력된 값에 따라 다음 동작이 결정된다면 프로그램을 실행하기 전에는 알 수 없다. 그리고 그 변수의 값에 의해 결정되는 것들은 모두 동적바인딩이다. 배열의 크기, 함수 호출 여부와 순서, 프로그램이 어느 부분 코드를 실행할지 여부 등이 결정된다. 또 하나 동적으로 결정되는 것은 실행환경에 의해 실행 중에 주어지는 것들이다. new에 의해 할당된 메모리의 주소, 실제 변수나 코드가 메모리의 어느 부분에 자리를 잡게 될지(실제 주소, physical address), 그리고 어떤 레지스터를 쓸지, 캐싱될지 여부, 스택의 최대 크기(예외가 발생할지 여부) 등은 모두 실행시에 OS 또는 실행환경에 의해 결정된다. 한 가지 경우가 더 있다면 환경에 의해 결정되는 값이 있다. 위치 정보, 시간 정보, 사용할 언어 등이 환경에 의해 정해지는 값이다. (예를 들어 랜덤 값은 시간 정보를 시드로 사용한다. 또 밤이냐 낮이냐에 따라 다르게 동작하는 경우도 있다.)
그런데 이러한 기본적인 프로그램의 동작 방식은 폰노이만 아키텍처의 기계에서는 컴퓨터 초기부터 동일했다. 동적바인딩이 중요해지는 이유는 이러한 것들 이외에 언어에서 의도적으로 나중에 결정될 여지를 남기는 경우다. 즉 프로그램(작업지시서)에서 반드시 결정하지 않아도 나중에 정해질 수 있게 언어를 설계한 것이다. 그 이유는 프로그램 작성을 쉽고 효율적으로 할 수 있게 하기 위해서다. 예를 들면 업캐스팅과 가상함수가 그런 대표적인 예다. 언어에서는 상속의 한 매카니즘으로 의도적으로 변수가 참조하는 객체의 타입이 딱 한가지가 아니라 여러 클래스 중 하나가 될 수 있게 했고 가상함수를 통해 메소드 호출이 정확히 어떤 코드를 실행할 것인지를 실행시에 결정하는 것이다. 가상함수의 예에서 본 것처럼 "intro()" 메소드는 객체가 무엇인가에 따라 다른 동작을 한다. 그러나 어쨌거나 주관하는 사람의 입장에서는 참석자를 차례로 "소개"하라고 하면 된다.
우리는 실제 업무의 수행에서 많은 경우 업무지시서에 너무 많은 것을 꼬치꼬치 기술하고 싶어하지 않는다. 상사들이 가장 많이 하는 얘기가 "업무에 능동적으로 유연하게 대응할 수 있는" 인재를 원한다고 한다. 우리는 컴퓨터에게 프로그램을 짜 줄 때 너무 많은 것을 일일이 시시콜콜 지정하고 싶지 않다. 이러한 세부사항의 기술을 가능하면 적게 해주고 컴퓨터가 알아서 하게 하는 것이 현대적 프로그래밍 언어가 바라는 방향이다. 동적바인딩은 그런 가능성을 열어주는 것으로 점점 더 많은 언어에서 점점 더 많은 부분을 정하지 않고 컴파일러가 알아서 하길 바라고 그 결과 컴파일러는 실행하면서 그때그때 상황에 맞추어 작업할 수 있게 코드를 만들게 된다. 물론 실행 프로그램의 효율성은 떨어질 수 밖에 없다. 그러나 프로그래밍 언어들은 개발자의 노력과 시간, 그리고 인간이 해낼 수 있는 더 많은 가능성을 택하고 대신에 약간의 효율성의 손해를 감수하는 방향으로 가고 있다. 그러기 위해서는 컴파일러가 똑똑해지고 실행환경이 점점더 거대해 지고 코드는 점점더 간결해 진다.
자바 언어에서 동적바인딩을 도입한 예는 우선 배열이나 모든 것을 new를 통해 heap에서 생성하는 것을 들 수 있다. 이것은 실행 환경의 개방성(이식성)을 위해서이기도 하고 객체 참조의 업캐스팅을 통해 코드에 유연성을 주는 방법이기도 하다. 즉 Object를 이용해 모든 객체를 동일하게 취급할 수 있다는 것이 자바 언어의 중요한 특징 중 하나다. 그러나 구현 관점에서는 이러한 동적바인딩 때문에 참조 객체를 읽어오기 위해서 기계어 코드에서 일단 주소를 읽어오고 그 주소로 해당 값을 다시 한번 읽어와야 한다. 즉 메모리 IO가 두번이나 발생해야 한다. 이것은 CPU 입장에서는 어마어마한 시간의 손실을 가져온다. (로드지연이 컴파일러와 CPU에게 얼마나 큰 근심거리인지를 안다면 이것이 자바 언어가 C 언어보다 느린 주요한 이유 중 하나임을 이해할 것이다) 한편 가상함수의 동적바인딩도 그러한 개념으로 이해할 수 있다. 메소드를 호출할 때마다 그 객체의 제일 앞에 있는 메소드의 바인딩 주소를 찾아서 그 주소로 점프(함수호출)해야 한다. 여기서도 추가의 메모리 IO가 발생한다.