[Java] 정적 바인딩과 동적 바인딩
정적 바인딩과 동적 바인딩
바인딩은 언제 해주는지에 따라 정적 바인딩과 동적 바인딩으로 나뉘고, 변수의 바인딩과 함수의 바인딩 또한 차이가 있다.
바인딩이란?
바인딩이란 이름(identifier)을 어떤 속성과 연관 짓는 것을 말하며, 보통 변수, 상수, 함수 등의 이름을 속성과 연관 짓는 것을 말한다. 여기서 연관 짓는 다는 것은 변수나 함수와 같은 식별자가 특정 값 또는 메모리 위치에 결합되는 프로세스를 의미한다.
- 변수에 구체적인 값을 할당하는 과정
- 메소드를 호출할 때 그 메소드가 위치한 메모리 주소로 연결하는 과정
정적 바인딩
컴파일 시에 한번 바인딩이 이루어지고 실행 동안 변하지 않고 유지된다.
변수나 함수 호출시에 해당하는 메모리 주소나 값이 컴파일러에 의해 미리 결정된다.
동적 바인딩
런타임 중에 이루어지는 바인딩으로, 프로그램의 실행파일이 만들어지고 이를 실행하면서 변수 및 함수가 호출될 때 바인딩 된다. 즉, 함수 호출 시에 해당하는 메모리 주소나 값이 프로그램이 실행되는 동안에 결정된다.
다형성을 사용하여 메서드를 호출할 때 발생하는 현상.
< 코드 예제>
class Parents {
// 정적 바인딩 _ static으로 선언하였기 떄문에 컴파일시에 바인딩 된다.
static void printMessageA(){
System.out.println("Parents class func was called _ message A");
}
// 동적 바인딩 _ static이 아니기 때문에 런타임 시간에 바인딩 된다.
void printMessageB(){
System.out.println("Parents class func was called _ message B");
}
}
class Child extends Parents {
// 정적 바인딩 _ 컴파일 시에 바인딩
static void printMessageA(){
System.out.println("Child class func was called _ message A");
}
//동적 바인딩
@Override
void printMessageB(){
System.out.println("Child class func was called _ message B");
}
}
public class Main {
public static void main(String[] args) {
Parents p = new Parents();
p.printMessageA();
p.printMessageB();
Parents c = new Child(); // 자식 객체를 부모 클래스의 참조 변수에 할당 => 업캐스팅.
c.printMessageA(); // 정적 바인딩으로 이미 컴파일시에 선언된 타입에 따라 부모 클래스의 메서드로 바인딩 됨.
c.printMessageB(); // static이 아니기 때문에, 들어있는 객체에 따라 런타임 시간에 동적바인딩이 됨. (오버라이딩된 메서드가 호출됨)
// new 는 런타임 시간에 메모리 할당된다.
}
}
<Parent>
Parents Class에서 static으로 선언된 메서드 ‘messageA’ 와 static으로 선언되지 않은 messageB를 각각 선언해 주었다.
<Child>
Parents 클래스를 상속받은 Child 클래스이다.
printMessageA 함수는 정적으로 선언되어. 컴파일 시간에 바인딩된다.
printMessageB 함수는 정적으로 선언되지 않았으며, Parents 클래스의 메서드를 오버라이딩하여 재정의 하고 있다.
<Main>
타입 Parents으로 p라는 참조변수를 선언하고 new를 이용하여 Parents 객체를 생성하여 할당하고 있다. (참조 변수의 타입과 실제 생성한 객체의 타입이 일치)
printMessageA 메서드는 static 으로 선언되었기 때문에 컴파일 시간에 바인딩 되고, printMessageB 메서드는 런타임시간에 동적 바인딩 될 것이라고 예측할 수 있다.
c.printMessageA() 를 호출하자 예상과 달리Parents 클래스에서 정의된 함수가 호출되었고, c.printMessageB() 를 호출하자 예상 대로 Child 클래스에서 Override한 함수가 호출되었다.
왜일까?
먼저 Java 언어의 세가지 특징을 이해해야 한다.
첫번째 , static으로 선언된 정적 메서드의 경우 컴파일 시간에 메서드가 정적 바인딩 된다는 것
두번째 , 정적 타입 언어이기 때문에 컴파일 시간에 변수의 타입을 결정한다는 것
세번째 , new 연산자는 런타임 시간에 객체를 동적으로 생성하고 메모리를 할당한다는 것이다.
c.printMessageA() 의 경우를 보겠다.
이 함수의 호출 결과를 보면 Parents 클래스에 정의된 함수가 있는 메모리에 바인딩 되었다는 것을 알 수 있다. 위의 특징을 고려하면 c.printMessageA함수의 경우에는 컴파일 시간에 바인딩 될 것이다. 그 이유는 static으로 선언된 정적 메서드이기 때문이다. 그렇다면 Parents 클래스와 Child클래스의 메서드중 어떤 메서드에 바인딩 해야될 것인가? 이것은 위에서 설명한 두번째 특징을 고려하면 결론을 도출할 수 있다. 컴파일 타임은 어떤 객체가 생성되는 시점이 아니고 변수의 타입만이 결정된 시점이다. 즉, 위 프로그램의 컴파일 시간에 컴파일러는 이 참조변수 c는Parents 타입으로 선언되었다는 사실만을 알게 된다. 결국 컴파일 시간에 정적 메서드를 바인딩 할떄는 참조변수의 타입을 참고하여 바인딩 된다는 사실을 알 수 있다.
두번째로 c.printMessageB() 의 경우를 살펴 보겠다.
이 함수의 경우에는 Child클래스에서 오버라이딩된 함수가 호출된 것을 알 수있다. 동적 바인딩이 되었다는 의미이다. Static으로 선언된 함수의 경우에는 컴파일 시간에 바인딩 되지만, 그렇지 않은 함수의 경우에는 런타임 시간에 동적 바인딩 된다. 동적 바인딩의 핵심은 호출된 시점에 실제 객체의 실제 타입에 따라 적절한 메서드가 호출된다는 것이다. 이는 new 생성자가 런타임시점에 객체를 생성하고 메모리를 할당하기 때문에 가능한 것으로 볼 수 있다.
업캐스팅과 다운캐스팅
업캐스팅
업캐스팅은 자식 클래스가 부모 클래스 타입으로 캐스팅 되는 것이다. 업캐스팅은 캐스팅 연산자 괄호를 생략할 수 있다. 단, 부모 클래스로 캐스팅 된다는 것은 멤버의 갯수 감소를 의미한다. 이는 곧 자식 클래스에서만 있는 속성과 메서드는 실행하지 못한다는 뜻이다. ( 멤버는 인스턴스에 적용될 수 있는 용어)
정리하자면 업캐스팅을 다루는데 있어 조심해야 할점은 크게 두가지로 요약할 수 있다.
1. 업캐스팅 하면 멤버 갯수가 제한되어 자식 클래스에만 있는 멤버는 사용할 수 없게 된다.
2. 업캐스팅 했지만 오버라이딩 된 메서드는 자식 클래스의 메서드로 실행이 된다. (이는 오버라이딩 특성상 코드가 실행하는 런타임 환경에서 동적으로 바인딩 되었기 때문이다.)
업캐스팅 하는 이유
업캐스팅을 사용하는 이유는 공통적으로 할 수 있는 부분을 만들어 간단하게 다루기 위해서이다. 부모 타입으로 자식객체들을 관리할 수 있다는 덤에서 다형성의 개념으로 볼 수 있다.
다운캐스팅
다운캐스팅은 부모 클래스를 자식 클래스로 캐스팅 하는 단순한 업캐스팅의 반대 개념이 아니다.
진정한 의미는 부모 클래스로 업캐스팅 된 자식 클래스를 복구하여, 본인의 필드와 기능을 회복하기 위에 있는 것이다. 즉 원래 있던 멤버들을 회복하기 위해 다운 캐스팅을 하는 것이다.
따라서 업캐스팅 되지 않은 생 부모 인스턴스는 다운 캐스팅을 할 수 없다. 애초에 업캐스팅 된 자식 객체를 대상으로 해야한다.
동적 바인딩이 아무래도 실행 시간에 바인딩 되다 보니 정적 바인딩 보다는 성능상 안 좋을 수도 있다. 하지만 다형성과 같은 기능을 사용할 수 있다는 장점이 존재하기 때문에 사용하는 것 같다.
<참고 자료>
https://woovictory.github.io/2020/07/05/Java-binding/
프로그래밍 언어론 원리와 실제 _ 창병모
https://lordofkangs.tistory.com/21