Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[준석] 12장 정리 #41

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
279 changes: 279 additions & 0 deletions 12장 - 다형성/junseok.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
# 12장 다형성

## 이번 장의 목표

포함 다형성의 관점에서 런타임에 상속 계층 안에서 적절한 메서드를 선택하는 방법을 이해하는 것이다.

## 다형성

하나의 추상 인터페이스에 대해 코드를 작성하고 이 추상 인터페이스에 대해 서로 다른 구현을 연결할 수 있는 능력이다[Czarnecki00].

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

객체지향 프로그래밍에서 사용하는 다형성은 다음으로 분류된다.

- 유니버셜 다형성
- 매개변수 다형성
- 포함 다형성(a.k.a. 서브타입 다형성)
- 임시 다형성
- 오버로딩 다형성
- 강제 다형성

### 매개변수 다형성

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

(**제네릭 프로그래밍**과 관련이 깊다)

EX) List 인터페이스

컬렉션에 보관할 요소의 타입을 임의의 타입 T로 지정하고 실제 인스턴스를 생성하는 시점에 T를 구체적인 타입으로 지정한다.

### **포함 다형성(a.k.a. 서브타입 다형성)**

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

가장 널리 알려진 형태의 다형성이기 때문에 특별한 언급 없이 다형성이라고 할 때는 포함 다형성을 의미한다.

```java
public class Movie {
private DiscountPolicy discountPolicy;
public Money calculateMovieFee(Screening screening) {
// discountPolicy 객체 타입에 따라 실행되는 메서드가 달라진다.
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
}
```

포함 다형성을 구현하는 가장 일반적인 방법은 상속을 사용하는 것이다.

상속을 사용하는 가장 큰 이유는 **상속이 클래스들을 계층으로 쌓아 올린 후 상황에 따라 적절한 메서드를 선택할 수 있는 메커니즘을 제공하기 때문이다.**

객체가 메시지를 수신하면 객체지향 시스템은 메시지를 처리할 적절한 메서드를 상속 계층 안에서 탐색한다.

실행한 메서드를 선택하는 기준은 어떤 메시지를 수신했는지, 어떤 클래스의 인스턴스인지, 상속 계층이 어떻게 구성돼 있는지에 따라 다르다.

포함 다형성(서브타입 다형성)의 전제 조건은 자식 클래스가 부모 클래스의 서브타입이어야 한다는 것이다. 그리고 **상속의 진정한 목적은 코드 재사용이 아니라 다형성을 위한 서브타입 계층을 구축하는 것이다.**

### 오버로딩 다형성

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

### 강제 다형성

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

EX) ‘+’연산자

피연산자가 모두 정수일 때는 덧셈 연산자로 동작하고,

피연산자 중 하나가 문자열일 때는 연결 연산자로 동작한다.

## 상속의 양면성

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

타입 계층에 대한 고민 없이 코드를 재사용하기 위해 상속을 사용하면 이해하기 어렵고 유지보수하기 버거운 코드가 만들어질 확률이 높다. 문제를 피할 수 있는 유일한 방법은 상속이 무엇이고 언제 사용해야 하는지를 이해하는 것이다.

### **메서드 오버라이딩**

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

### **메서드 오버로딩**

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

### 데이터 관점의 상속

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

### 행동 관점의 상속

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

객체의 경우에는 각 인스턴스별로 독립적인 메모리를 할당받아야 한다.

하지만 메서드의 경우엔 한 번만 메모리에 로드하고 **각 인스턴스별로 클래스를 가리키는 포인터를 갖게 하는 것**이 경제적이다.

런타임 시스템이 자식 클래스에 정의되지 않은 메서드가 있을 때 이 메서드를 부모 클래스 안에서 탐색하기 때문에 부모 클래스에서 구현한 메서드를 자식 클래스의 인스턴스에서 수행할 수 있다.

메시지를 수신한 객체는 class포인터로 연결된 자신의 클래스에서 적절한 메서드가 존재하는지 찾는다. 만약 메서드가 없으면 클래스의 parent 포인터를 따라 부모 클래스를 차례대로 훑어가면서 적절한 메서드를 탐색한다.

class포인터와 parent포인터를 조합해 현재 인스턴스 클래스부터 최상위 클래스까지 접근 가능하다.

## 업캐스팅과 동적 바인딩

### 같은 메시지, 다른 메서드

선언된 참조 타입과 무관하게 실제로 메시지를 수신하는 객체의 타입에 따라 실행되는 메서드가 달라질 수 있는 것은 **업캐스팅**과 **동적 바인딩**이라는 메커니즘이 작용하기 때문이다.

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

업캐스팅과 동적 메서드 탐색은 개방-폐쇄 원칙을 따르게 해주는 방법으로, 코드를 변경하지 않고도 기능을 추가할 수 있게 해준다.

다운캐스팅

반대로 부모 클래스의 인스턴스를 자식 클래스 타입으로 변환하기 위해서는 명시적인 타입 캐스팅이 필요하다.

EX) Lecture lecture = new GradeLecture(…);

GradeLecture gradeLecture = (GradeLecture)lecture;

### 정적 바인딩과 동적 바인딩

**정적 바인딩, 초기 바인딩, 컴파일타임 바인딩**

컴파일타임에 호출할 함수를 결정하는 방식이다.

**동적 바인딩, 지연 바인딩**

런타임에 실행될 메서드를 결정하는 방식이다.

## 동적 메서드 탐색과 다형성

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

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

동적 메서드는 **self**가 가리키는 객체의 클래스에서 시작해 메서드 탐색이 종료되면 **self 참조**는 자동으로 소멸된다.

동적 메서드 탐색은 두 가지 원리로 구성된다.

1. 자동적인 메시지 위임
2. 동적인 문맥 사용

## 자동적인 메시지 위임

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

C++에서는 상속 계층 내 동일한 이름의 메서드가 공존해 발생하는 혼란을 방지하기 위해 부모 클래스에 선언된 이름이 동일한 메서드 전체를 숨겨 클라이언트가 호출하지 못하도록 한다.

이를 이름 숨기기라고 부른다[Eckel03].

이처럼 사용하는 언어마다 동적 메서드 탐색과 관련한 규칙은 다를 수 있다.

## 동적인 문맥 사용

**메서드를 탐색하는 경로는 self 참조를 시작으로 런타임에 어떤 메서드를 실행할지 결정한다.**

동적인 문맥을 결정하는 것은 메시지를 수신한 객체를 가리키는 self 참조이다.

self 전송의 특성을 간단한 예제로 살펴보자.

```java
public class User {
public void introduce() {
/* getName()은 현재 클래스의 메서드를 호출하는 것이 아니라
**현재 객체(self참조가 가리키는 객체)에게 getJob 메시지를 전송하는 것이다.**
*/
print("My name is" + getJob());
}
public String getJob() {
return "user";
}
}
```

inroduce 메서드 탐색 과정을 따라가보자.

1. User 인스턴스가 inroduce 메시지를 수신하면 self 참조는 메시지를 수신한 User 인스턴스를 가리키도록 자동으로 할당된다.
2. 시스템은 User객체에서 introduce 메서드를 발견하고 실행시킨다.
3. inroduce 메서드를 실행하면서 getJob 메서드 호출 구문을 발견하면 시스템은 self 참조가 가리키는 현재 객체에게 메시지를 전송해야 한다고 판단한다.
4. introduce를 수신한 동일한 객체에게 getJob 메시지를 전송한다.
5. self 참조로부터 다시 메서드 탐색을 하고 getJob 메서드를 실행 후 메서드 탐색을 종료한다.

여기에 상속이 추가되면 복잡해진다.

```java
public class Student extends User{
@Override
public String getJob() {
return "student";
}
}
```

Student 인스턴스가 inroduce 메시지를 수신했다 가정하면

1. Student에서 introduce 못찾아 계층 구조를 따라 올라가 User에서 itroduce를 찾는다.
2. getJob 메서드 발견 후 introduce 수신한 Student인스턴스에 메시지 전송한다.
3. Student인스턴스의 getJob 메서드 실행 후 메서드 탐색 종료한다.

이 결과 이해하기 어려운 코드가 만들어진다.

## 이해할 수 없는 메시지

프로그래밍 언어가 정적 타입 언어에 속하는지, 동적 타입 언어에 속하는지에 따라 달라진다.

### 정적 타입 언어와 이해할 수 없는 메시지

자바와 같은 정적 타입 언어는 컴파일 시에 상속 계층 전체를 탐색하고, 메시지를 처리할 수 있는 메서드를 발견하지 못했다면 컴파일 에러를 발생시킨다.

안정적이지만 유연하지 않다.

### 동적 타입 언어와 이해할 수 없는 메시지

정적 타입 언어와의 차이점은 컴파일 단계가 없어 실제로 코드를 실행하기 전에는 메시지 처리 가능 여부를 판단할 수 없다는 점이 있다.

또한 이해할 수 없는 메시지에 대해 예외를 던지거나 메시지를 이해할 수 없다는 메시지에 대해 응답할 수 있는 메서드를 구현할 수 있다.

동적 타입 언어는 이해할 수 없는 메시지를 처리할 수 있는 능력을 가짐으로써 메시지가 선언된 인터페이스와 메서드가 정의된 구현을 분리할 수 있다.

유연성이 있지만 안정적이지 않다.

## Self vs Super

self 전송의 경우 메서드 탐색을 시작할 클래스를 반드시 실행 시점에 동적으로 결정해야 하지만

super 전송의 경우 컴파일 시점에 미리 결정해 놓을 수 있다.

**super 참조**는 지금 이 클래스의 부모 클래스에서부터 메서드 탐색을 시작하라는 의도이다.

super 참조를 이용해 부모 클래스에게 **메시지를 전송**한다.

**왜 메시지를 호출이 아니라 메시지를 전송한다라고 표현?**

호출되는 메서드는 바로 한단계 위의 부모 클래스가 아닌 더 상위의 클래스일 수 있기 때문이다.

이를 **super 전송**이라고 부른다.

## 상속 대 위임

### 위임과 self 참조

자신이 수신한 메시지를 다른 객체에게 동일하게 전달해서 처리를 요청하는 것을 **위임**이라고 한다.

위임은

- 본질적으로 자신이 정의하지 않거나 처리할 수 없는 속성 또는 메서드의 탐색 과정을 다른 객체로 이동시키기 위해 사용한다.
- 클래스를 이용한 상속 관계를 객체 사이의 합성 관계로 대체해서 다형성을 구현하기 위해 사용한다.
- 이를 위해 위임은 항상 현재의 실행 문맥을 가리키는 **self 참조를 인자로 전달한다.**

**포워딩과 위임 차이**

포워딩

처리를 요청할 때 self참조를 전달하지 않는 경우이다.

위임

처리를 요청할 때 self참조를 전달하는 경우이다.

### 프로토타입 기반의 객체지향 언어

클래스가 존재하지 않고 오직 객체만 존재하는 프로토타입 기반의 객체지향 언어에서 상속을 구현하는 유일한 방법은 객체 사이의 위임을 이용하는 것이다.

자바스크립트에서는 prototype 체인으로 연결된 객체 사이에 메시지를 위임함으로써 상속을 구현할 수 있다.

# 읽고 난 후

이번 장을 읽고 클래스 없이도 객체 사이의 협력 관계를 구축할 수 있고, 상속 없이 다형성을 구현할 수 있다는 점을 알았다. 이처럼 클래스는 객체를 편리하게 정의하고 생성하기 위해 제공되는 프로그래밍 구성 요소일 뿐이지, 너무 클래스에 목 메이지 말아야 겠다고 다시 한 번 생각하게 되었다.

중요한 것은 클래스가 아닌 메시지와 협력이다!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 이건 대체


우리 모두 메시지와 협력에 집중하여 객체를 지향하자!!