1. Binding Time의 개념
1-1. Binding의 개념
- 바인딩이란, 두 가지 사물 사이의 관계를 나타낸다.
- bound; 엮다. binding;제본이라는 뜻을 가진 것을 유추해 보자.
- a name(변수 등)에 the thing it name을 엮어주는 것.
1-2. Binding Time
- 이름에 속성이나 값이 결정되는 시점
- 바인딩이 되는 시점인데, 일반적으로 구현 결정이 내려지는 시점을 나타낸다.
- 언어 디자인 시점 :
작업 흐름 구조, 기본 데이터 유형의 집합, 복잡한 데이터 유형을 만드는 데에 사용되는 생성자 등을 정의할 때의 바인딩 시간 - 언어 구현 시점 :
스택 & 힙의 구조, 최대 크기 등과 같은 세부 구현 사항을 결정할 때의 바인딩 시간 - 프로그램 작성 시점 :
프로그래머가 프로그램의 알고리즘, 데이터 구조, 변수 이름 및 함수를 설정한다. (이것도 바인딩의 일부라고 본다.) - 컴파일 시점 (Compile time) :
정적으로 정의된 데이터의 레이아웃과 같은 구현 결정을 내림 - 링크 시점 (Link time) :
코드 내에 서서 사용된 라이브러리 모듈들의 링크 - 로드 시점 (Load time) :
초기 OS에서는, 프로그램 내 객체의 machine address(실제 물리 주소)를 로드 시간에 결정했지만,
현대 OS에서는, 가상 및 물리 주소를 구분. 가상 주소는 링크 시간에, 물리 주소는 실행 시간에 변경될 수 있다.
(운영 체제 수업 때 자세히 다룬다.) - 실행 시점 (Run time) :
실행 시작부터 끝까지의 전체 범위를 나타낸다.
변수에 값을 바인딩하는 것부터 다음 3 가지 상세 구분 시점까지 모두 포함한다.
- 프로그램의 실행 시점
- 모듈의 진입 시점
- elaboration time : 선언이 처음 보이는 시점, 변수를 선언하여 변수가 메모리 공간을 차지하는 시점
(**point a which a declaration is first "seen"**)
스토리지 할당 및 바인딩 프로세스
cf. 여기서 declaration은 선언과 정의를 포괄하는 의미
declare : 선언 ex. extern
define : 정의 ex. i=7;
1-3. Static binding vs Dynamic binding
프로그램의 실행과 관련하여 결정되거나 실행 시간에 결정되는 시점
- 정적(Static): 바인딩이 실행되기 전에 결정되는 경우
즉, Compiler는 Compile time에 다양한 결정을 내리게 된다.
정적 바인딩은 프로그램 실행 중에 변경되지 않는다. - 동적(Dynamic): 바인딩이 프로그램이 실행 중인 동안 결정되는 경우.
즉, Run time에 결정되며, 프로그램의 실행 중에 변경될 수 있다.
Compiler-based language는 보통 Static binding을 사용하여 실행 효율성을 높일 수 있다.
pure interpreter는 프로그램이 실행될 때마다 선언을 다시 분석해야 한다.
(최악의 경우 서브루틴을 호출할 때마다 지역변수와 선언을 재분석해야 할 수도 있다.)
Static Binding : Before run time -> Early binding times // greater efficiency, mostly comiled languages
Dynamic Binding : At run time -> Later binding times // greater flexiblity, most interpreted languages
유기적으로 생각해 보면 쉽다.
정적인 바인딩은 "고정적"이라는 이야기다. 실행 시점 이전에 바인딩이 모두 결정된다는 이야기이고, 이는 매우 효율적이다.
그렇다면 실행 이전에 모두 번역이 이루어지는 컴파일 기반의 언어가 주를 이룰 것이다.
동적인 바인딩은 반대로 생각하면 편하다.
"동적"이라는 것은 실행 시점에 바인딩이 결정되는 경우이며, 이는 선언의 분석등이 매우 유연하다는 이야기이다.
당연히 target program이 실행되는 중 계속 제어권을 가지고 실행 중 분석이 이루어지는 인터프리터 기반 언어가 주를 이룰 것이다.
2. 다형성 (Polymorphism)
🤔 그렇다면, 만약 효율성만을 따질 때 모든 언어를 정적으로 컴파일할 수 있는가??
대답은 "쉽지 않다."이다.
그 언어의 설계 구조에 따라 바인딩의 결정을 실행시점까지 연기해야 하는 것을 요구하기 때문이다.
-> later binding time
이에는 다양한 이유가 있다.
- 이전에도 말했듯 유연성과 표현력을 향상시키기 위해서이다.
- 또한 scripting 언어는 모든 타입의 호검사를 run time까지 미뤄야 한다.
- 그리고 또 다른 중요한 이유는 다형성 때문이다.
* 다형성(Polymorphism) :
프로그램이 객체를 다루는 방식을 유연하게 다룰 수 있는 기능이다.
서로 다른 타입의 객체에 대한 참조를 임의로 지정된 이름 있는 변수에 할당할 수 있으며,
프로그램이 해당 객체가 처리할 준비가 되지 않는 연산자(메서드)를 적용하지 않는 한 실행 시간에 유연하게 처리할 수 있다.
2-1. 다형성 예시 ; 다형성이 구현되지 않은 경우
첫 번째 예시를 보자.
# Python code
>>> s = "hello world"
>>> s.find("wor")
6
>>> s.value()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute
'value'
일단 파이썬은 Python의 Dynamic Binding을 통해 변수의 타입을 명시적으로 선언하지 않아도 된다.
(interpreter 언어)
이는 유연성을 제공하는 측면이다.
여기에서 s는 "hello world"를 참조한다.
Python은 실행 중, 변수 s가 문자열 타입임을 자동으로 파악한다.
Python interpreter가 s가 문자열 객체임을 이해하고, 해당 객체에서 find 메서드를 사용할 수 있기 때문에 동작하지만,
str 객체에는 없는 함수인 value 메서드를 사용할 때 에러가 발생하는 것이다.
이러한 유연성은 코드 작성을 간단하게 만들 수 있으며, 다양한 데이터 유형을 사용할 때 특히 유용하다.
그러나 이러한 유연성은 안전성 측면에서 문제가 될 수 있다.
코드 작성자가 실수로 변수 s를 다른 유형의 데이터로 재할당할 경우 코드 실행 중에 오류가 발생할 수 있다.
2-2. 다형성 예시 ; 다형성이 구현된 경우
다음 두 번째 예시를 보자.
class Rectangle:
def __init__(self, width, height):
self.width = width;
self.height = height def calcArea(self):
return self.width * self.height
class Circle:
def __init__(self, radius):
self.radius = radius
def calcArea(self):
return self.radius * self.radius * math.pi
s = Rectangle(10, 20)
s.calcArea()
s = Circle(10)
s.caclArea()
같은 Python으로 예시를 들어보겠다.
이 예시는 객체 지향 프로그래밍의 다형성을 보여주는 예제이다.
다형성은 어떤 클래스가 공통된 메서드를 가질 때, 이 메서드를 해당 클래스의 인스턴스에 대해 호출할 수 있다는 개념을 나타낸다.
<유연성!>
여기서 Rectangle 클래스와 Circle 클래스는 각각 calcArea() 메서드를 가지고 있다.
이러한 메서드의 이름은 동일하나, 각 클래스에서의 구현은 다르다.
그러나 다형성을 활용하면 두 클래스의 인스턴스를 동일한 방식으로 다룰 수 있다.
두 클래스가 동일한 메서드 이름을 가지고 있지만, 메서드 호출은 어떤 객체가 s에 할당되었느냐에 관계없이 동일한 방식으로 작동한다.
=> Polymorphism
2-3. 다형성 예시 ; 객체 지향 언어가 추구하는 다형성을 구현하는 방식
* 예시를 보자.
class Parent {
String name;
public Parent(String s) {
name = s;
}
public String getName() {
return name;
}
public void setName(String s) {
name = s;
}
}
class Child extends Parent {
int value;
public Child(String s, int v) {
super(s);
value = v;
}
public String getName() {
return "Child: " + super.getName();
}
public int getValue() {
return value;
}
public void setValue(int v) {
value = v;
}
}
class Main {
public static void main(String[] args) {
Parent p = new Parent("parent");
System.out.println(p.getName());
Parent c = new Child("child", 2019);
System.out.println(c.getName());
System.out.println(c.getValue());
}
}
다음 코드의 메모리 구조를 보자. // Early Binding의 구조
new Child()라는 객체를 생성할 때 3가지 함수를 모두 모두 메모리 구조에 넣는다고 가정해 보자.
객체 하나를 만들 때마다 반복적인 함수들을 모두 새로 만들어줘야 한다.
매우 매우 비효율적인 방법이 아닐 수 없다.
실제 객체지향 언어의 Java 메모리 구조를 보면 위와 같다.
실제 함수들은 코드 영역 메모리 구조에 단 한 개씩 존재하며,
새로 생성된 Parent, Child의 객체 메모리 구조에는 해당 객체가 가지고 있는 함수의 "주소값"이 참조되어 있는 것뿐이다.
p.getName()을 보면,
Parent의 메모리 구조에 있는 String getName()에는 실제 코드가 들어가 있는 것이 아니라,
그 함수의 위치를 나타내는 주소값이 들어가 있는 것이다.
그렇다면, c.getName()의 구조를 보자.
위 코드를 보면, Child에서 Parent의 getName() 함수를 오버라이딩 하여 다시 만들었다.
함수는 실제로 Parent와 Child의 getName()이 따로 있을 것이다.
Parent c = new Child("child", 2019);
System.out.println(c.getName());
코드를 보면, Child라는 객체를 만들어서 부모 타입인 객체에 저장하였다.
c라는 객체가 getName()을 호출했을 때,
getName()는 부모 클래스에 있지만
자식 클래스가 오버라이딩을 했기 때문에 실제로는 자식 클래스의 getName()을 호출할 것이다.
* 이런 동작이 어떻게 가능한가?
아래와 같이,
Parent의 메모리 구조 내에 있는 String getName()에 실제 Child.getName() 코드가 존재하는 곳의 주소값을 참조해 주면 된다.
부모인 것처럼 불렀지만, 실제로 자식의 getName()이 호출된다.
왜냐하면 Parent의 메모리 구조 내에 있는 getName()은 애초에 실제 코드가 없이 주소값만 저장하는 공간이다.
binding을 자식의 것과 엮어주면 된다.
-> 가상함수 테이블
이와 같은 방법이 다형성을 구현하기 위해 객체지향언어를 디자인하는 방향이다.
이는 Runtime (실행 중에) 일어나며, Compiled 언어들이 Late binding을 사용하기 때문이다.