상속의 목적이 코드 재사용이라면 사용하지 말아야 한다. 다형성이 런타임에 메시지를 처리하기에 적합한 메서드를 동적으로 탐색하는 과정을 통해 구현되며, 상속이 이런 메서드를 찾기 위한 일종의 탐색 경로를 클래스 계층의 형태로 구현된다.
📖 12.1 다형성
- 많은 형태를 가질 수 있는 능력
- Universal 다형성
- Parametric 다형성
- 클래스의 인스턴스 변수나 메서드의 매개변수 타입을 임의의 타입으로 선언한 후 사용하는 시점에 구체적인 타입으로 지정하는 방식
- 제네릭 프로그래밍
- Inclustion 다형성
- 메시지가 동일하더라도 수신한 객체의 타입에 따라 실제로 수행되는 행동이 달라지는 능력
- Subtype 다형성
- 가장 널리 알려진 형태
- Parametric 다형성
- Ad Hoc 다형성
- Overloading 다형성
- 하나의 클래스 안에 동일한 이름의 메서드가 존재하는 경우
- Coercion 다형성
- 언어가 지원하는 자동적인 타입 변환이나 사용자가 직접 구현한 타입 변환을 이용해 동일한 연산자를 다양한 타입에 사용할 수 있는 방식
- java의
+연산자
- Overloading 다형성
- 상속의 일차적인 목적은 서브타입의 구현이다.
📖 12.2 상속의 양면성
객체지향 프로그램을 작성하기 위해서는 항상 데이터와 행동이라는 두 가지 관점을 함께 고려해야 한다.
- 데이터 관점의 상속
- 부모 클래스에서 정의한 모든 데이터를 자식 클래스의 인스턴스에 자동으로 포함
- 행동 관점의 상속
- 부모 클래스에서 정의한 일부 메서드 역시 자동으로 자식 클래스에 포함
🔖 12.2.1 상속을 사용한 강의 평가
🎈 Lecture 클래스 살펴보기
@RequiredArgsConstructor
public class Lecture {
/**
* 이수 여부를 판단할 기준 점수
*/
private final int pass;
/**
* 과목명
*/
private final String title;
/**
* 학생들의 성적
*/
private final List<Integer> scores;
public double average() {
return scores.stream()
.mapToInt(Integer::intValue)
.average().orElse(0);
}
public List<Integer> getScores() {
return Collections.unmodifiableList(scores);
}
public String evaluate() {
return String.format("Pass:%d Fail:%d", passCount(), failCount());
}
private long passCount() {
return scores.stream().filter(score -> score >= pass).count();
}
private long failCount() {
return scores.size() - passCount();
}
}
🎈 상속을 이용해 Lecture 클래스 재사용하기
@RequiredArgsConstructor
public class Grade {
/**
* 등급 이름
*/
@Getter
private final String name;
/**
* 최대 성적, 최소 성적
*/
private final int upper, lower;
public boolean isName(String name) {
return this.name.equals(name);
}
public boolean include(int score) {
return score >= lower && score <= upper;
}
}
public class GradeLecture extends Lecture {
private final List<Grade> grades;
public GradeLecture(int pass, String title, List<Integer> scores, List<Grade> grades) {
super(pass, title, scores);
this.grades = grades;
}
@Override
public String evaluate() {
return super.evaluate() + ", " + gradesStatistics();
}
private String gradesStatistics() {
return grades.stream()
.map(this::format)
.collect(joining(" "));
}
private String format(Grade grade) {
return String.format("%s:%d", grade.getName(), gradeCount(grade));
}
private long gradeCount(Grade grade) {
return getScores().stream()
.filter(grade::include)
.count();
}
public double average(String gradeName) {
return grades.stream()
.filter(each -> each.isName(gradeName))
.findFirst()
.map(this::gradeAverage)
.orElse(0d);
}
private double gradeAverage(Grade grade) {
return getScores().stream()
.filter(grade::include)
.mapToInt(Integer::intValue)
.average()
.orElse(0);
}
}
- 부모 클래스와 자식 클래스에 동일한 시그니처를 가진 메서드가 존재할 경우 자식 클래스의 메서드 우선순위가 더 높다.
- 메서드 오버라이딩
- 자식 클래스에 부모 클래스에는 없던 새로운 메서드를 추가하는 것도 가능
- 부모 클래스에서 정의한 메서드와 이름은 동일하지만 시그니처는 다른 메서드를 자식 클래스에 추가
- 메서드 오버로딩
🔖 12.2.2 데이터 관점의 상속
데이터 관점에서 상속은 자식 클래스의 인스턴스 안에 부모 클래스의 인스턴스를 포함하는 것으로 볼 수 있다.
- 자식 클래스의 인스턴스는 자동으로 부모 클래스에서 정의한 모든 인스턴스 변수를 내부에 포함하게 되는 것이다.
🔖 12.2.3 행동 관점의 상속
부모 클래스의 모든 퍼블릭 메서드는 자식 클래스의 퍼블릭 인터페이스에 포함된다.
- 외부의 객체가 부모 클래스의 인스턴스에게 전송할 수 있는 모든 메시지는 자식 클래스의 인스턴스에게도 전송할 수 있다.
- 런타임에 시스템이 자식 클래스에 정의되지 않은 메서드가 있을 경우 이 메서드를 부모 클래스 안에서 탐색하기 때문❗️
📖 12.3 업캐스팅과 동적 바인딩
🔖 12.3.1 같은 메시지, 다른 메서드
@RequiredArgsConstructor
public class Professor {
private final String name;
private final Lecture lecture;
public String compileStatistics() {
return String.format("[%s] %s - Avg: %.1f", name, lecture.evaluate(), lecture.average());
}
}
- 업캐스팅
- 부모 클래스 타입으로 선언된 변수에 자식 클래스의 인스턴스를 할당하는 것이 가능
- 서로 다른 클래스의 인스턴스를 동일한 타입에 할당하는 것을 가능하게 해준다.
- 동적 바인딩
- 선언된 변수의 타입이 아니라 메시지를 수신하는 객체의 타입에 따라 실행되는 메서드가 결정
- 코드를 변경하지 않고도 실행되는 메서드를 변경할 수 있다.
🔖 12.3.2 업캐스팅
Lecture lecture = new GradeLecture(...);
- 명시적으로 타입을 변환하지 않고도 부모 클래스 타입의 참조 변수에 자식 클래스의 인스턴스를 대입할 수 있게 허용
public class Professor {
public Professor(String name, Lecture lecture) {...}
}
Professor professor = new Professor("다익스트라", new GradeLecture(...));
- 부모 클래스 타입으로 선언된 파라미터에 자식 클래스의 인스턴스를 전달하는 것도 가능
Lecture lecture = new GradeLecture(...);
GradeLecture gradeLecture = (GradeLecture)lecture;
- 다운캐스팅
- 부모 클래스의 인스턴스를 자식 클래스 타입으로 변환하기 위해서는 명시적인 타입 캐스팅이 필요
Professor는Lecture를 상속받는 어떤 자식 클래스와도 협력할 수 있는 무한한 확장 가능성을 지닌다.- 설계가 유연하고 확장이 용이
🔖 12.3.3 동적 바인딩
컴파일타임에 호출할 함수를 결정하는 방식
- 정적 바인딩(static binding)
- 초기 바인딩(early binding)
- 컴파일타임 바인딩(compile-time binding)
런타임에 실행될 메서드를 결정하는 방식
- 동적 바인딩(dynamic binding)
- 지연 바인딩(late binding)
📖 12.4 동적 메서드 탐색과 다형성
객체지향 시스템은 다음 규칙에 따라 실행할 메서드를 선택
- 메시지를 수신한 객체는 먼저 자신을 생성한 클래스에 적합한 메서드가 존재하는지 검사한다.
- 존재하면 메서드를 실행하고 탐색 종료
- 메서드를 찾지 못했다면 부모 클래스에서 메서드 탐색을 계속한다.
- 이 과정은 적합한 메서드를 찾을 때까지 상속 계층을 따라 올라가며 계속된다.
- 상속 계층의 가장 최상위 클래스에 이르렀지만 메서드를 발견하지 못한 경우 예외를 발생시키며 탐색을 중단
객체가 메시지를 수신하면 컴파일러는 **self 참조(self reference)**라는 임시 변수를 자동으로 생성한 후 메시지를 수신한 객체를 가리키도록 설정
- java의
this
동적 메서드 탐색은 두 가지 원리로 구성
- 자동적인 메시지 위임
- 동적인 문맥 사용
🔖 12.4.1 자동적인 메시지 위임
- 상속 계층을 정의하는 것은 메서드 탐색 경로를 정의하는 것과 동일
- 일부 언어들은 상속이 아닌 다른 방법을 이용해 메시지를 자동으로 위임할 수 있는 메커니즘을 제공
- 루비의 모듈(module)
- 스몰토크와 스칼라의 트레이트(trait)
- 스위프트의 프로토콜(protocol)과 확장(extension) 메커니즘
- 자식 클래스에서 어떤 메서드를 구현하고 있느냐에 따라 부모 클래스에 구현된 메서드의 운명이 결정되기도 함.
- 자식 클래스에서 부모 클래스의 방향으로 자동으로 메시지 처리가 위임되기 때문
- 동일한 시그니처를 가지는 자식 클래스의 메서드는 부모 클래스의 메서드를 감추지만 이름만 같고 시그니처가 완전히 동일하지 않은 메서드들은 상속 계층에 걸쳐 사이좋게 공존할 수도 있다.
- 메서드 오버로딩
🎈 메서드 오버라이딩
Lecture lecture = new Lecture(...);
lecture.evaluate();
Lecture클래스 안에evaluate메서드가 존재하기 때문에 시스템은 메서드를 실행한 후 메서드 탐색을 종료
Lecture lecture = new GradeLecture(...);
lecture.evaluate();
GradeLecture클래스 안에evaluate메서드가 존재하기 때문에 먼저 발견된 메서드가 실행- 자식 클래스의 메서드가 부모 클래스의 메서드를 감추는 것처럼 보이게 된다.
🎈 메서드 오버로딩
GradeLecture lecture = new GradeLecture(...);
lecture.average("A");
GradeLecture에서 종료
Lecture lecture = new GradeLecture(...);
lecture.average();
Lecture에서 종료- 클라이언트의 관점에서 오버로딩된 모든 메서드를 호출할 수 있다.
대부분의 사람들은 하나의 클래스 안에서 같은 이름을 가진 메서드들을 정의하는 것은 메서드 오버로딩으로 생각하고 상속 계층 사이에서 같은 이름을 가진 메서드를 정의하는 것은 메서드 오버로딩으로 생각하지 않는 경향이 있다.
- 일부 언어에서 상속 계층 사이의 메서드 오버로딩을 지원하지 않기 때문
- C++의 이름 숨기기(name hiding)
🔖 12.4.2 동적인 문맥
동적인 문맥을 결정하는 것은 바로 메시지를 수신한 객체를 가리키는 self 참조
- self 참조가 동적 문맥을 결정한다는 사실은 종종 어떤 메서드가 실행될지를 예상하기 어렵게 만든다.
- self 전송(self send)
public class Lecture {
public String stats() {
return String.format("Title: %s, Evaluation Method: %s", title, getEvaluationMethod());
}
public String getEvaluationMethod() {
return "Pass or Fail";
}
}
getEvaluationMethod()라는 구문은 현재 클래스의 메서드를 호출하는 것이 아니라 현재 객체에게getEvaluationMethod메시지를 전송하는 것이다.- 현재 객체
- self 참조가 가리키는 객체
- stats 메시지를 수신했던 객체
- self 전송
- self 참조가 가리키는 바로 그 객체에서부터 메시지 탐색을 다시 시작
- 현재 객체
public class GradeLecture extends Lecture {
@Override
public String getEvaluationMethod() {
return "Grade";
}
}
GradeLecture에stats메시지를 전송Lecture클래스의stats메서드와GradeLecture클래스의getEvaluationMethod메서드의 실행 결과를 조합한 문자열이 반환
- self 전송은 자식 클래스에서 부모 클래스 방향으로 진행되는 동적 메서드 탐색 경로를 다시 self 참조가 가리키는 원래의 자식 클래스로 이동
- 최악의 경우에는 실제로 실행될 메서드를 이해하기 위해 상속 계층 전체를 훑어가며 코드를 이해해야 하는 상황이 발생
🔖 12.4.3 이해할 수 없는 메시지
객체가 메시지를 이해할 수 없다면 어떻게 할까❓
- 정적 타입 언어와 동적 타입 언어에 따라 달라진다.
🎈 정적 타입 언어와 이해할 수 없는 메시지
정적 타입 언어에서는 코드를 컴파일할 때 상속 계층 안의 클래스들이 메시지를 이해할 수 있는지 여부를 판단
- 상속 계층 전체를 탐색한 후에도 메시지를 처리할 수 있는 메서드를 발견하지 못했다면 컴파일 에러 발생
Lecture lecture = new GradeLecture(...);
lecture.unknownMessage(); // 컴파일 에러!
🎈 동적 타입 언어와 이해할 수 없는 메시지
동적 타입 언어에는 컴파일 단계가 존재하지 않기 때문에 실제로 코드를 실행해보기 전에는 메시지 처리 가능 여부를 판단할 수 없다.
- 상속 계층을 거슬러 올라가며 메서드를 탐색 후, 예외를 던진다.
- 예외를 던지지 않고 응답할 수 있는 메서드를 구현할 수도 있다.
좀 더 순수한 관점에서 객체지향 패러다임을 구현한다.
- 메시지가 선언된 인터페이스와 메서드가 정의된 구현을 분리할 수 있다.
- 코드를 이해하고 수정하기 어렵게 만들고, 디버깅 과정을 복잡화게 만든다.
🔖 12.4.4 self 대 super
public class GradeLecture extends Lecture {
@Override
public String evaluate() {
return super.evaluate() + ", " + gradesStatistics();
}
}
- super 참조를 이용해 '메시지를 전송'한다.
- 부모 클래스의 메서드가 아니라 더 상위에 위치한 조상 클래스의 메서드일 수도 있다.
public class FormattedGradeLecture extends GradeLecture {
public FormattedGradeLecture(int pass, String title, List<Integer> scores, List<Grade> grades) {
super(pass, title, scores, grades);
}
public String formatAverage() {
return String.format("Avg: %1.1f", super.average());
}
}
super.average()에 의해 실행되는 메서드는GradeLecture의 부모 클래스인Lecture의average메서드이다.
super 참조의 정확한 의도는 '지금 이 클래스의 부모 클래스에서부터 메서드 탐색을 시작하세요'다.
- 실행하고자 하는 메서드가 반드시 부모 클래스에 위치하지 않아도 되는 유연성을 제공
- super 전송(super send)
self 전송이 메시지를 수신하는 객체의 클래스에 따라 메서드를 탐색할 시작 위치를 동적으로 결정하는 데 비해 super 전송은 항상 메시지를 전송하는 클래스의 부모 클래스에서부터 시작된다.
- super 전송의 경우에는 컴파일 시점에 미리 결정해 놓을 수 있다.
- super 참조는 부모 클래스의 코드에 접근할 수 있게 함으로써 중복 코드를 제거할 수 있게 한다.
📖 12.5 상속 대 위임
🔖 12.5.1 위임과 self 참조
self 참조는 항상 메시지를 수신한 객체를 가리킨다.
- 메서드 탐색 중에는 자식 클래스의 인스턴스와 부모 클래스의 인스턴스가 동일한 self 참조를 공유하는 것으로 봐도 무방하다.
위임(delegation)
- 자신이 수신한 메시지를 다른 객체에게 동일하게 전달해서 처리를 요청하는 것
- 자신이 정의하지 않거나 처리할 수 없는 속성 또는 메서드의 탐색 과정을 다른 객체로 이동시키기 위해 사용
- 항상 현재의 실행 문맥을 가리키는 self 참조를 인자로 전달
- 포워딩은 self 참조를 전달하지 않음
- 위임의 정확한 용도는 클래스를 이용한 상속 관계를 객체 사이의 합성 관계로 대체해서 다형성을 구현하는 것
🔖 12.5.2 프로토타입 기반의 객체지향 언어
클래스가 존재하지 않고 오직 객체만 존재하는 프로토타입 기반의 객체지향 언어에서 상속을 구현하는 유일한 방법은 객체 사이의 위임을 이용하는 것이다.
- 메서드를 탐색하는 과정은 클래스 기반 언어의 상속과 거의 동일
- 객체지향 패러다임에서 클래스가 필수 요소가 아님.
- javascript는 오직 객체들 사이의 메시지 위임만을 이용해 다형성을 구현
중요한 것은 클래스 기반의 상속과 객체 기반의 위임 사이에 기본 개념과 메커니즘을 공유한다는 점
