12 다형성
평소 자주 사용하는 다형성에 대한 장이였습니다. 이번 장에서는 상속에 대한 의미를 알 수 있었고 특히 오버라이딩혹은 오버로딩이 객체지향 프로그래밍 내부적으로 어떻게 동작하는지 알 수 있었습니다. 또한 종종 사용하던 this에 super가 어떠한 방식으로 구현되는지 역시 알 수 있었습니다. 백엔드 개발자는 코드를 작성하면서 다형성과 뗄래야 뗄 수 없는 관계라 생각합니다. 그렇기에 기억에 많이 남는 장이였습니다.
상속은 타입 계층을 구조화하기 위해 사용해야 한다.
이전 장에서도 상속은 코드 재사용을 위해서 사용되어서는 안된다고 말을 하였습니다. 그리고 12장 다형성에서 상속이 주는 의미에 진정한 의미에 대해 이야기를 하고 있습니다. 또한 타입 계층을 구조화한다는 말은 추후 나올 내용에 대해 미리 귀띔을 한다 생각을 하면 될듯 합니다.
01 다형성
컴퓨터 과학에서는 다형성을 하나의 추상 인터페이스에 대해 코드를 작성하고 이 추상 인터페이스에 대해 서로 다른 구현을 연결할 수 있는 능력으로 정의한다.
...
일반적으로 하나의 클래스 안에 동일한 이름의 메서드가 존재하는 경우를 가리키 오버로딩 다형성이라고 부른다.
...
강제 다형성은 언어가 지원하는 자동적인 타입 변환이나 사용자가 직접 구현한 타입 변환을 이용해 동일한 연산자를 다양한 타입에 사용할 수 있는 방식을 가리킨다.
...
매개변수 다형성은 제네릭 프로그래밍과 관련이 높은데 인스턴스 변수나 메서드의 매개변수 타입을 임의의 타입으로 선언한 후 사용하는 시점에 구체적인 타입으로 지정하는 방식을 가리킨다.
...
포함 다형성은 메시지가 동일하더라도 수신한 객체의 타입에 따라 실제로 수행되는 행동이 달라지는 능력을 말한다.
...
포함 다형성을 위해 상속을 사용하는 가장 큰 이유는 상속이 클래스들을 계층을 쌓아 올린 후 상황에 따라 적절한메서드를 선택할 수 있는 메커니즘을 제공하기 때문이다.
다형성의 분류에 대해 이야기를 하고 있습니다. 일반적으로 다형성이라 함은 포함 다형성을 말하며 이번 장에서는 다형성에 대해 주로 이야기를 하고 있습니다.
02 상속의 양면성
상속의 목적은 코드 재사용이 아니다. 상속은 프로그래밍을 구성하는 개념들을 기반으로 다형성을 가능하게 하는 타입 계층을 구축하기 위한 것이다.
위에서 이미 말한 내용이지만 반복해서 나오는 내용이며, 중요하다 생각하기에 다시 한번 더 언급하였습니다.
상속을 사용한 강의 평가
이처럼 자식 클래스 안에 상속받은 메서드와 동일한 시그니처의 메서드를 재정의해서 부모 클래스의 구현을 새로운 구현으로 대체하는 것을 메서드 오버라이딩이라고 부른다.
...
부모 클래스에서 정의한 메서드와 이름은 동일하지만 시그니처는 다른 메서드를 자식 클래스에 추가하는 것을 메서드 오버로딩이라고 부른다.
다형성을 이루는 기술적인 방법에 대해 이야기하고 있습니다. 그리고 이는 메시지 오버라이딩과 오버로딩입니다.
데이터 관점의 상속
요약하면 데이터 관점에서 상속은 자식 클래스의 인스턴스 안에 부모 클래스의 인스턴스를 포함하는 것으로 볼 수 있다. 따라서 자식 클래스의 인스턴스는 자동으로 부모 클래스에서 정의한 모든 인스턴스 변수를 내부에 포함하게 되는 것이다.
상속을 하면 가장 먼저 떠올리는 내용입니다. 기본적으로 상속을 할 경우 자식 클래스는 부모 클래스가 가지고 있는 속성들을 사용할 수 있게 됩니다.(일반적인 경우에만 그렇습니다. 만약 접근 제어자가 private이라면 상속을 하지 못하게 됩니다.)
행동 관점의 상속
행동 관점의 상속은 부모 클래스가 정의한 일부 메서드를 자식 클래스의 메서드로 포함시키는 것을 의미한다.
...
런타임에 시스템이 자식 클래스에 정의되지 않은 메서드가 있을 경우 이 메서드를 부모 클래스 안에서 탐색하기 때문이다.
...
객체의 경우에는 서로 다른 상태를 저장할 수 있도록 각 인스턴스별로 독립적인 메모리를 할당받아야 한다. 하지만 메서드의 경우에는 동일한 클래스의 인스턴스끼리 공유가 가능하기 때문에 클래스는 한 번만 메모리에 로드하고 각 인스턴스 별로 클래스를 가리키는 포인터를 갖게 하는 것이 경제적이다.
...
메시지를 수신한 객체는 class 포인터로 연결된 자신의 클래스에서 적절한 메서드가 존재하는지를 찾는다. 만약 메서드가 존재하지 않으면 클래스의 parent 포인터를 따라 부모 클래스를 차례대로 훑어가면서 적절한 메서드가 존재하는지를 검색한다.
행동 즉 메서드의 측면에서 바라보았을 때의 상속을 이야기하고 있습니다. 이 역시 상속을 떠올리면 ,데이터 관점처럼, 자연스레 따라오는 아이디어중 하나입니다. 그리고 개념적으로는 위의 그림과 같이 됩니다. 이는 추후 서술한 자기 참조를 이해하는데 큰 도움이 된다 생각합니다.
03 업캐스팅과 동적 바인딩
같은 메시지, 다른 메서드
선언된 변수의 타입이 아니라 메시지를 수신하는 객체의 타입에 따라 실행되는 메서드가 결정된다. 이것은 객체지향 시스템이 메시지를 처리할 적절한 메서드를 검파일 시점이에 결정하기 때문에 가능하다. 이를 동적 바인딩이라고 부른다.
...
업캐스팅은 서로 다른 클래스의 인스턴스를 동일한 타입에 할당하는 것을 가능하게 해준다.
...
동적 메서드 탐색은 부모 클래스의 타입에 대해 메시지를 전송하더라도 실행 시에는 실제 클래스를 기반으로 실행될 메서드가 선택되게 해준다.
...
개방-폐쇄 원칙이 목적이라면 업캐스팅과 동적 메서드 탐색은 목적에 이르는 방법이다.
일반적으로 다형성을 생각할때 가장 먼저 떠올리는 같은 메시지에 대해 다르게 반응하는 것에 대한 이야기입니다. 그리고 이를 가능케 하는 것이 같은 업캐스팅 즉 부모 클래스 타입의 참조 변수에 자식 클래스 타입의 객체를 할당하는 것과 동적으로 메서드를 탐색하여 자식 클래스에 없더라도 클라이언트 입장에서는 이를 모르게 하게 하는 것입니다.
업 캐스팅
업 캐스팅으로 인해 미래의 자식 클래스들도 협력에 참여할 수 있게 된다.
클라이언트 입장에서는 실제 객체가 누구든 메시지에 대해 응답할 수 있을거라 믿고 보내기 때문에 형식만 맞다면 메시지를 전송할 수 있습니다. 그리고 이를 기술적으로 가능하게 하는 것이 업 캐스팅입니다.
동적 바인딩
이처럼 컴파일 타입에 호출할 함수를 결정하는 방식을 정적 바인딩(static binding), 초기 바인딩(early binding), 또는 컴파일 타임 바인딩(compile-time binding)이라고 부른다.
...
이처럼 실행될 메서드를 런타임에 결정하는 방식을 동적 바인딩(dynamic) 또는 지연 바인딩(late binding)이라고 부른다.
전통적인 프로그래밍에서는 정적 바인딩을 지원합니다. 그리고 이는 코드에 결합되어 코드에 명시되어 있는 내용만 수행될 수 있습니다. 반면 객체지향 프로그래밍에서는 컴파일이 아닌 런타임이 코드에 적혀있는 내용을 수행하며 이를 동적으로 변경할 수 도 있습니다.
04 동적 메서드 탐색과 다형성
객체 지향 시스템은 다음 규칙에 따라 실행할 메서드를 선택한다.
- 메시지를 수신한 객체는 먼저 자신을 생성할 클래스에 적합한 메서드가 존재하는지 검사한다. 존재하면 메서드를 실행하고 탐색을 종료한다.
- 메서드를 찾지 못했다면 부모 클래스에서 메서드 탐색을 계속한다. 이 과정은적합한 메서드를 찾을 때까지 상속 계층을 따라 올라가며 계속 된다.
- 상속 계층의 가장 최상위 클래스에 이르렀지만 메서드를 발견하지 못한 경우 예외를 발생시키며 탐색을 중단한다.
...
self 참고(self reference)가 바로 그것이다. 객체가 메시지를 수신하면 컴파일러는 self 참조라는 임시 변수를 자동으로 생성한 후 메시지를 수신한 객체를 가리키도록 설정한다.
...
동적 메서드 탐색은 두 가지 원리로 구성된다.
1. 자동적인 메시지 위임이다. 자식 클래스는 자신이 이해할 수 없는 메시지를 전송받은 경우 상속 꼐층을 따라 부모 클래스에게 처리를 위임한다.
2. 메서드를 탐색하기 위해 동적인 문맥을 사용한다는 것이다. 메시지를 수신했을 때 실제로 어떤 메서드를 실행할지를 결정하는 것은 컴파일 시점이 아닌 실행 시점에 이뤄지며 메서드를 탐색하는 경로는 self 참조를 이용해서 결정한다.
저는 해당 대목이 가장 인상 깊었습니다. 그 동안 자바의 this혹은 super 키워드에 대해 깊은 이해없이 사용하였지만 12장을 계기로 세부적인 구현에 대해 알게 되었습니다. 특히 동직인 문맥을 이용해서 메서드를 탐색한다는 점이 인상 깊었습니다.
자동적인 메시지 위임
여기서 핵심은 적절한 메서드를 찾을 때까지 상속 계층을 따라 부모 클래스로 처리가 위임된다는 것이다.
...
메시지는 상속 계층을 따라 부모 클래스에게 자동으로 위임된다.
...
자식 클래스와 부모 클래스 양쪽 모두에 동일한 시그니처를 가진 메서드가 구현돼 있다면 자식 클래스의 메서드가 먼저 검색된다. 따라서 자식 클래스의 메서드가 부모 클래스의 메서드를감추는 것 처럼 보이게 된다.
자식 클래스에서 메시지를 처리할 수 있는 메서드가 없으면 찾을 때 까지 부모 클래스로 쭈욱 타고 올라갑니다. 그리고 어떤 클래스이든 상관없이 메시지를 처리할 수 있다면 처리합니다.
메시지 오버로딩
다시 말해서 클라이언트의 관정에서 오버로딩된 모든 메서드를 호출할 수 있는 것이다.
어디서 오버로딩이 되었든 상관없이 해석만 할 수 있다면 호출이 가능합니다.
동적인 문맥
메시지를 수신한 객체가 무엇이냐에 따라 메서드 탐색을 위한 문맥이 동적으로 바뀐다는 것이다. 그리고 이 동적인 문맥을 결정하는 것은 바로 메시지를 수신한 객체를 가리키는 self 참조이다.
...
현재 클래스의 메서드를 호출하는 것이 아니라 현재 객체에게 메시지를 전송하는 것이다
...
self 전송을 이해하기 위해서는 self 참조가 가리키는 바로 그 객체에서부터 메시지 탐색을 다시 시작한다는 사실을 기억해야 한다.
...
메소드 탐색은 처음에 메시지 탐색을 시작했던 self 참조가 가리키는 바로 그 클래스에서부터 다시 시작한다는 것이다.
가장 알짜배기인 부분이라 생각합니다. 메서드 탐색을 받은 객체부터 탐색을 진행하고 만약 자기 참조가 일어난다면 자기 참조가 일어난 객체에서 메서드 탐색을 수행하는 것이 아닌 처음 메시지를 전송받은 클래스의 객체부터 시작하는게 핵심인것 같습니다.
이해할 수 없는 메시지
상속 계층 전체를 탐색한 후에도 처리할 수 있는 메서드를 발견하지 못했다면 컴파일 에러를 발생시킨다.
...
몇 가지 동적 타입 언어는 최상위 클래스까지 메서드를 탐색한 후에 메서드를 처리할 수 없다는 사실을 발견하면 self 참조가 가리키는 현재 객체에게 메시지를 이해할 수 없다는 메시지를 전송한다.
메서드 탐색과 이어지는 이야기입니다. java로 예를 들자면 object 클래스까지 타고 올라가도 찾을 수 없다면 예외를 발생시킵니다.
self 대 super
super 참조의 정확한 의도는 '지금 이 클래스의 부모 클래스에서부터 메서드 탐색을 시작하세요'다.
...
super 전송(super send)이라고 부른다.
...
self 전송이 메시지를 수신하는 객체의 클래스에 따라 메서드를 탐색할 시작 위치를 동적으로 결정하는데 비해 super 전송은 항상 메시지를 전송하는 클래스의 부모 클래스에서부터 시작된다.
self는 메시지를 전송받은 객체부터 탐색을 하기에 문맥에 따라 시작 위치가 동적으로 변화합니다. 그리고 super는 객체와 상관없이 항상 해당 클래스의 부모 클래스부터 탐색을 시작한다는 차이가 존재합니다.
05 상속 대 위임
메서드 탐색 중에는 자식 클래스의 인스턴스와 부모 클래스의 인스턴스가 동일한 self 참조를 공유하는 것으로 봐도 무방하다.
...
이처럼 자신이 수신한 메시지를 다른 객체에게 동일하게 전달해서 처리를 요청하는 것을 위임이라고 부른다.
포워딩과 위임
객체가 다른 객체에게 요청을 처리할 때 인자로 self를 전달하지 않을 수도 있다. 이것은 요청을 전달받은 최초의 객체에 다시 메시지를 전송할 필요는 없고 단순히 코드를 재사용하고 싶은 경우라고 할 수 있다. 이처럼 처리를 요청할 때 self 참조를 전달하지 않는 경우를 포워딩이라고 부른다. 이와 달리 self 참조를 전달하는 경우에는 위임이라고 부른다. 위임의 정확한 용도는 클래스를 이용한 상속 관계를 객체 사이의 합성 관계로 대체해서 다형성을 구현하는 것이다.
위임에 대해 이야기하고 있습니다. 사실 이 장의 제목인 상속, 위임을 보았을 때 둘의 차이가 명확하지 않았습니다. 그러나 책 덕분에 self 참조의 유무에 따라 달라진다는 것을 알게 되었습니다. 그리고 이를 기술적으로 들어가면 포워딩과 위임으로 나누어 질 수 있다는 것 역시 깨닫게 되었습니다.