12장 다형성

오브젝트의 12장을 요약한 내용 입니다.

Intro

  • 상속의 목적은 코드 재사용이 아니다.

  • 상속은 타입 계층을 구조화하기 위해 사용해야 한다.

  • 타입 계층은 객체 지향 프로그래밍의 중요한 특성 중 하나인 다형성의 기반을 제공한다.

  • 상속을 이용해 자식 클래스를 추가하려 한다면 스스로에게 다음과 같은 질문을 해보자

    • 단순히 코드를 재사용하기 위해서인가? → 예, 라고 생각한다면 상속을 사용하지 말아라

    • 인스턴스들을 동일하게 행동하는 그룹으로 묶기 위해서인가? → 예, 라고 생각한다면 사용해도 된다.

  • 많은 시간이 흐른 지금도 여전히 상속은 다형성을 구현할 수 있는 가장 일반적인 방법이다.

    하지만 최근의 언어들은 상속 이외에도 다형성을 구현할 수 있는 다양한 방법들을 제공하고 있기 때문에 과거에 비해 상속의 중요성이 많이 낮아졌다고 할 수 있다.

  • 이번 장을 통해서 다형성이 런타임에 메시지를 처리하기에 적합한 메서드를 동적으로 탐색하는 과정을 통해 구현 되며 상속이 이런 메서드를 찾기 위한 일종의 탐색 경로를 클래스 계층의 형태로 구현하기 위한 방법이라는 사실을 이해할 것이다.

다형성

  • 다형성이라는 단어는 ‘많은’을 의미하는 ‘poly’와 ‘형태’를 의미하는 ‘morph’의 합성어로 ‘많은 형태를 가질 수 있는 능력’을 의미한다.

  • 다형성의 분류

    • 유니버셜 : 타입이 달라도 같은 code를 수행

      • 매개변수 : 제네릭 프로그래밍과 관련이 높은데 클래스의 인스턴스 변수나 메서드의 매개변수 타입을 임의의 타입으로 선언한 후 사용하는 시점에 구체적인 타입으로 지정하는 방식 ex) List

      • 포함 : 메시지가 동일 하더라도 수신한 객체의 타입에 따라 실제로 수행되는 행동이 달라 지는 기능 Ex) discountPolicy.calculate()

    • 임시 : 타입이 다르면 다른 version의 code를 수행

      • 오버로딩 : 하나의 클래스 안에 동일한 이름의 메서드가 존재하는 경우 ex) plus(int a), plus(double a)

      • 강제 : 언어가 지원하는 자동적인 타입 변환이나 사용자가 직접 구현한 타입 변환을 이용해 동일한 연산자를 다양한 타입에 사용할 수 있는 방식 ex) ‘+’ 연산자 → 정수형 + 정수형, 문자열 + 정수형

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

상속의 양면성

  • 객체지향 프로그램을 작성하기 위해서는 항상 데이터행동이라는 두 가지 관점을 함께 고려해야 한다.

  • 데이터 관점의 상속 : 상속을 이용하면 부모 클래스에서 정의한 모든 데이터를 자식 클래스의 인스턴스에 자동으로 포함시킬 수 있다.

  • 행동 관점의 상속 : 데이터뿐만 아니라 부모 클래스에서 정의한 일부 메서드 역시 자동으로 자식 클래스에 포함시킬 수 있다.

  • 단순히 데이터와 행동의 관점에서만 바라보면 상속이란 부모 클래스에서 정의한 데이터와 행동을 자식 클래스에서 자동적으로 공유할 수 있는 재사용 매커니즘으로 보일 것이다. 하지만 이 관점은 상속을 오해한 것이다.

업캐스팅과 동적 바인딩

같은 메시지, 다른 메서드

  • 업캐스팅 : 부모 클래스 타입으로 선언된 변수에 자식 클래스의 인스턴스를 할당하는 것이 가능하다.

  • 동적 바인딩 : 메시지를 처리할 적절한 메서드를 컴파일 시점이 아니라 실행 시점에 결정한다.

  • 업캐스팅동적 메서드 탐색은 코드를 변경하지 않고도 기능을 추가할 수 있게 해주며 이것은 개방-폐쇄 원칙의 의도 와도 일치한다.

동적바인딩

  • 함수 호출메시지 전송 사이의 차이는 생각보다 큰데 프로그램 안에 작성된 함수 호출 구문과 실제로 실행되는 코드를 연결하는 언어적인 매커니즘이 완전히 다르기 때문이다.

  • 함수 호출 : 컴파일 타임에 결정 (정적 바인딩, 초기 바인딩, 컴파일타임 바인딩)

  • 메시지 전송 : 실행될 메서드를 런타임에 결정 (동적 바인딩, 지연바인딩)

동적 메서드 탐색과 다형성

  • 동적 메서드 탐색은 self가 가리키는 객체의 클래스에서 시작해서 상속 계층의 역방향으로 이뤄지며 메서드 탐색이 종료되는 순간 self 참조는 자동으로 소멸된다.

  • 동적 메서드 탐색 원리

    • 자동적인 메시지 위임 : 자식 클래스는 자신이 이해할 수 없는 메시지를 전송받은 경우 상속 계층에 따라 부모 클래스에게 처리를 위임한다.

    • 동적인 문맥 : 메시지를 수신 했을 때 실제로 어떤 메서드를 실행 할지를 결정하는 것은 컴파일 시점이 아닌 실행 시점에 이뤄지며, 메서드를 탐색하는 경로는 self 참조를 이용해서 결정한다.

동적인 문맥

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

    public String getEvaluationMethod() {
        return "Pass or Fail";
    }
}
  • getEvaluationMethod 메서드를 호출한다고 표현했지만 사실 이 말은 정확하지는 않다.

  • 현재 클래스의 메서드를 호출하는 것이 아니라 현재 객체에게 메시지를 전송하는 것이다.

  • 그렇다면 현재 객체한 무엇인가? 바로 self 참조가 가리키는 객체다. 이 객체는 처음에 stats 메시지를 수신했던 바로 그 객체다.

    public class GradeLecture extends Lecture {
      @Override
      public String getEvaluationMethod() {
          return "Grade";
      }
    }
  • GradeLecture 클래스에 stats 메시지를 전송하면 Lecture 클래스의 stats 메서드를 실행하는 중에 self 참조가 가리키는 객체에게 getEvaluationMethod 메시지를 전송하는 구문과 마주치게 된다. 이제 메서드 탐색은 self 참조가 가리키는 객체에서 시작된다. 여기서 self 참조가 가리키는 객체는 바로 GradeLecture의 인스턴스다. 따라서 메시지 탐색은 Lecture 클래스를 벗어나 self 참조가 가리키는 GradeLecture에서 부터 다시 시작된다.

  • 417, 419 페이지 참조

이해할 수 없는 메시지

  • 정적 타입 언어에서는 코드를 컴파일할 때 상속 계층 안의 클래스들이 메시지를 이해할 수 있는지 여부를 판단한다.

  • 동적 타입 언어 역시 메시지를 수신한 객체의 클래스부터 부모 클래스의 방향으로 메서드를 탐색한다. 차이점이라면 동적 타입 언어에는 컴파일 단계가 존재하지 않기 때문에 실제로 코드를 실행 해보기 전에는 메시지 처리 가능 여부를 판단할 수 없다는 점이다.

    • 만약 상속 계층 안의 어떤 클래스도 메시지를 처리할 수 없다면 메서드 탐색은 다시 한번 최상위 클래스에 이르게 되고 최종적으로 예외 메시지를 던지게 된다.

    • doesNotUnderstand(스몰토크)나 method_missing(루비) 메시지에 응답할 수 있는 메서드를 구현할 수 있다.

  • 이해할 수 없는 메시지를 처리할 수 있는 동적 타입 언어의 특징은 메타 프로그래밍 영역에서 진가를 발휘한다. 마틴 파울러는 동적 타입 언어의 이러한 특징을 이용해 도메인-특화 언어를 개발하는 방식을 동적 리셉션이라고 부른다.

self 대 super

  • self 참조의 가장 큰 특징은 동적이라는 점이다. self 참조는 메시지를 수신한 객체의 클래스에 따라 메서드 참색을 위한 문맥을 실행 시점에 결정한다.

  • super 참조는 부모 클래스에서부터 메서드 탐색을 시작한다는 의미이다.

상속 대 위임

위임과 self 참조

  • 메서드 탐색 중에는 자식 클래스의 인스턴스와 부모 클래스의 인스턴스가 동일한 self 참조를 공유하는 것으로 봐도 무방하다.

  • 따라서 그림 12.20을 그림 12.21과 같이 바꿀 수 있다. 그리고 상속 계층을 구성하는 객체들 사이에서 self 참조를 공유하기 때문에 개념적으로 각 인스 턴스에서 self 참조를 공유하는 self라는 변수를 포함하는 것처럼 표현할 수 있다.

  • 426p 참고

Last updated