Ch12 요약(다형성)
Ch12 다형성
들어가며
챕터 12 다형성
의 내용은 객체지향 설계
에 대한 내용이라기 보다는 상속이 다형성을 구현하는 메커니즘
에 대한 내용이다.
객체지향 패러다임이 성공을 거둔 핵심적인 이유 중 하나는 지속 가능한 설계가 가능
한 것이라고 생각한다. 그리고 이 지속 가능성
이란 유지보수의 용이성
과 확장의 용이성
에서 기인한 것이라고 할 수 있다.
다형성
이 객체지향을 논할 때 빠질 수 없는 핵심적인 키워드인 이유가 바로 지속 가능한 설계
를 가능하게 하기 때문이다.
이번 챕터의 주제는 상속의 관점에서 다형성이 구현되는 메커니즘 살펴보기 이다.
다형성
이란 동일한 메시지에 대하여 서로 다른 메서드를 실행할 수 있는 것 이라고 하였다. 그렇다면 객체지향 프로그래밍에서는 어떤 메커니즘으로 동일한 메시지
에 대해서 서로 다른 메서드
를 실행할 수 있을까?
핵심을 먼저 간추리자면
런타임
에 메시지와 메서드를 바인딩하는동적 바인딩
런타임
에 메시지를 처리할 메서드를 탐색하는동적 메서드 탐색
이 두 가지에 유념하여 챕터를 살펴보도록 하자.
핵심.
-
상속
의 목적은코드 재사용
이 아니다.상속
은타입 계층 구조화
를 위해서 사용해야 한다. -
상속을 사용하려는 목적은 ‘클라이언트’ 관점에서 객체들을 동일하게 행동하는 그룹으로 묶기 위함이다.
-
다형성
은런타임
에 메시지를 처리하기에 적합한 메서드를동적으로 탐색하는 과정
을 통해 구현된다. -
상속
은 동적으로 메서드를 탐색하기 위한 일종의탐색 경로
를 클래스 계층 형태로 구현하기 위한 방법이다. -
상속
의 진정한 목적은코드 재사용
이 아니라 다형성을 위한 서브타입 계층을 구축하는 것이다. -
상속
을 사용하는 가장 큰 이유는 상속이 클래스를 계층으로 쌓아올린 후 상황에 따라 적절한 메서드를 선택할 수 있는 메커니즘을 제공하기 때문이다. -
객체가
메시지를 수신
하면 객체지향 시스템은 메시지를 처리할 적절한 메서드를 상속 계층 안에서 탐색한다. -
상속
의 메커니즘을 이해하는데업캐스팅
,동적 메서드 탐색
,동적 바인딩
,self 참조
,class 참조
,super 참조
가 중요한 키워드다. -
자식 클래스의 인스턴스는 직접 정의한 인스턴스 변수 뿐 아니라 부모 클래스가 정의한 인스턴스 변수도 함께 포함한다.
-
상속을 인스턴스 관점에서 바라볼 때 개념적으로 자식클래스의 인스턴스가 부모 클래스의 인스턴스를 포함하고 있는 것으로 바라보는 것이 유용하다.
-
상속
을 하면부모 클래스의 퍼블릭 인터페이스
는자식 클래스의 퍼블릭 인터페이스
에 포함된다. 따라서 외부의 객체가 부모 클래스의 인스턴스에 전송할 수 있는 모든 메시지는 자식 클래스의 인스턴스에게도 전송할 수 있다. -
부모 클래스의 퍼블릭 인터페이스가 자식 클래스의 퍼블릭 인터페이스에 합쳐진다고 표현했지만 실제로 클래스의 코드가 합쳐지거나(.class에 합쳐지거나) 부모 클래스의 퍼블릭 인터페이스 코드를 자식 클래스에 복사하는 것이 아니다.
-
런타임
에 자식 클래스의 인스턴스에게 전송한 메시지에 대해서 메서드가 자식 클래스에 정의되지 않은 경우, 부모 클래스에 위임하여 부모 클래스에서 메서드 탐색을 한다. -
인스턴스
의 경우에는 서로 다른 상태를 저장할 수 있도록 각 인스턴스 별로 독립적인 메모리(Heap)를 할당 받아야 하지만,메서드
의 경우 동일한 클래스의 인스턴스끼리 공유가 가능하기 때문에 클래스는 한번만 메모리에 로드(Class Area or Method Area) 하고 각 인스턴스들이클래스(Class 메타 정보)를 가리키는 포인터
를 갖게 하는것이 경제적이다. -
서로 다른 상태를 갖는 인스턴스들은 Heap에 개별 할당되고, 메서드는 공유할 수 있도록 Class Area에 할당된다. 그리고 각 인스턴스들은 이 Class Area에 존재하는 Class의 위치를 가리키는 Class Pointer를 갖는다.
-
메시지
를 수신한 객체는 갖고있는Class Pointer
를 이용하여연결된 자신의 Class 메타정보
를 찾아 적절한 메서드가 존재하는지를 검색 한다. 만약 메서드가 존재하지 않는다면,Parent Pointer
를 따라 부모 클래스를 차례로 훑어가면서 적절한 메서드가 존재하는지를 검색 한다. -
자식 클래스
에서부모 클래스
로의메서드 탐색
이 가능하기 때문에 자식 클래스는 마치 부모 클래스의 구현된 메서드 복사본을 갖고있는 것 처럼 보인다. -
Class Pointer
와Parent Pointer
를 조합하면 현재 인스턴스의 클래스에서 최상위 부모 클래스에 이르기 까지 모든 부모 클래스에 접근하는 것이 가능하다. -
코드 안에서 선언된 참조 타입과 무관하게 실제로 메시지를 수신한 객체의 타입에 따라 실행되는 메서드가 달라질 수 있는 것은 ‘업캐스팅’과 ‘동적 바인딩’이라는 메커니즘이 작용하기 때문이다.
-
부모 클래스
타입으로 선언된 변수에자식 클래스
의 인스턴스를 할당하는 것이 가능하다(ex: List= new ArrayList<>();). 이를 업캐스팅
이라고 부른다. -
선언된 변수의 타입이 아니라
메시지를 수신하는 객체의 타입에 따라 실행되는 메서드가 결정
된다. 이것은 객체지향 시스템이 메시지를 처리할 적절한 메서드를 컴파일 시점이 아닌 런타임에 결정하기 때문에 가능하다. 이를동적 바인딩
이라고 한다. -
동일한 수신자
에게동일한 메시지
를 전송하는동일한 코드
를 이용하여서로 다른 메서드를 실행
할 수 있는 이유는업캐스팅
과동적 메서드 탐색
이라는 메커니즘이 존재하기 때문이다. -
업캐스팅
은 서로 다른 클래스의 인스턴스를동일한 타입에 할당
하는 것을 가능하게 해준다. 따라서 부모 클래스에 대해 작성된 코드를 전혀 수정하지 않고도 자식 클래스에 적용할 수 있다. -
동적 메서드 탐색
은 부모 클래스의 타입에 대해 메시지를 전송하더라도 실행 시에는 실제 클래스를 기반으로 실행될 메서드가 선택되게 해준다. -
객체지향 언어에서는 메시지를 수신했을 때 실행될 메서드가 런타임에 결정된다. 실행될 메서드를 런타임에 결정하는 방식을
동적 바인딩
이라 부른다. -
메시지를 수신한 객체는 먼저 자신을 생성한 클래스에 적절한 메서드가 존재하는지 탐색한다. 존재하면 메서드를 실행하고 탐색을 종료한다. 만일 존재하지 않는다면 부모 클래스에서 메서드 탐색을 계속한다. 이 과정은 적합한 메서드를 찾을 때 까지 상속 계층을 따라 올라가며 계속된다.
-
객체가 메시지를 수신하면
self 참조
라는 임시 변수를 자동으로 생성한 후메시지를 수신한 객체를 가리키도록 설정
한다. -
동적 메서드 탐색
은self 참조가 가리키는 객체의 클래스
에서 시작하여상속 계층의 역방향
으로 이뤄지며메서드 탐색이 종료되는 순간 self 참조는 자동 소멸
된다. -
시스템은
self 참조
,class pointer
,parent pointer
를 조합하여메서드 탐색
을 한다. -
동적 메서드 탐색
은자동적인 메시지 위임
과동적 문맥
이라는 원리로 구성된다. -
자동적인 메시지 위임
이란 자식 클래스가 자신이 이해할 수 없는 메시지를 전송받은 경우 상속 계층을 따라 부모 클래스에게 메시지 처리를 자동으로 위임 하는 것이다. -
동적 문맥
이란 메시지를 수신했을 때 실제 어떤 메서드를 실행할지는 컴파일 시점이 아닌 런타임에 이뤄진다. 그리고메서드 탐색 경로는 self 참조를 이용해서 결정
한다. -
‘런타임’에 실제로 메시지를 수신한 객체가 어떤 타입인지에 따라 메서드 탐색 경로가 결정된다. 여기서 가장 중요한 역할을 하는 것이 바로 메시지를 수신한 객체를 가리키는 ‘self 참조’이다.
-
동일한 코드라고 하도라도 self 참조가 가리키는 객체가 무엇인지에 따라 메서드 탐색을 위한 상속 계층의 범위가 동적으로 변한다. 따라서
self 참조
가 가리키는객체의 타입을 변경
함으로써 객체가 실행될 문맥을 동적으로 변경할 수 있다. -
자기 자신에게 다시 메시지를 전송하는 것을
self 전송(this.operation())
이라고 부른다. -
self 전송
을 이해하기 위해서는 메서드 호출이 아니라메시지 전송
에 유념해야 한다. -
self 전송
은 self 참조가 가리키는 바로 그 객체에서 부터 메시지 탐색을 다시 시작한다는 사실을 기억해야 한다. -
self 전송
을 통해 메서드 탐색이 처음 시작했던 self 참조가 가리키는 클래스 부터 다시 시작한다. -
self 전송
은 자식 클래스에서 부모 클래스 방향으로 진행되는 동적 메서드 탐색 경로를 다시 self 참조가 가리키는 원래의 자식 클래스로 이동시킨다. -
동적 바인딩
,self 참조
,class pointer
,super 참조
는 상속을 이용해다형성
을 구현하고 코드 재사용을 위한 가장 핵심적인 재료다. -
동적 바인딩
,self 참조
는 동일한 메시지를 수신하더라도 객체의 타입에 따라 적합한 메서드를 동적으로 선택할 수 있게 한다. -
다형성
은self 참조
가 가리키는 현재 객체에게 메시지를 전달하는 특성을 기반으로 한다. 동일한 타입의 객체 참조에게 동일한 메시지를 전달하더라도 self 참조가 가리키는 객체의 클래스가 무엇이냐에 따라 메서드 탐색을 위한 문맥이 달라진다. -
self 참조
가동적인 문맥을 결정
한다는 사실을 이해하고 나면 상속을 바라보는 새로운 시각이 형성된다. 바로 자식 클래스에서 부모 클래스로 ‘self 참조’를 전달하는 메커니즘으로 상속을 바라보는 것이다. -
메서드 탐색
중에는 자식 클래스의 인스턴스와 부모 클래스의 인스턴스가동일한 self 참조를 공유
한다. -
위임
은 본질적으로자신이 정의하지 않거나 처리할 수 없는 속성 또는 메서드 탐색 과정을 다른 객체로 이동시키기 위해 사용
된다. 이를 위해 위임은 항상 현재의 실행 문맥을 가리키는 self 참조를 인자로 전달한다. 이것이self 참조를 전달하지 않는 포워딩과 위임의 차이점
이다. -
위임
의 정확한 용도는 클래스를 이용한 상속 관계를 객체 사이의 합성 관계로 대체해서 다형성을 구현하는 것이다.
다형성
다형성
이란 동일한 메시지에 대해 서로 다른 메서드를 실행할 수 있는 것
이다. 다시말해, 메시지를 수신한 객체의 구체적인 클래스 타입에 따라 실행되는 메서드를 변경할 수 있다.
이러한 메커니즘이 가능한 이유는
- 부모 클래스 타입으로 선언된 변수에 자식 클래스의 인스턴스를 할당하는 것이 가능하게끔 하는
업캐스팅
런타임
에메시지를 처리할 메서드를 결정
하는동적 바인딩
런타임
에 메시지를 수신한 객체에서부터 탐색이 시작되는동적 메서드 탐색
런타임
에 메시지를 수신한 객체를 가리키는 변수인self 참조
- 클래스의 메서드와 같은 메타 정보를 저장하는
Class
와 이 위치를 가리키는class pointer
과 같은 요소들이 있기 때문이다.
아래에서 하나씩 살펴보며 다형성
이 어떤 기술적 메커니즘으로 구현되는지 살펴보자.
업캐스팅
상속
을 이용하면 부모 클래스의 퍼블릭 인터페이스가 자식 클래스의 퍼블릭 인터페이스에 합쳐지기 때문에 부모 클래스의 인스턴스에 전송할 수 있는 메시지를 자식 클래스의 인스턴스에 전송할 수 있다. 즉, 컴파일러는 명시적인 타입 변환이 없어도 자식 클래스가 부모 클래스를 대체할 수 있게 허용한다.
컴파일러의 관점에서 자식 클래스는 아무런 제약 없이 부모 클래스를 대체
할 수 있기 때문에 부모 클래스의 인스턴스와 협력하는 클라이언트는 다양한 자식 클래스의 인스턴스와도 협력이 가능하다.
동적 바인딩
객체지향 시스템에서 메서드를 실행하는 유일한 방법은 메시지를 전송
하는 것이다. 메서드 호출
을 위해서는 메시지 전송
이 먼저 선행되어야 한다.
그리고 객체지향 언어에서는 메시지를 수신했을 때 실행될 메서드가 런타임에 결정된다. foo.bar()
라는 코드를 읽는 것만으로는 실행되는 bar가 어떤 클래스의 어떤 메서드인지
판단하기가 어렵다. foo가 가리키는 객체가 실제로 어떤 클래스의 인스턴스
인지를 알아야 하고, bar() 메서드가 해당 클래스의 상속 계층의 어디에 위치하는지
를 알아야 한다.
이처럼 실행될 메서드를 런타임에 결정
하는 방식을 동적 바인딩
이라고 부른다.
객체지향 언어가 제공하는 업캐스팅
과 동적 바인딩
을 사용하면 부모 클래스 참조 변수에 대한 메시지 전송을 자식 클래스 메서드 호출로 변환
할 수 있다.
동적 메서드 탐색
런타임
에 메시지를 실행할 메서드를 바인딩
하는 것이 동적 바인딩
이라고 하였다. 그리고 이러한 기술적인 메커니즘을 가능하게 하는 규칙이 바로 동적 메서드 탐색
이다.
동적 메서드 탐색
은 동적 바인딩
을 가능하게 하는 규칙이다. 그리고 아래의 규칙에 따라 실행할 메서드를 선택한다.
- 메시지를 수신한 객체(
self 참조가 가리키는 객체
)는 먼저 자신을 생성한 클래스(class pointer
가 가리키는 클래스)에 메시지를 처리할 적합한 메서드가 있는지를 탐색한다. 만일 존재한다면메서드를 실행하고 탐색을 종료
한다. - 메서드를 찾지 못했다면 부모 클래스(
parent pointer
가 가리키는 클래스)에서 메서드 탐색을 계속한다. 이 과정은 적합한 메서드를 찾을 때 까지 상속 계층을 따라 올라가며 계속된다.
self 참조 변수
메서드 탐색
과 관련해서 기억해야 할 중요한 변수가 하나 있다. 바로 메서드 탐색의 시작점이자 경로를 결정하는 ‘self 참조’ 변수 이다.
self 참조
변수는 객체가 메시지를 수신하면 컴파일러가 자동으로 생성하여 메시지를 수신한 객체를 가리키도록 설정한다. 이 때문에 동적 메서드 탐색은 self가 가리키는 객체의 class부터 시작
하여 상속 계층의 역방향으로 이뤄지며 메서드 탐색이 종료되는 순간 self 참조는 자동으로 소멸된다.
시스템은 self 참조
, class pointer
, parent pointer
를 함께 조합하여 메서드를 탐색한다.
동적 메서드 탐색의 두 가지 원리
지금까지의 내용을 종합해보면 동적 메서드 탐색
은 두 가지 원리
로 구성된다는 것을 알 수 있다.
- 자동적인 메시지 위임 : 자식 클래스는 자신이 처리할 수 없는 메시지를 전송받으면 상속 계층을 따라 부모 클래스에게 메시지 처리를 위임한다.
- 동적 문맥 : 메시지를 수신했을 때 어떤 메서드를 실행할지는 컴파일 시점이 아닌
런타임 시점에 결정
되며, 메서드를 탐새하는 경로는 ‘self 참조’를 통해서 결정한다.
메시지가 처리되는 문맥을 이해하기 위해서는 런타임에 실제로 메시지를 수신한 객체가 어떤 타입인지를 추적
해야 한다. 이 객체의 타입에 따라 메서드를 탐색하는 문맥이 동적으로 결정되며, 여기서 가장 중요한 역할을 하는 것이 바로 ‘self’ 참조다.
자동적인 메시지 위임
동적 메서드 탐색
의 관점에서 상속 계층
을 바라본다면, 메시지를 수신한 객체가 자신이 이해할 수 없는 메시지는 부모 클래스에게 전달하기 위한 물리적인 경로를 정의
하는 것으로 볼 수 있다. 상속 계층 안의 클래스는 메시지를 처리할 방법을 알지 못할 경우 메시지에 대한 처리를 부모 클래스에게 위임한다.
여기서 핵심은 적절한 메서드를 찾을 때 까지 상속 계층을 따라 부모 클래스로 처리가 위임된다는 사실이다.
상속
을 사용할 경우 프로그래머가 메시지 위임과 관련된 코드를 명시적으로 작성할 필요가 없다. 메시지는 상속 계층을 따라 부모 클래스에게 자동으로 위임
된다. 이런 관점에서 상속 계층을 정의하는 것은 메서드 탐색 경로를 정의하는 것과 동일하다.
동적인 문맥
중요한 것은 메시지를 수신한 객체
가 무엇이냐에 따라 메서드 탐색을 위한 문맥이 동적으로 바뀐다는 것
이다. 그리고 이 동적인 문맥을 결정하는 것이 메시지를 수신한 객체를 가리키는 ‘self 참조’ 이다.
동일한 코드라고 하더라도 self 참조
가 가리키는 객체가 무엇인지에 따라 메서드 탐색을 위한 상속 계층의 범위가 동적
으로 변한다. 따라서 self 참조
가 가리키는 객체의 타입을 변경함으로써 객체가 실행 될 문맥을 동적으로 변경할 수 있다.
self 전송
객체지향 시스템에서 어떤 메서드 호출 구문을 보았을 때는 항상 메시지 전송
이후 메서드 호출
이 됨을 유념해야 한다.
이를테면 this.method()
는 현재 클래스의 메서드를 호출하는게 아니라 현재의 (self 참조가 가리키는)객체에게 메시지를 전송하는 것이다.
많이 사용되는 패턴인 템플릿 메서드 패턴
을 떠올리면 쉽다. 템플릿 메서드 패턴에서는 추상화된 퍼블릭 인터페이스를 정의하고 부모 클래스에서 호출
한다. 근데 실제로 호출되는 메서드는 추상 메서드를 구현한 자식 클래스의 메서드
가 호출된다. 이는 실제로 메시지를 수신한 객체가 자식 클래스의 객체로, ‘self 참조’가 가리키는 객체는 자식 클래스의 객체이기 때문이다.
이와 같이 어떤 클래스에서 자신의 메서드를 호출하는 것 처럼 보이더라도 런타임에 ‘self 참조’가 가리키는 객체가 무엇이냐에 따라 자식 클래스의 메서드가 호출될 수 있다.
결론적으로 self 전송을 이해하기 위해서는 self 참조가 가리키는 바로 그 객체(메시지를 수신한 객체)에서 부터 메시지 탐색을 다시 한다는 사실을 이해해야 한다.
상속 vs 위임
지금까지 살펴본 바와 같이 다형성
은 self 참조
가 가리키는 현재 객체에게 메시지를 전달하는 특성을 기반으로 한다.
동일한 타입의 객체 참조(참조 변수)에게 동일한 메시지를 전송하더라도 런타임에 self 참조가 가리키는 객체의 클래스가 무엇이냐에 따라 메서드 탐색을 위한 문맥이 달라진다.
일단 self 참조가 동적인 문맥을 결정
한다는 사실을 이해하고 나면 상속
을 바라보는 새로운 시각이 형성된다.
바로 자식 클래스에서 부모 클래스로 ‘self 참조’를 전달하는 메커니즘으로 상속을 바라보는 것이다.
아래와 같은 Ruby
로 구현한 코드를 살펴보자. 미리 말하지만 클래스 기반의 Java
와 같은 객체지향 언어에서는 본질적인 위임
을 코드로 구현하기 어렵다.
위임
은 본질적으로 자신이 정의하지 않거나 처리할 수 없는 속성 또는 메서드 탐색 과정을 다른 객체로 이동시키기 위해 사용된다. 이를 위해 위임
은 항상 현재의 실행 문맥을 가리키는 self 참조를 인자로 전달
한다.
다시말해 위임
은 객체 사이의 동적인 연결 관계를 이용해 ‘상속’을 구현하는 방법 이다.
나머지
이번 장에서는 다형성
을 구현하기 위해 상속
이 어떠한 메커니즘을 갖는지와 위임
이 갖는 의미에 대해서 살펴보았다.
클래스
기반의 객체지향 언어든, 프로토타입
기반의 객체지향 언어든 본질은 같다.
메시지를 수신한 객체
를 가리키는self 참조
로 부터런타임에 메서드를 동적으로 탐색하기 위한 경로인 동적인 문맥
이 형성되고,- 탐색 과정에서 필요에 따라
부모 클래스에게 self 참조를 전달하여 메시지 처리를 위임
하는 과정을 통해 다형성이 구현된다.
참고 및 출처
- 오브젝트