목차

오브젝트; 업캐스팅과 동적 바인딩

🗓️

업캐스팅과 동적 바인딩

같은 메시지, 다른 메서드

실행 시점에 메서드를 탐색하는 과정을 살펴보자.

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()메시지를 이해할 수 있기 때문에 ProfessorLecture를 상속받는 어떤 자식 클래스와도 협력할 수 있는 무한한 가능성을 가진다. 그러므로 이 설계는 유연하며 확장이 용이하다.

동적 바인딩

전통적인 언어와 객체지향에서 함수 또는 메시지를 실행하는 방법은 다르다.

객체지향

  • 함수를 실행하기 위해 함수를 호출함.
  • 호출될 함수를 컴파일 타임, 즉 작성하는 시점에 결정한다.
  • 코드상에 foo()가 있다면 실제로 실행되는 코드는 foo()다.
  • 컴파일 타임에 호출할 함수를 결정하는 방식을 정적 바인딩, 초기 바인딩, 컴파일타임 바인딩 이라고 한다

전통적인 언어

  • 메서드를 실행하기 위해 메시지를 전송함.
  • 메시지를 수신했을 때 실행될 메서드가 런타임에 결정됨.
  • foo.bar() 라는 코드를 읽는 것 만으로는 실행되는 bar()가 어떤 클래스의 어떤 메서드인지 판단하기 어렵다. 인스턴스를 알아야 하고, 상속계층을 알아야 하기 때문임.
  • 메서드를 런타임에 결정하는 방식을 동적 바인딩, 지연 바인딩 이라고 한다.