Ch.12 다형성

Previously...

객체 지향 프로그래밍에서 사용되는 기법인 상속과 합성에 대하여 알아보았다.

하지만 11장에서 본 것처럼 코드 재사용을 목적으로 상속을 사용하면 변경하기 어렵고 유연하지 못한 설계에 이를 확률이 높아진다.

About

이번 장에서는 상속의 관점에서 다형성이 구현되는 기술적인 메커니즘을 살펴본다.

다형성

Poly(많은) + Morph(형태)의 합성어. (Greek)

= "많은 형태(Polymorph)를 가질 수 있는 능력"을 의미한다.

컴퓨터 과학에서는 다형성을 "하나의 추상 인터페이스에 대해 서로 다른 구현을 연결할 수 있는 능력"으로 정의한다.

간단히 말해서 "여러 타입을 대상으로 동작할 수 있는 코드를 작성할 수 있는 방법".

오버로딩 다형성 (Overloading)

하나의 클래스 안에 동일한 이름의 메서드가 존재하는 경우.

  • e.g., plus(Money amount), plus(BigDecimal amount), ...

강제 다형성 (Coercion)

언어가 지원하는 자동적인 타입 변환이나 사용자가 직접 구현한 타입 변환을 이용해 동일한 연산자를 다양한 타입에 사용할 수 있는 방식.

  • e.g., Java - "1" + 1 -> +가 연결 연산자로 동작하여 String + String으로 강제 형변환 -> "11"

매개변수 다형성 (Parametric)

제네릭 프로그래밍과 관련이 높다.

클래스 인스턴스 변수나 메서드의 매개변수 타입을 임의의 타입으로 선언한 후 사용한 시점에 구체적인 타입으로 지정하는 방식을 가리킨다.

포함 다형성 (Inclusion) / 서브타입 다형성 (Subtype)

  • 메시지가 동일하더라도 수신한 객체의 타입에 따라 실제로 수행되는 행동이 달라지는 능력.

  • 객체지향에서 가장 널리 알려진 형태의 다형성으로, 다형성이라 하면 통상적으로 포함 다형성을 일컫는다.

상속

상속의 목적은 코드 재사용이 아니라 "프로그램을 구성하는 개념들을 기반으로 다형성을 가능하게 하는 타입 계층을 구축하기 위한 것"이다.

상속의 메커니즘을 이해하는 데 필요한 몇 가지 개념을 살펴보자.

  • 업캐스팅

  • 동적 메서드 탐색

  • 동적 바인딩

  • self 참조

  • super 참조

메서드 오버라이딩

자식 클래스 안에 상속받은 메서드와 동일한 시그니처의 메서드를 재정의해서 부모 클래스의 구현을 새로운 구현으로 대체하는 것.

메서드 오버로딩

부모 클래스에서 정의한 메서드와 이름은 동일하지만 시그니처는 다른 메서드를 자식 클래스에 추가하는 것.

데이터 관점의 상속

자식 클래스의 인스턴스 안에 부모 클래스의 인스턴스를 포함하는 것.

행동 관점의 상속

부모 클래스가 정의한 일부 메서드를 자식 클래스의 메서드로 포함시키는 것.

  • 객체의 경우에는 서로 다른 상태를 저장할 수 있도록 각 인스턴스별로 독립적인 메모리를 할당받아야 한다.(힙 영역)

  • 메서드의 경우에는 동일한 클래스의 인스턴스끼리 공유가 가능하기 때문에 클래스는 한 번만 메모리에 로드하고 각 인스턴스별로 클래스를 가리키는 포인터를 갖게 하는 것(스태틱 영역)이 경제적이다.

업캐스팅

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

Parent instance = new Child();

즉, 서로 다른 클래스의 인스턴스를 동일한 타입에 할당하는 것을 가능하게 해준다. 부모 클래스에 대해 작성된 코드를 전혀 수정하지 않고도 자식 클래스에 적용할 수 있다.

이를 업캐스팅이라고 한다.

동적 바인딩

선언된 변수의 타입이 아니라 메시지를 수신하는 객체의 타입에 따라 실행되는 메서드가 결정된다.

  • 퍼블릭 인터페이스는 같더라도 실제 객체의 구현 내용에 따라 런타임에서 실행되는 내용도 달라진다.

이것은 객체지향 시스템이 메시지를 처리할 적절한 메서드를 컴파일 시점이 아닌 런타임 시점에 결정하기 때문에 가능하다.

이를 동적 바인딩이라고 한다.

동적 메서드 탐색

동적 메서드 탐색은 부모 클래스의 타입에 대해 메시지를 전송하더라도 실행 시에는 실제 클래스를 기반으로 실행될 메서드가 선택되게 해준다.

따라서 코드를 변경하지 않고도 실행되는 메서드를 변경할 수 있다.

동일한 수신자에게 동일한 메시지를 전송하는 "동일한 코드"를 이용해 서로 다른 메서드(로직)를 실행할 수 있는 이유는 업캐스팅동적 메서드 탐색이라는 기반 메커니즘이 존재하기 때문이다.

동적 바인딩 v.s. 정적 바인딩

정적 바인딩 (Static Binding) / 초기 바인딩 (Early Binding) / 컴파일타임 바인딩 (Compile-time Binding)

  • 함수를 호출하는 전통적인 언어들은 호출될 함수를 컴파일타임에 결정한다.

  • 코드를 작성할 시점에 호출될 코드가 결정된다.

동적 바인딩 (Dynamic Binding) / 지연 바인딩 (Late Binding)

  • 객체지향 언어에서는 메시지를 수신했을 때 실행될 메서드가 런타임에 결정된다.

  • 호출된 메서드가 해당 클래스의 상속 계층의 어디에 위치하는지를 알아야 한다.

  • 실행될 메서드를 런타임에 결정한다.

동적 메서드 탐색

  • 메시지를 수신한 객체는 자신을 생성한 클래스에(스태틱 영역) 적합한 메서드가 존재하는지 검사한다.

    • 존재하면 메서드를 실행하고 탐색을 종료한다.

  • 메서드를 찾지 못했다면 부모 클래스에서 메서드 탐색을 계속한다.

    • 적합한 메서드를 찾을 때까지 상속 계층을 따라 올라가며 계속한다.

  • 상속 계층의 최상위 클래스에 이르렀지만 메서드를 발견하지 못했다면 예외를 발생시키며 탐색을 중단한다.

self 참조(self reference)라는 메시지 탐색과 관련된 중요한 변수가 있다.

  • 객체가 메시지를 수신하면 컴파일러는 self 참조라는 임시 변수를 자동으로 생성해 메시지를 수신한 객체를 가리키도록 설정한다. self가 가리키는 객체의 클래스로부터 시작해 상속 계층의 역방향으로 이동해 메서드 탐색이 종료되면 자동으로 소멸된다.

  • self는 항상 메시지를 수신한 객체를 가리킨다.

정적 타입 언어에 속하는 C++, Java, C# 등에서는 self 참조를 this라고 부르고, 동적 타입 언어에 속하는 스몰토크, 루비에서는 self 참조를 위한 키워드로 self를 사용한다. 파이썬에서는 자유지만 대부분 self를 사용한다.

이해할 수 없는 메시지를 만나면?

  • 정적 타입 언어 - 상속 계층을 탐색한 후 처리할 수 없다면 컴파일 에러가 발생한다.

  • 동적 타입 언어 - 실제로 코드를 실행해보기 전에는 메시지 처리 가능 여부를 판단할 수 없다.

    • 이해할 수 있든 없든 객체가 메시지를 처리할 수 있다고 믿고 메시지를 전송한다.

    • 이러한 특징은 객체가 메시지를 처리할 수 있는 능력을 가짐으로써 메시지가 선언된 인터페이스와 정의된 구현을 분리할 수 있다.

    • 이런 특징은 메타 프로그래밍에서 진가를 발휘한다.

    • 특히 동적 타입 언어로 인해 정적 타입 언어보다 더 쉽고 강력한 도메인-특화 언어(Domain-Specific Language, DSL)을 개발할 수 있는 것으로 간주된다. 이러한 개발 방식을 동적 리셉션(Dynamic Reception)이라고 부른다.

self v.s. super

  • self 참조의 가장 큰 특징은 동적이라는 점이다.

    • 메시지를 수신한 객체의 클래스에 따라 메서드 탐색을 위한 문맥을 실행 시점에 결정한다.

  • super 참조 (super reference) - "지금 이 클래스의 부모 클래스에서부터 메서드 탐색을 시작하세요."

    • super 전송에서는 컴파일 시점에 메서드 탐색을 시작할 클래스를 결정한다.

    • 하지만 super를 런타임에 결정하는 경우도 있는데, 예를 들어 스칼라의 트레이트는 super의 대상을 믹스인되는 순서에 따라 동적으로 결정한다.

객체지향 언어에서 클래스는 필수 요소가 아니다

자바스크립트에는 클래스가 존재하지 않기 때문에(ES6 전까지), 오직 객체들 사이의 메시지 위임만을 이용해 다형성을 구현한다. 이것은 객체지향 패러다임에서 클래스가 필수 요소가 아니라는 점을 잘 보여준다.

Last updated