BottleH Blog

Object - 12장 다형성

    Tags

  • java
Object - 12장 다형성 thumbnail

상속의 목적이 코드 재사용이라면 사용하지 말아야 한다. 다형성이 런타임에 메시지를 처리하기에 적합한 메서드를 동적으로 탐색하는 과정을 통해 구현되며, 상속이 이런 메서드를 찾기 위한 일종의 탐색 경로를 클래스 계층의 형태로 구현된다.

📖 12.1 다형성

  • 많은 형태를 가질 수 있는 능력
  • Universal 다형성
    • Parametric 다형성
      • 클래스의 인스턴스 변수나 메서드의 매개변수 타입을 임의의 타입으로 선언한 후 사용하는 시점에 구체적인 타입으로 지정하는 방식
      • 제네릭 프로그래밍
    • Inclustion 다형성
      • 메시지가 동일하더라도 수신한 객체의 타입에 따라 실제로 수행되는 행동이 달라지는 능력
      • Subtype 다형성
      • 가장 널리 알려진 형태
  • Ad Hoc 다형성
    • Overloading 다형성
      • 하나의 클래스 안에 동일한 이름의 메서드가 존재하는 경우
    • Coercion 다형성
      • 언어가 지원하는 자동적인 타입 변환이나 사용자가 직접 구현한 타입 변환을 이용해 동일한 연산자를 다양한 타입에 사용할 수 있는 방식
      • java의 + 연산자
  • 상속의 일차적인 목적은 서브타입의 구현이다.

📖 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;
  • 다운캐스팅
    • 부모 클래스의 인스턴스를 자식 클래스 타입으로 변환하기 위해서는 명시적인 타입 캐스팅이 필요
  • ProfessorLecture를 상속받는 어떤 자식 클래스와도 협력할 수 있는 무한한 확장 가능성을 지닌다.
    • 설계가 유연하고 확장이 용이

🔖 12.3.3 동적 바인딩

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

  • 정적 바인딩(static binding)
  • 초기 바인딩(early binding)
  • 컴파일타임 바인딩(compile-time binding)

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

  • 동적 바인딩(dynamic binding)
  • 지연 바인딩(late binding)

📖 12.4 동적 메서드 탐색과 다형성

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

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

객체가 메시지를 수신하면 컴파일러는 **self 참조(self reference)**라는 임시 변수를 자동으로 생성한 후 메시지를 수신한 객체를 가리키도록 설정

  • java의 this

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

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

🔖 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"; } }
  • GradeLecturestats 메시지를 전송
    • 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의 부모 클래스인 Lectureaverage 메서드이다.

super 참조의 정확한 의도는 '지금 이 클래스의 부모 클래스에서부터 메서드 탐색을 시작하세요'다.

  • 실행하고자 하는 메서드가 반드시 부모 클래스에 위치하지 않아도 되는 유연성을 제공
  • super 전송(super send)

self 전송이 메시지를 수신하는 객체의 클래스에 따라 메서드를 탐색할 시작 위치를 동적으로 결정하는 데 비해 super 전송은 항상 메시지를 전송하는 클래스의 부모 클래스에서부터 시작된다.

  • super 전송의 경우에는 컴파일 시점에 미리 결정해 놓을 수 있다.
  • super 참조는 부모 클래스의 코드에 접근할 수 있게 함으로써 중복 코드를 제거할 수 있게 한다.

📖 12.5 상속 대 위임

🔖 12.5.1 위임과 self 참조

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

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

위임(delegation)

  • 자신이 수신한 메시지를 다른 객체에게 동일하게 전달해서 처리를 요청하는 것
  • 자신이 정의하지 않거나 처리할 수 없는 속성 또는 메서드의 탐색 과정을 다른 객체로 이동시키기 위해 사용
  • 항상 현재의 실행 문맥을 가리키는 self 참조를 인자로 전달
    • 포워딩은 self 참조를 전달하지 않음
  • 위임의 정확한 용도는 클래스를 이용한 상속 관계를 객체 사이의 합성 관계로 대체해서 다형성을 구현하는 것

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

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

  • 메서드를 탐색하는 과정은 클래스 기반 언어의 상속과 거의 동일
  • 객체지향 패러다임에서 클래스가 필수 요소가 아님.
    • javascript는 오직 객체들 사이의 메시지 위임만을 이용해 다형성을 구현

중요한 것은 클래스 기반의 상속과 객체 기반의 위임 사이에 기본 개념과 메커니즘을 공유한다는 점

Written by@BottleH
Back-End Developer

GitHub