공부방/북스터디

[오브젝트] 12장

midcon 2024. 3. 26. 03:04

12장 정리

코드 재사용을 목적으로 상속을 사용하면 변경하기 얼벼고 유연하지 못한 설계에 이를 확률이 높아진다.
상속의 목적은 코드 재사용이 아니다. 상속은 타입 계층을 구조화하기 위해 사용해야한다.
클라이언트 관점에서 인스턴스들을 동일한 행동 그룹군으로 묶기 위해서 사용해야한다.
이번 장에서는 상속의 관점에서 다형성이 구현되는 기술적인 메커니즘을 살펴본다.
다형성이 런타임에 메시지를 처리하기에 적합한 메서드를 동적으로 탐색하는 과정을 통해 구현되며, 상속이 이런 메서드를 찾기 위한 일종의 탐색 경로를 클래스 계층의 형태로 구현하기 위한 방법임을 이해할 수 있을 것이다.


다형성

다형성이라는 단어는 '많은 형태를 가질 수 있는 능력'을 의미한다.

  • 오버로딩 다형성: 메서드 오버로드
  • 강제 다형성: + 연산자의 피연산자에 따른 타입 강제
  • 매개변수 다형성: 제네릭
  • 포함 다형성: 인터페이스 -> 가장 일반적으로 의미하는다형성 = 서브타입 다형성

상속의 진정한 목적은 코드 재사용이 아닌 다형성을 위한 서브타입 계층을 구축하는 것이다.

상속의 양면성

객체지향의 패러다임에서는 데이터와 행동을 객체라는 하나의 실행 단위 안으로 통합한다. 

따라서 객체 지향 프로그램을 작성하기 위해서는 항상 데이터와 행동이라는 두가지 관점을 함께 고려해야 한다.
단순히 데이터와 행동의 관점에서 보면 상속은 부모 클래스에서 정의한 데이터와 행동을 자식 클래스에서 자동으로 공유할 수있는 재사용 메커니즘으로 보인다.
하지만 상속의 목적은 재사용이 아니라 프로그램을 구성하는 개념들을 기반으로 다형성을 가능하게 하는 타입 계층을 구축하는 것이다.
타입 계층에 대한 고민 없이 코드를 재사용하기 위해 상속을 사용하면 이해하기 어렵고 유지보수하기 버거운 코드가 만들어질 확률이 높다.

데이터 관점의 상속

데이터 관점에서 상속은 자식 클래스의 인스턴스 안에 부모 클래스의 인스턴스를 포함하는 것으로 볼 수 있다.
따라서 자식 클래스의 인스턴스는 자동으로 부모 클래스에서 정의한 모든 인스턴스 변수를 내부에 포함하는 것이다.

행동 관점의 상속

행동 관점의 상속은 부모 클래스가 정의한 일부 메서드를 자식 클래스의 메서드로 포함시키는 것을 의미한다.
부모 클래스의 모든 퍼블릭 메서드는 자식 클래스의 퍼블릭 인터페이스에 포함된다.
런타임 시점에 자식 클래스에 정의되지 않은 메서드가 있을 경우 이 메서드를 부모 클래스 안에서 탐색하기 때문에 외부의 객체가 부모 클래스의 인스턴스에게 전송할 수 있는 모든 메시지는 자식 클래스의 인스턴스에게도 전송할 수 있다.
위의 그림에서 보듯 메시지를 수신한 객체는 class 포인터로 연결된 자신의 클래스에서 적절한 메서드가 존재하는지를 찾고, 메서드가 존재하지 않으면 클래스의 parent 포인터를 따라 부모 클래스를 차례대로 훑어가면서 적절한 메서드가 존재하는지 탐색한다.

업캐스팅과 동적 바인딩

같은 메시지, 다른 메서드

  • 업캐스팅: 부모 클래스 타입으로 선언된 변수에 자식 클래스의 인스턴스를 할당하는 것
  • 동적 바인딩: 선언된 변수의 타입이 아니라 메시지를 수신하는 객체의 타입에 따라 실행되는 메서드가 결정되는 것

동일한 수신자에게 동일한 메시지를 전송하는 동일한 코드로 서로 다른 메서드를 실행할 수 있는 이유는 업캐스팅과 동적 메서드 탐색이라는 기반 메커니즘이 존재하기 때문이다.
이 두가지를 통해 코드를 변경하지 않더라도 실행되는 메서드를 변경할 수 있다.

업캐스팅

컴파일러의 관점에서 자식 클래스는 아무런 제약 없이 부모 클래스를 대체할 수 있기 때문에 부모 클래스와 협력하는 클라이언트는 현재 상속 계층에 존재하는 자식 클래스 뿐 아니라 앞으로 추가 될 미래의 자식 클래스의 인스턴스와도 협력이 가능하다.
이를 통해 유연하며 확장 용이한 설계가 가능하다.

동적 바인딩

함수를 호출하는 전통적인 언어에서는 컴파일 타임에 호출될 함수를 결정하지만 객체지향 언어에서는 메시지를 수신했을 때 실행될 메서드가 런타임에 결정된다.

동적 메서드 탐색과 다형성

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

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

메시지 탐색에서 self 참조란 객체가 메시지를 수신하면 컴파일러는 self 참조라는 임시 변수를 자동으로 생성한 후 메시지를 수신한 객체를 가리키도록 설정한다.
동적 메서드 탐색을 self가 가리키는 객체의 클래스에서 시작해서 상속 계층을 타고 올라가면서 이루어지고, 메서드 탐색이 종료되는 순간 self 참조는 자동으로 소멸된다.
동적 메서드 탐색은 자동적인 메시지 위임동적인 문맥 사용의 두가지 원리로 구성된다.

자동적인 메시지 위임

메시지를 수신한 객체가 자신이 이해할 수 없는 메시지인 경우 부모 클래스에 처리를 위임한다.
상속을 이용할 경우 프로그래머가 메시지 위임과 관련된 코드를 명시적으로 작성하지 않아도 자동으로 위임된다.

동적인 문맥

메시지를 수신학 객체가 무엇이냐에 따라 메서드 탐색을 위한 문맥이 동적으로 바뀐다. 그리고 이 문맥을 결정하는 것은 메시지를 수신한 객체를 가리키는 self 참조다.
동일한 코드라고 하더라도 self 참조가 가리키는 객체가 무엇인지에 따라 메서드 탐색을 위한 상속 계층의 범위가 동적으로 변하므로 self 참조가 가리키는 객체의 타입을 변경함으로써 객체가 실행될 문맥을 동적으로 바꿀 수 있다.
self 전송은 자식 클래스에서 부모 클래스 방향으로 진행되는 동적 메서드 탐색 경로를 다시 self 참조가 가리키는 원래의 자식 클래스로 이동시킨다.
따라서 self 전송으로 인해 극단적으로 이해하기 어려운 코드가 만들어질 수 있다.

self와 super

self 참조는 메시지를 수신한 객체의 클래스에 따라 메서드 탐색을 위한 문맥을 실행 시점에 결정하며, 어떤 클래스에서 메시지 탐색이 시작될지 알지 못한다.
super 참조는 항상 해당 클래스의 부모 클래스에서부터 메서드 탐색을 시작한다.

상속 대 위임

위임과 self 참조

self 참조가 동적인 문맥을 결정한다는 사실을 이해하고 나면 상속을 자식 클래스에서 부모 클래스로 self 참조를 전달하는 메커니즘으로 바라볼 수 있다.
self 참조는 항상 메시지를 수신한 객체를 가리키므로 메서드 탐색 중에는 자식 클래스의 인스턴스와 부모 클래스의 인스턴스가 동일한 self 참조를 공유하는 것으로 볼 수 있다.


인상깊었던 점

"상속은 타입 계층을 구조화하기 위해 사용해야한다."
이번 장에서 계속 강조했던 문장이었던것 같다.
상속의 관점에서 다형성이 구현되는 메커니즘을 살펴보면서 self 참조와 super 참조에 대해 알 수 있었다.
또한 self 전송의 설명을 통해 상속을 통해 다형성을 구현할 때는 내부 구현을 알아야 하기 때문에 단순히 코드 재사용 측면에서 상속을 이용하기에는 위험할 수 있다는 생각이 들었다.