업캐스팅과 동적 바인딩
같은 메시지, 다른 메서드
실행 시점에 메서드를 탐색하는 과정을 살펴보자.
public class Professor {
private String name;
private Lecture lecture;
public Professor(String name, Lecture lecture) {
this.name = name;
this.lecture = lecture;
}
public String compileStatistics() {
return String.format("[%s] %s - Avg: %.1f", name, lecture.evaluate(), lecture.average());
}
}
다음은 다익스트라 교수가 강의하는 알고리즘 과목의 성적 통계를 계산하는 코드다.
Professor professor = new Professor("다익스트라",
new Lecture("알고리즘",
70,
Arrays.asList(81, 95, 75, 50, 45)));
String statistics = professor.compileStatistics();
Lecture
대신 아래와 같이 GradeLecture
로 전달할 수 있다
Professor professor = new Professor("다익스트라",
new GradeLecture("알고리즘",
70,
Arrays.asList(new Grade("A", 100, 95),
new Grade("B", 94, 80)...
),
Arrays.asList(81, 95, 75, 50, 45)));
String statistics = professor.compileStatistics();
생성자의 인자 타입은 Lecture
로 선언되어 있지만 GradeLecture
의 인스턴스를 전달하더라도 아무 문제 없이 실행된다. 동일한 객체 참조인 lecture
에 대해 동일한 evaluate
메시지를 전송하는 동일한 코드 안에서 서로 다른 클래스 안에 구현된 메서드를 실행할 수 있다는 사실을 알 수 있다.
이처럼 코드 안에서 선언된 참조 타입과 무관하게 실제로 메시지를 수신하는 객체의 타입에 따라 실행되는 메서드가 달라질 수 있는 것은 다음의 메커니즘이 작동하기 때문이다.
- 업캐스팅 : 부모 클래스 타입으로 선언된 변수에 자식 클래스의 인스턴스를 할당하는 것이 가능하다. 서로 다른 클래스의 인스턴스를 동일한 타입에 할당하게 해준다.
- 동적 바인딩 : 선언된 변수의 탕비이 아니라 메시지를 수신하는 객체의 타입에 따라 실행되는 메서드가 결정된다. 이것은 객체지향 시스템이 메시지를 처리할 적절한 메서드를 컴파일 시점이 아니라 실행 시점에 결정하기 때문에 가능하다.
개방-폐쇄 원칙과 의존성 역전 원칙
업캐스팅과 동적 메서드 탐색에 대한 설명을 읽다 보면 자연스럽게 머릿속에서 개방-폐쇄 원칙이 떠오를 것이다. 업캐스팅과 동적 메서드 탐색은 코드를 변경하지 않고도 기능을 추가할 수 있게 해주며 이것은 개방-폐쇄 원칙의 의도와도 일치한다.
개방-폐쇄 원칙은 유연하고 확장 가능한 코드를 만들기 위해 의존관계를 구조화하는 방법을 설명한다. 업캐스팅과 동적 메서드 탐색은 상속을 이용해 개방-폐쇄 원칙을 따르는 코드를 작성할 때 하부에서 동작하는 기술적인 내부 메커니즘을 설명한다. 개방-폐쇄 원칙이 목적이라면 업캐스팅과 동적 메서드 탐색은 목적에 이르는 방법이다.
업캐스팅
상속을 이용하면 부모 클래스의 퍼블릭 인터페이스가 자식 클래스의 퍼블릭 인터페이스에 합쳐지기 때문에 부모 클래스의 인스턴스에게 전송할 수 있는 메시지를 자식 클래스의 인스턴스에게 전송할 수 있다.
대입문과 메서드 파라미터 두가지를 통해 명시적으로 타입 변환을 하지 않고 부모 클래스 타입의 참조 변수에 자식 클래스의 인스턴스를 대입할 수 있게 허용한다.
Lecture lecture = new GradeLecture(..);
부모 클래스 타입으로 선언된 파라미터에 자식 클래스의 인스턴스를 전달하는 것도 가능하다.
public class Professor {
public Professor(String name, Lecture lecture) {
//...
}
}
Professor professor = new Professor("다익스트라", new GradeLecture(..));
반대로 부모 클래스의 인스턴스를 자식 클래스 타입으로 변환하기 위해서는 명시적인 타입 캐스팅이 필요한데 이것을 다운캐스팅이라 한다
Lecture lecture = new GradeLecture(..);
GradeLecture GradeLecture = (GradeLecture) lecture; // << 다운캐스팅
Lecture
의 모든 자식 클래스는 evaluate()
메시지를 이해할 수 있기 때문에 Professor
는 Lecture
를 상속받는 어떤 자식 클래스와도 협력할 수 있는 무한한 가능성을 가진다. 그러므로 이 설계는 유연하며 확장이 용이하다.
동적 바인딩
전통적인 언어와 객체지향에서 함수 또는 메시지를 실행하는 방법은 다르다.
객체지향
- 함수를 실행하기 위해 함수를 호출함.
- 호출될 함수를 컴파일 타임, 즉 작성하는 시점에 결정한다.
- 코드상에
foo()
가 있다면 실제로 실행되는 코드는foo()
다. - 컴파일 타임에 호출할 함수를 결정하는 방식을 정적 바인딩, 초기 바인딩, 컴파일타임 바인딩 이라고 한다
전통적인 언어
- 메서드를 실행하기 위해 메시지를 전송함.
- 메시지를 수신했을 때 실행될 메서드가 런타임에 결정됨.
foo.bar()
라는 코드를 읽는 것 만으로는 실행되는bar()
가 어떤 클래스의 어떤 메서드인지 판단하기 어렵다. 인스턴스를 알아야 하고, 상속계층을 알아야 하기 때문임.- 메서드를 런타임에 결정하는 방식을 동적 바인딩, 지연 바인딩 이라고 한다.