다형성 – 5.함수의 바인딩

함수를 컴파일하게 되면 해당 함수의 프로그램 코드가 물리적인 메모리상의 임의의 위치에 저장되며, 저장된 메모리 주소(address)를 호출하는 코드와 연결시켜 줍니다. 함수를 호출하는 코드와 함수가 위치한 메모리 주소를 연결시켜 주는 과정을 바인딩(binding)이라고 합니다. 즉, 함수의 바인딩이라고 하는 것은 함수 호출에 대하여 이러한 함수 호출을 수행하기 위하여 실행될 함수의 프로그램 코드(함수가 저장된 주소)를 결정(연결)하는 작업을 가리키는 용어입니다.

바인딩은 다형성 개념을 구현하기 위한 중요한 개념으로 초기 바인딩(early binding)은 프로그램을 컴파일 할 때에 발생하고 후기 바인딩(late binding)은 프로그램이 실행될 때에 발생합니다.

프로그램이 컴파일 될 때 처리되는 바인딩 프로그램이 실행될 때 처리되는 바인딩
Early Binding Late Binding
초기 바인딩 후기 바인딩, 지연 바인딩, 늦은 바인딩
Compile Time Binding Run Time Binding
컴파일 시간 바인딩 실행 시간 바인딩, 런타임 바인딩
Static Binding Dynamic Binding
정적 바인딩 동적 바인딩

* 본 문서에서는 Early Binding, Compile Time Binding, Static Binding을 초기 바인딩으로, Late Binding, Run Time Binding, Dynamic Binding을 후기 바인딩으로 기술하도록 하겠습니다.

아래의 예와 같이 함수를 호출하는 코드(circle_area(5))와 연결될 함수(function circle_area($radius){})의 주소가 함수를 호출하는 코드(circle_area(5))가 가르키게 되는 연결될 함수(function circle_area($radius){})의 메모리상의 주소가 프로그램이 실행되는 동안에 변동될 가능성이 없는 경우라면 컴파일 할 때 바인딩 되지만 그렇지 않고 유동적이라면 컴파일 할 때 바인딩 되지 않고 프로그램이 실행될 때 해당 주소로 바인딩(해당 함수와 연결) 됩니다.

동일한 이름을 가진 함수의 바인딩

오버로딩에 의해 정의된 함수의 매개변수의 자료형이 다르거나 또는 함수에 전달되는 매개변수의 갯수가 틀리기만 하면 하나의 프로그램에 동일한 이름을 가진 두 개 이상의 함수가 동시에 존재할 수 있습니다.

부모 클래스로부터 상속받은 자식 클래스에서 오버라이딩(재정의)한 함수는 부모 클래스의 함수와 동일한 이름을 가지며, 동일한 매개변수와 반환값의 자료형으로 선언됩니다.

하나의 프로그램에 존재하는 함수 호출과 이러한 함수 호출에 의하여 실행되는 프로그램 코드를 바인딩하기 위해서는 컴파일할 때 또는 실행할 때에 함수 호출에서 사용되는 함수의 이름, 함수에 전달되는 매개변수의 자료형, 함수에 전달되는 매개변수의 개수 등을 확인하여 이러한 조건이 모두 일치되는 함수를  실행시키도록 바인딩 작업을 수행합니다.

객체지향언어와 관련하여 구분한다면 초기 바인딩은 클래스 정보를 사용하는 정적 함수의 호출(static method call)을 처리하고 후기 바인딩은 객체를 사용하는 비정적 함수의 호출(non static method call)을 처리하는 것이 일반적입니다. 또한 초기 바인딩할 때 오버로딩 함수를 처리하고 후기 바인딩할 때 오버라이딩(재정의)된 함수를 처리합니다.

사실 특정 프로그램 코드의 바인딩 시점을 결정하는 것은 프로그래밍 언어마다 다소 차이가 있기 때문에 일률적으로 정의하기는 어렵습니다. 따라서 PHP에서는 여기에서 기술한 내용과 다르게 동작할 수 있다는 것을 염두에 두시기 바랍니다.

초기 바인딩(early binding)

위와 같이 name() 함수를 호출하는 경우에는 컴파일할 때 이미 어떤 클래스에 정의된 name() 함수를 호출해야 할지 명확하게 알고 있기 때문에 초기 바인딩(early binding)을 하게 됩니다.

(참고) 매직 상수 ‘__CLASS__’는 실행 시간(run time)에 처리되는 일반 상수와 달리 컴파일 시간(compile time)에 처리됩니다. 이에 따라 컴파일 할 때 초기 바인딩을 통해 3번행의 ‘return __CLASS__;’에서 __CLASS__ 상수값은 ‘Car’로 할당되며, 9번행의 ‘return __CLASS__;’에서 __CLASS__ 상수값은 ‘SportsCar’로 할당됩니다. 결국 위의 코드는 초기 바인딩 직후 시점에서 본다면 아래와 같이 작성된 것과 동일합니다.

후기 바인딩(Late Binding)

위와 같이 getName() 함수에 있는 $this->name() 코드에서 호출하는 name() 함수의 주소는 부모클래스 Car의 객체에서 호출할 수도 있고, 자식클래스 sportsCar의 객체에서 호출할 수도 있기 때문에 컴파일 시간에 바인딩 할 수 없게 됩니다. 이러한 경우에는 실행하면서 어떤 객체에서 호출하느냐에 따라 해당 객체의 name() 함수와 바인딩하게 됩니다.

후기 정적 바인딩(Late Static Binding)

위에서 살펴보았듯이 오버라이딩(재정의)된 경우를 보면 호출하는 객체에 따라 해당 멤버를 실행 시간에 적절히 후기 바인딩하게 됩니다. 그러면 정적 함수의 경우는 어떻게 되는지 살펴보겠습니다.

일반적인 상속 개념에서 보면, 위 예제와 후기 바인딩(late binding)에서의 예제와 비교할 때 self::name()의 반환값과 $this->name()의 반환값이 동일하여, $scar->getName()과 SportsCar::getName()의 출력값이 둘 다 ‘SportsCar’이기를 기대하지만, 기대와는 달리 self::name()의 반환값은 ‘Car’을 반환합니다. 이 문제는 위에서 살펴본 매직 상수 __CLASS__와 같이 정적 레퍼런스 self::가 컴파일 시간(compile time)에 자신이 속한(자신을 에워싼) 함수가 정의된 현재 클래스로 결정되기 때문입니다.

즉, $this->name()에서의 $this는 상속 규칙에 따라 호출하고 있는 객체로 처리되므로 자식 클래스의 객체에서 호출하게 되면 자식 클래스의 name() 함수에 접근하게 되는 반면에, self::name()에서의 self::는 자식 클래스에서 호출하더라도 self:: 자신이 사용되고 있는 위치에 있는(self::를 에워싼) 클래스(Car)의 name() 함수에 접근하게 되는 것입니다.

결국 ‘self::name()’ 코드는 아래와 같이 컴파일러에 의해 초기 바인딩 할 때 ‘Car::name()’로 변경됩니다.

다시 정리한다면 기본적으로 self 키워드는 일반적인 상속 규칙을 따르지 않으며, 항상 자신이 속한 클래스를 참조합니다. 즉, 부모 클래스에서 함수를 만들어 자식 클래스에서 호출하더라도 상속 규칙에서 벗어나 자식을 참조하지 않습니다.

객체를 통해 멤버를 호출하였을 때는 해당 객체에 저장된 값과 getName() 함수를 적절히 바인딩해 주고 있으나 객체가 아닌 ‘클래스::함수()’와 같이 정적으로 멤버를 호출하였을 때는 부모 클래스(Car)가 되었든 자식 클래스(SportsCar)가 되었든 상관없이 7번행에서 지정한 self::name()에 의해 자신이 위치한 클래스(Car)의 name() 함수와 바인딩하게 됩니다. 즉 초기 바인딩(early binding)이 이루어지고 있는 것입니다. self 키워드와 함께 정적 멤버 선언에 사용되는 parent 또는 클래스명(Car, SportsCar)을 지정하더라도 자신이 위치한 클래스의 name() 함수와 정적으로, 즉 컴파일 시간에 초기 바인딩(early binding)이 이루어지는 것입니다.

이러한 문제를 해결하기 위하여 PHP는 5.3.0 버전부터 정적 상속이라는 컨텍스트(context) 내에서 호출된 클래스를 참조하는 데 사용할 수 있는 후기 정적 바인딩(late static binding;지연 정적 바인딩, 늦은 정적 바인딩)이라는 기능을 구현하게 되며, 이를 위해 static 키워드의 용도를 확장하여 사용하게 됩니다. 이에 따라 static 키워드를 범위지정연산자(::)와 함께 프로퍼티 또는 함수에 붙여 호출하게 되면 일반적인 상속 규칙에서 예상하는 대로 정상적으로 프로그램이 실행될 때 후기 바인딩(late binding)하여 처음 호출된 클래스를 참조하도록 하였습니다.

(참고) 전달 호출, 비전달 호출

전달 호출(forwarding call)은 self::, parent::, static::, 또는 클래스 계층 구조에서forward_static_call()에 의한 정적인 호출 방법으로 피호출(호출 받는) 멤버에 호출하는 자신의 클래스명이 저장된 호출 정보(calling information)를 전달합니다.

비전달 호출(non-forwarding call)은 피호출(호출 받는) 멤버에 호출 정보(calling information)를 전달하지 않습니다. 비전달 호출에는 A::함수()와 같이 클래스명에 의한 정적 함수 호출(static method call)과 $s->함수()와 같이 객체에 의한 비정적 함수 호출(non static method call)이 있습니다.

후기 정적 바인딩(late static binding)은 마지막으로 비전달 호출한 클래스의 이름 또는 객체의 클래스 이름을 전달하며, 후기 정적 바인딩을 구현하는 static::의 스코프는 get_called_class()에 의해 반환되는 클래스입니다.

후기 정적 바인딩(late static binding) 기능을 통해 객체를 통한 비정적 함수 호출뿐만 아니라 클래스명에 의한 정적 함수 호출에서도 다형성(polymorphism) 개념을 정상적으로 구현할 수 있게 되었습니다.

답글 남기기