목차

오브젝트; 동적 메서드 탐색과 다형성

🗓️

동적 메서드 탐색과 다형성

객체지향 언어가 제공하는 업캐스팅과 동적 바인딩을 이용하면 부모 클래스 참조에 대한 메시지 전송을 자식 클래스에 대한 메서드 호출로 변환할 수 있다.

객체지향 시스템은 다음 규칙에 따라 실행할 메서드를 선택한다.

  • 메시지를 수신한 객체는 먼저 자신을 생성한 클래스에 적합한 메서드가 존재하는지 검사하낟. 존재하면 메서드를 실행하고 탐색을 종료한다.
  • 메서드를 찾지 못했다면 부모 클래스에서 메서드 탐색을 계속 한다. 이 과정은 적합한 메서드를 찾을 때 까지 상속 계층을 따라 올라가며 계속된다.
  • 상속 계층의 가장 최상위 클래스에 이르렀지만 메서드를 발견하지 못한 경우 예외를 발생시키며 탐색을 종료한다.

메시지 탐색과 관련하여 self 참조는 중요하다. 객체가 이 메시지를 수신하면 컴파일러는 self참조라는 임시 변수를 자동으로 생성한 후 메시지를 수신한 객체를 가리키도록 설정한다. 동적 메서드 탐색은 self가 가리키는 객체의 클래스에서 시작해서 상속 계층의 역방향으로 이뤄지며 메서드 탐색이 종료되는 순간 self 참조는 자동으로 소멸된다

  • 메서드 탐색은 자식 클래스에서 부모 클래스의 방향으로 진행된다
  • 항상 자식 클래스의 메서드가 부모 클래스의 메서드보다 먼저 탐색된다.
  • 자동적인 메시지 위임 : 자식 클래스는 자신이 이해할 수 없는 메시지를 전송받은 경우 상속 계층을 따라 부모 클래스에게 처리를 위임한다.
  • 동적인 문맥 : 메시지를 수신했을 때 실제로 어떤 메서드를 실행할지를 결정하는 것은 실행시점이며, 메서드를 탐색하는 경로는 self참조를 이용한다.

selfthis

정적 타입 언어에 속하는 C++, Java, C#에서는 self 참조를 this라고 부른다. 동적 타입 언어에 속하는 스몰토크, 루비에서는 self 참조를 나타내는 키워드로 self를 사용한다. python에서는 self 참조의 이름을 임의로 정할 수 있지만 대부분의 개발자들은 전통을 존중해서 self라는 이름을 사용한다.

자동적인 메시지 위임

  • 상속 계층 : 동적 메서드 탐색을 위한 물리적인 경로
  • 상속을 이용하는 경우 메시지 위임에 대한 코드를 명시적으로 작성할 필요가 없음.
  • 메서드 오버라이딩의 경우 자식클래스의 메서드가 동일한 시그니처를 가진 부모 클래스의 메서드보다 먼저 탐색 될 수 있다.
  • 메서드 오버로딩의 경우 자식클래스와 부모클래스의 공존이 가능하다.

메서드 오버라이딩

Lecture lecture = new Lecture(...);
lecture.evalate();

이 경우 Lectureevaluate()가 바로 발견된다.

Lecture lecture = new GradeLecture(...);
lecture.evalate();

GradeLectureLecture에서 동일한 메서드를 정의하고 있다. 그렇기 때문에 GrandLectureevaluate()가 발견된다.

동적 메서드 탐색이 자식 클래스에서 부모 클래스의 순서로 진행ㅗ디기 때문에 양쪽 모두 동일한 시그니처를 가진 메서드가 구현돼 있다면 자식 클래스의 메서드가 먼저 검색 된다. 이는 자식 클래스의 메서드가 부모 클래스의 메서드를 감추는 것처럼 보이게 된다.

메서드 오버로딩

  • 메서드 오버로딩 : 시그니처가 다르기 때문에 동일한 이름의 메서드가 공존하는 경우

하나의 클래스 안에서 같은 이름을 가진 메서드들을 정의하는 것만이 메서드 오버로딩이 아니라 상속 계층 사이에서 같은 이름을 가진 메서드를 정의하는 것 또한 마찬가지다.

동적인 문맥

동적인 문맥을 결정하는 것은 메시지를 수신한 객체를 가리키는 self참조다. self 참조가 Lecture의 인스턴스를 가리키고 있다면 메서드를 탐색할 문맥은 Lecture클래스에서 시작해서 Object클래스에서 종료되는 상속 계층이 된다.

동일한 코드라고 하더라도 self참조가 가리키는 객체가 무엇인지에 따라 메서드 탐색을 위한 상속 계층의 범위가 동적으로 변한다. 따라서 self참조가 가리키는 객체의 타입을 변경함으로써 객체가 실행될 문맥을 동적으로 바꿀수 있다.

다만 self 전송이 이뤄지는 경우 어떤 메서드가 실행될지 예상하기 어려워진다.

public class Lecture {
    public String stats() {
        return String.format("Title: %s, Evaluation Method: $s", title, getEvalulationMethod());
    }

    public String getEvaluationMethod() {
        return "Pass or Fail";
    }
}

자신의 getEvaluationMethod()메서드를 호출한다고 하지만 정확하게는 현재 객체에게 getEvaluationMethod 메시지를 전송하는 것이다. 여기서 현재 객체self를 의미한다. 그것은 처음에 stats메시지를 수신했던 객체다.

self 전송을 하기 위해서는 self 참조가 가리키는 바로 그 객체에서부터 메시지 탐색을 다시 해야한다.

stats() 탐색과정

  1. Lecturestats 메시지 수신
  2. self 참조가 Lecture인스턴스를 가리킴
  3. 시스템은 Lecture에서 stats() 를 발견하고 이를 실행.
  4. stats() 실행 중 getEvaluationMethod() 호출을 발견하면, 시스템은 self 참조가 가리키는 현재 객체에게 메시지를 전송해야 한다고 판단.
  5. self 참조가 가리키는 Lecture 클래스에서부터 다시 메서드 탐색이 시작됨.
  6. LecturegetEvaluationMethod()를 실행한 에 탐색을 종료

getEvaluationMethod()라는 문장 자체는 Lecture의 getEvaluationMethod() 를 실행하라는 의미가 아니라 self가 참조하는 현재 객체에 getEvaluationMethod 메시지를 전송하라는 의미다.

상속의 경우

GradeLecture 클래스에서 getEvaluationMethod() 를 오버라이딩 하는 경우..

public class Lecture {
    public String stats() {
        return String.format("Title: %s, Evaluation Method: $s", title, getEvalulationMethod());
    }

    public String getEvaluationMethod() {
        return "Pass or Fail";
    }
}

public class GradeLecture extends Lecture {
    @Override
    public String getEvaluationMethod() {
        return "Grade";
    }
}
  1. GradeLecturestats 메시지를 전송
  2. self참조는 GradeLecture 인스턴스를 가리키도록 설정
  3. 메서드 탐색은 GradeLecture클래스에서부터 시작
  4. 해당 클래스에 stats 메시지를 처리할 수 있는 적절한 메시지가 없기 때문에 부모 클래스인 Lecture에서 메서드 탐색을 계속함.
  5. Lecture에서 stats()를 발견하고 실행.
  6. stats()실행 중 self가 가리키는 객체에게 getEvaluationMethod 메시지를 전송하는 구문 발견
  7. 메서드 탐색은 self 가 가리키는 객체에서 시작됨. 즉, GradeLecture 의 인스턴스.

self 전송은 자시가 클래스에서 부모 클래스 방향으로 진행되는 동적 메서드 탐색 경로를 다시 self 참조가 가리키는 원래의 자식 클래스로 이동 시킨다. 이는 self 전송이 깊은 상속 계층과 계층 중간중간에 함정처럼 숨겨져있는 메서드 오버라이딩과 만나면 극단적으로 이해하기 어려운 코드가 될 수 있다는 말임.

이해할 수 없는 메시지

클래스는 자신이 처리할 수 없는 메시지를 수신하면 부모 클래스로 처리를 위임한다. 이 탐색의 끝에서 메시지를 처리할 수 없게되면 어떻게 되는가?

정적 타입 언어와 이해할 수 없는 메시지

정적 타입 언어에서는 코드를 컴파일 할 때 상속 계층 안의 클래스들이 메시지를 이해할 수 있는지 여부를 판단한다. 전체 탐색 후 처리할 수 있는 메서드를 발견하지 못했다면 컴파일 에러를 발생시킨다.

자바의 경우 Object까지 탐색한다.

동적 타입 언어와 이해할 수 없는 메시지

정적 타입과 마찬가지로 메시지를 수신한 객체의 클래스로 부터 부모 클래스의 방향으로 메서드를 탐색한다. 다만 동적 타입 언어는 컴파일 단계가 없기 때문에 런타임 이전에 메시지 처리 가능 여부를 판단할 수 없다.

다만 예외는 각 언어에 따라 런타임 메시지를 전달한다.

  • 루비 : method_missing
  • 스몰토크: doesNoteUnderstand

이러한 메시지 throw 대신에 각 예외 메시지에 응답할 수 있는 메서드를 구현하면, 인터페이스에 정의되어 있지 않아도 메시지 처리가 가능하다.

동적 타입 언어는 이해할 수 없는 메시지를 처리할 수 있는 능력을 가짐으로써 메시지가 선언된 인터페이스와 메서드가 정의된 구현을 분리할 수 있다. 그러나 이러한 유연성은 코드를 이해하고 수정하기 어렵게 만들뿐만 아니라 디버깅 과정이 복잡해지는 이유다.

이해할 수 없는 메시지와 도메인-특화 언어

이해할 수 없는 메시지를 처리할 수 있는 동적 타입 언어의 특징은 메타 프로그래밍 영역에서 진가를 발휘한다. 특히 동적 타입 언어의 이러한 특징으로 인해 동적 타입 언어는 정적 타입 언어보다 더 쉽고 강력한 도메인-특화 언어 (DSL) 를 개발할 수 있는 것으로 간주된다. 마틴 파울러는 동적 타입 언어의 이러한 특징을 이용해 DSL 를 개발하는 방식을 동적 리셉션 Dynamic reception 이라고 부른다.

self 대 super

self 참조의 가장 큰 특징은 동적인 부분에 있다. self 참조는 메시지를 수신한 객체의 클래스에 따라 메서드 탐색을 위한 문맥을 실행 시점에 결정한다. 자식 클래스에서 부모 클래스의 구현을 재사용하는 경우, 대부분의 객체지향 언어들은 super 참조를 위한 내부 변수를 제공한다.

public class GradeLecture extends Lecture {
    @Override
    public String evaluate() {
        return super.evaluate() + ", " + gradeStatistics();
    }
}

이러한 경우 ‘부모’의 메서드를 호출하는 것이 아니라 결국 더 ‘조상’의 메서드를 호출할 수도 있다.

  • 지금 이 클래스의 부모 클래스에서부터 메서드 탐색을 시작하세요
public class FormattedGradeLecture extends GradeLecture {
    public FormattedGradeLecture(String name, int pass, List<Grade> grades, List<Integer> scores) {
        super(name, pass, grade, sroces);
    }

    public String formatAverage() {
        return String.format("Avg: %1.1f", super.average());
    }
}

FormattedGradeLecture에서 super.average()를 호출했을때 부모인 GradeLectureaverage()가 존재하지 않아도 상위로 찾아가는 유연성이 있다.

이처럼 super 참조를 통해 메시지를 전송하는 것은 마치 부모 클래스의 인스턴스에게 메시지를 전송하는 것 처럼 보이기 때문에 이를 super 전송 이라 부른다.

super 참조의 문법

대부분의 객체지향 언어는 부모 클래스에서부터 메서드 탐색이 시작하게 하는 super 탐조를 위한 의사변수를 제공한다. 자바에서는 이 의사 변수를 가리키기 위해 super라는 예약어를 사용한다.

self 전송이 메시지를 수신하는 객체의 클래스에 따라 메서드를 탐색할 시작 위치를 동적으로 결정하는데 비해 super 전송은 항상 메시지를 전송하는 클래스의 부모 클래스에서부터 시작된다. 그래서 self전송은 어떤 클래스에서 메시지 탐색이 시작될지 알지 못한다. 즉 이것은 self전송은 메시지 탐색을 시작하는 클래스가 미정이지만 super전송에서는 미리 정해진다는 것이다.

super 전송과 동적 바인딩

상속에서 super가 컴파일 시점에 미리 결정된다고 설명했지만 super를 런타임에 결정하는 경우도 있다. 사용하는 언어의 특성에 따라 컴파일 시점이 아닌 실행 시점에 super의 대상이 될 수도 있다.

🏷️