오브젝트: 다형성과 동적 메서드 탐색의 원리
12장 다형성
0. 개요
- 단순히 코드를 재사용하기 위한 목적으로 상속 사용을 하지 않는다
- 상속은 런타임에 메시지를 처리하기 적합한 메서드를 동적으로 찾기 위해 일종의 탐색 경로를 클래스 계층 형태로 구현하기 위한 방법이다.
1. 다형성
다형성의 분류
- 유니버셜
- 매개변수(Parametric)
- 클래스의 인스턴스 변수나 메서드의 매개변수 타입을 임의의 타입으로 선언한 후 사용하는 시점에서 구체적인 타입으로 지정하는 방식
- 포함(Inclusion)
- 메시지가 동일하더라도 수신한 객체의 타입에 따라 실제로 수행되는 행동이 달라지는 능력
- 일반적으로 알려진 다형성
- 자식 클래스가 부모 클래스의 서브 타입이어야 한다
- 매개변수(Parametric)
- 임시(Ad Hoc)
- 오버로딩
- 하나의 클래스 안에 동일한 이름의 메서드가 존재하는 경우
- 강제(Coercion)
- 언어가 지원하는 자동적인 타입 변환이나 사용자가 직접 구현한 타입 변환을 이용해 동일한 연산자를 다양한 타입에 사용할 수 있는 방식
- 예) 문자열 ‘+’ 연산자 사용 시 강제 형변환
- 오버로딩
포함 다형성을 위해 상속을 사용하는 이유
상속이 클래스들을 계층으로 쌓아 올린 후 상황에 따라 적절한 메서드를 선택할 수 있는 메커니즘을 제공하기 때문이다.
2. 상속의 양면성
- 자식 클래스의 인스턴스를 통해 부모 클래스에 정의된 메서드를 실행하는 것은 실제로 부모 클래스의 메서드를 합치거나 복사하는 것이 아니다.
- 런타임에 시스템이 자식 클래스에 정의가 되지 않은 메서드를 부모 클래스 안에서 탐색하는 것이다.
- 메시지를 수신한 객체는
class포인터
로 연결된 자신의 클래스에서 적절한 메서드가 존재하는지 확인한다- 만약 없으면 클래스의
parent포인터
를 따라 부모 클래스를 차례대로 훑어 가면서 적절한 메서드가 존재하는지 검색한다
- 만약 없으면 클래스의
3. 업캐스팅과 동적 바인딩
동일한 수신자에게 동일한 메시지를 전송하는 동일 코드를 이용해 서로 다른 메서드를 실행할 수 있는 이유는 업캐스팅과 동적 메서드 탐색이라는 메커니즘이 있기 때문이다
- 업 캐스팅
- 부모 클래스 타입으로 선언된 변수에 자식 클래스의 인스턴스를 할당하는 것
- 동적 바인딩
- 메시지를 처리할 적절한 메서드를 컴파일 시점이 아니라 실행 시점에 결정
업 캐스팅
부모 클래스의 인스턴스 대신 자식 클래스의 인스턴스를 사용하더라도 메시지를 처리하는데 문제가 발생하지 않고, 컴파일러는 명시적인 타입 변환없이도 자식 클래스가 부모 클래스를 대체할 수 있게 허용한다.
- 활용) 대입문, 메서드의 파라미터 타입
다운 캐스팅
부모 클래스의 인스턴스를 자식 클래스 타입으로 변환하기 위한 명시적인 타입 캐스팅이다
동적 바인딩
컴파일 타임에 호출할 함수를 결정하는 방식
- 정적 바인딩
- 초기 바인딩(elarly binding)
- 컴파일 타임 바인딩
런타임 타임에 호출할 함수를 결정하는 방식
- 동적 바인딩
- 지연 바인딩(late binding)
4. 동적 메서드 탐색과 다형성
실행할 메서드 선택 규칙
- 메서지를 수신한 객체는 먼저 자신을 생성한 클래스에 적합한 메서드가 존재하는지 검사한다.
- 만약 존재하면 메서드를 실행하고 탐색 종료한다.
- 메서드를 찾지 못했다면 부모 클래스에서 메서드를 탐색을 이어간다. 적합한 메서드를 찾을 때까지 탐색 계속 상속 계층을 따라 올라간다.
- 상속 계층의 가장 최상위 클래스에서도 메서드를 발견하지 못하면 예외를 발생시키며 탐색을 한다.
Self 참조
- 객체가 메시지를 수신하면 컴파일러는
self 참조
라는 임시 변수를 자동으로 생성한 후 메시지를 수신한 객체를 가리키도록 설정한다. - 동적 메서드 탐색은
self가 가리키는 객체의 클래스에서 시작해서 상속 계층의 역방향(부모쪽으로)
으로 이루어진다. - 메서드 탐색이 종료되는 순간 self 참조는 자동으로 소멸된다
동적 메서드 탐색의 원리
- 자동적인 메시지 위임
- 자식에게 없을 경우 부모에게 처리를 위임
- 동적인 문맥 사용
- 런타임 시점에 메서드 결정
- 탐색경로는 self 참조 이용
참고) C++는 같은 클래스 안에서 메서드 오버로딩은 허용하지만 상속 계층 사이에서 메서드 오버로딩은 금지한다. 자바는 상속 계층 사이에서도 메서드 오버로딩이 가능하다
self참조
가 동적 문맥을 결정한다는 것은 종종 어떤 메서드가 실행될지 예상하기 어렵게 만든다
- 자신에게 다시 메세지를 전송하는 self 전송(self send)
- 현재 클래스의 메서드를 호출하는 것보다 현재 객체에게 메시지를 전송하는 것이 더 정확한 표현이다
- 메시지를 전달 받으면 self 참조가 가리키는 그 클래스부터 다시 탐색을 이어나간다.
- 특정 경우엔 부모의 메소드와 자식의 메소드 실행결과를 조합한 문자를 반환하는 결과가 실행 될 수 도 있다.
- 메시지를 전달 받으면 self 참조가 가리키는 그 클래스부터 다시 탐색을 이어나간다.
이해할 수 없는 메시지
- 정적 타입 언어와 이해할 수 없는 메시지
- 상속 계층 전체를 탐색한 후에도 메시지를 처리할 수 없는 메서드를 발견하지 못한다면 컴파일 에러를 발생시킨다.
- 동적 타입 언어와 이해할 수 없는 메시지
- 실제로 코드를 실행해보기 전에는 메시지 처리 가능 여부를 판단할 수 없다
- 동적 타입 언어에서는 이해할 수 없는 메시지에 대해 예외를 던지는 것 외에도 선택할 수 있는 방법이 하나 더 존재한다.
- 예외 메시지에 응답할 수 있는 메서드를 구현하는 것이다.
- 이 경우 객체는 자신의 인터페이스에 정의되지 않은 메시지를 처리하는 것이 가능해진다.
- 예외 메시지에 응답할 수 있는 메서드를 구현하는 것이다.
self 대 super
자식 클래스에서 부모 클래스의 인스턴스 변수나 메서드에 접근할 수 있는 용도의 super 참조라는 내부 변수를 제공한다. 정확히는 super 참조를 이용해 메서드를 호출하는 것이 아니라, 부모 클래스에게 메시지를 전달하는 것이다.(부모의 부모에서도 처리될 수 있다.)
super 참조를 통해 메시지를 전송하는 것은 마치 부모 클래스의 인스턴스에게 메시지를 전송하는 것 처럼 보이기 때문에 super send(super 전송) 이라고 부른다
super 전송은 항상 해당 클래스의 부모 클래스부터 메서드 탐색을 시작한다. self 전송에서 메시지 탐색을 시작하는 클래스는 미정이지만, super 전송에서는 미리 정해진다. 즉, 컴파일 시점에 미리 결정할 수 있다.
5. 상속 대 위임
위임과 self 참조
메서드 탐색 중에는 자식 클래스의 인스턴스와 부모 클래스의 인스턴스가 동일한 self 참조를 공유하는 것으로 봐도 무방하다.
자신이 수신한 메시지를 다른 객체에게 동일하게 전달해서 처리를 요청하는 것을 위임이라고 한다. 위임은 본질적으로 자신이 정의하지 않거나 처리할 수 없는 속성 또는 메서드의 탐색 과정을 다른 객체로 이동시키기 위해 사용한다.
위임을 위해 항상 현재 실행 문맥을 가리키는 self 참조를 인자로 전달한다. 위임과 포워딩의 차이는 self 참조를 전달 하느냐 안하느냐 차이다.
상속은 동적으로 메서드를 탐색하기 위해 현재의 실행 문맥을 가지고 있는 self 참조를 전달한다. 그리고 이 객체들 사이에서 메시지를 전달하는 과정은 자동으로 이루어진다. 그렇기 때문에 자동적인 메시지 위임이라고 부르는 것이다.
프로토타입 기반의 객체지향 언어
클래스가 존재하지 않고 오직 객체만 존재하는 프로토타입 기반의 객체지향 언어에서 상속을 구현 하려면 객체 사이의 위임을 이용해야한다.
자바 스크립트의 모든 객체는 다른 객체를 가리키는 용도로 사용하는 prototype
이라는 링크를 가진다. 언어 차원에서 제공되기 때문에 self 참조를 직접 전달하거나 메시지 포워딩을 번거롭게직접 구현하지 않아도 된다.
자바스크립트에서 인스턴스 메시지를 수신하면 메시지를 수신한 객체의 prototype안에서 메시지를 응답할 적절한 메서드가 존재하는지 검사한다. 만약 존재하지 않는다면 prototype이 가리키는 객체를 따라 메시지 처리를 자동적으로 위임한다.