Skip to content

Latest commit

 

History

History
 
 

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

README.md

Chapter 12. 다형성

"상속의 목적은 코드 재사용이 아니다. 상속은 타입 계층을 구조화하기 위해 사용해야 한다."

📌 핵심 개념

이 장에서는 다형성의 메커니즘을 다룹니다. 상속이 어떻게 다형성을 가능하게 하는지, 그리고 런타임에 메시지가 어떻게 처리되는지 그 내부 원리를 파헤칩니다.

🎯 학습 목표

  • 다형성의 다양한 형태 이해하기
  • 업캐스팅과 동적 바인딩의 동작 원리 파악하기
  • 동적 메서드 탐색 과정 완벽히 이해하기
  • self 참조와 super 참조의 차이점 이해하기
  • 상속을 메시지 위임 관점에서 이해하기
  • 프로토타입 기반 객체지향과의 연결고리 파악하기

📖 목차

  1. 다형성
  2. 상속의 양면성
  3. 업캐스팅과 동적 바인딩
  4. 동적 메서드 탐색과 다형성
  5. 상속 대 위임
  6. 핵심 정리

1. 다형성

📂 코드: Lecture.java (step01-02)

1.1 다형성이란?

🎯 정의

다형성 (Polymorphism):

poly (많은) + morph (형태) = 많은 형태를 가질 수 있는 능력

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

핵심:
여러 타입을 대상으로 동작할 수 있는 코드를 작성하는 방법

1.2 다형성의 분류

📊 다형성의 4가지 형태

┌──────────────────────────────────────────┐
│              다형성 (Polymorphism)         │
├──────────────┬───────────────────────────┤
│              │                           │
│  Universal   │        Ad-hoc             │
│  (보편적)      │        (임시적)            │
│              │                           │
├──────┬───────┼──────────┬────────────────┤
│      │       │          │                │
│매개변수│ 포함   │ 오버로딩    │ 강제            │
│다형성  │다형성  │ 다형성     │ 다형성           │
└──────┴───────┴──────────┴────────────────┘

1️⃣ 매개변수 다형성 (Parametric Polymorphism)

정의:

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

= 제네릭 프로그래밍

예시:

// 제네릭 타입 T
public interface List<T> {
    void add(T element);
    T get(int index);
}

// 사용 시점에 구체적인 타입 지정
List<String> names = new ArrayList<>();
names.add("Alice");

List<Integer> numbers = new ArrayList<>();
numbers.add(42);

장점:

✅ 타입 안전성 보장
   - 컴파일 타임에 타입 체크
   
✅ 코드 재사용
   - 동일한 로직을 다양한 타입에 적용
   
✅ 형변환 불필요
   - 자동으로 적절한 타입 반환

2️⃣ 포함 다형성 (Inclusion Polymorphism)

정의:

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

= 서브타입 다형성 (Subtype Polymorphism)

핵심:

일반적으로 "다형성"이라고 하면
포함 다형성을 의미함!

이 장에서 주로 다루는 내용

예시:

public class Movie {
    private DiscountPolicy discountPolicy;
    
    public Money calculateMovieFee(Screening screening) {
        return fee.minus(discountPolicy.calculateDiscountAmount(screening));
    }
}

실행 시:

// AmountDiscountPolicy 인스턴스
Movie avatar = new Movie(
    new AmountDiscountPolicy(Money.wons(800), ...)
);

// PercentDiscountPolicy 인스턴스  
Movie starWars = new Movie(
    new PercentDiscountPolicy(0.1, ...)
);

// 동일한 calculateDiscountAmount 메시지
// 하지만 실제 실행되는 메서드는 다름!
avatar.calculateMovieFee(screening);    // AmountDiscountPolicy.calculateDiscountAmount
starWars.calculateMovieFee(screening);  // PercentDiscountPolicy.calculateDiscountAmount

특징:

✅ 런타임 다형성
   - 실행 시점에 메서드 결정
   
✅ 상속 또는 인터페이스 구현으로 달성
   - 타입 계층 필요
   
✅ 업캐스팅 + 동적 바인딩
   - 메커니즘의 핵심

3️⃣ 오버로딩 다형성 (Overloading Polymorphism)

정의:

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

= 메서드 오버로딩

예시:

public class Money {
    private BigDecimal amount;
    
    // 동일한 이름 "plus"
    public Money plus(Money that) {
        return new Money(this.amount.add(that.amount));
    }
    
    public Money plus(BigDecimal that) {
        return new Money(this.amount.add(that));
    }
    
    public Money plus(long that) {
        return new Money(this.amount.add(BigDecimal.valueOf(that)));
    }
}

장점:

✅ 기억해야 할 이름 수 감소
   - plusMoney, plusBigDecimal, plusLong → plus
   
✅ 일관된 인터페이스
   - 유사한 작업을 동일한 이름으로

주의사항:

// ❌ 오버로딩 + 강제 다형성 = 혼란
public void print(int value) {
    System.out.println("int: " + value);
}

public void print(long value) {
    System.out.println("long: " + value);
}

print(10);    // int
print(10L);   // long
print('A');   // ??? (char → int 자동 변환)

4️⃣ 강제 다형성 (Coercion Polymorphism)

정의:

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

예시:

// + 연산자의 다형성
1 + 2           // 정수 덧셈 = 3
1.5 + 2.3       // 실수 덧셈 = 3.8
"Hello" + "!"   // 문자열 연결 = "Hello!"
"Count: " + 42  // 문자열 연결 = "Count: 42" (int → String 강제 변환)

// 자동 타입 변환
int i = 10;
long l = i;     // int → long (강제 변환)
double d = i;   // int → double (강제 변환)

문제점:

// 오버로딩과 강제 다형성의 조합
public void process(int value) { ... }
public void process(long value) { ... }

process(10);     // int 버전 호출
process(10L);    // long 버전 호출
process('A');    // ??? char → int? char → long?

// 모호함!

1.3 다형성 비교표

다형성 종류 시점 목적 예시
매개변수 컴파일타임 타입 안전한 재사용 List<T>
포함 런타임 타입별 행동 분화 discountPolicy.calculate()
오버로딩 컴파일타임 편의성 plus(int), plus(long)
강제 컴파일/런타임 타입 호환 "Hello" + 1

1.4 핵심 통찰

┌─────────────────────────────────────────────────────┐
│  이 장의 초점:                                         │
│                                                     │
│  포함 다형성 (Subtype Polymorphism)                    │
│                                                     │
│  = 런타임에 메시지를 처리하기에 적합한                       │
│    메서드를 동적으로 탐색하는 과정                          │
└─────────────────────────────────────────────────────┘

2. 상속의 양면성

📂 코드: Lecture.java | GradeLecture.java

2.1 상속의 두 가지 관점

객체지향 프로그램 = 데이터 + 행동

따라서 상속도 두 가지 관점에서 이해해야 함:
1. 데이터 관점의 상속
2. 행동 관점의 상속

2.2 예제: 강의 평가 시스템

📝 Lecture 클래스 (기본 강의)

public class Lecture {
    private int pass;                    // 이수 기준 점수
    private String title;                // 과목명
    private List<Integer> scores = new ArrayList<>();  // 성적 목록
    
    public Lecture(String title, int pass, List<Integer> scores) {
        this.title = title;
        this.pass = pass;
        this.scores = 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 lecture = new Lecture(
    "객체지향 프로그래밍",
    70,
    Arrays.asList(81, 95, 75, 50, 45)
);

String evaluation = lecture.evaluate();
// 결과: "Pass:3 Fail:2"

📊 Grade 클래스 (등급)

public class Grade {
    private String name;        // 등급명
    private int upper, lower;   // 상한, 하한 점수
    
    private Grade(String name, int upper, int lower) {
        this.name = name;
        this.upper = upper;
        this.lower = lower;
    }
    
    public String getName() {
        return name;
    }
    
    public boolean isName(String name) {
        return this.name.equals(name);
    }
    
    // 점수가 이 등급 범위에 포함되는지 확인
    public boolean include(int score) {
        return score >= lower && score <= upper;
    }
}

📈 GradeLecture 클래스 (등급 평가 강의)

public class GradeLecture extends Lecture {
    private List<Grade> grades;  // 등급 목록
    
    public GradeLecture(String name, int pass, List<Grade> grades, 
                        List<Integer> scores) {
        super(name, pass, scores);
        this.grades = grades;
    }
    
    // 메서드 오버라이딩: 부모 메서드 + 등급 통계
    @Override
    public String evaluate() {
        return super.evaluate() + ", " + gradesStatistics();
    }
    
    // 등급별 통계
    private String gradesStatistics() {
        return grades.stream()
                .map(grade -> format(grade))
                .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);
    }
}

사용:

Lecture lecture = new GradeLecture(
    "객체지향 프로그래밍",
    70,
    Arrays.asList(
        new Grade("A", 100, 95),
        new Grade("B", 94, 80),
        new Grade("C", 79, 70),
        new Grade("D", 69, 50),
        new Grade("F", 49, 0)
    ),
    Arrays.asList(81, 95, 75, 50, 45)
);

String evaluation = lecture.evaluate();
// 결과: "Pass:3 Fail:2, A:1 B:1 C:1 D:1 F:1"

double averageA = lecture.average("A");
// A 등급 평균: 95.0

2.3 데이터 관점의 상속

🎯 개념

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

개념적으로:
자식 클래스의 인스턴스 안에
부모 클래스의 인스턴스가 포함되는 것

📊 메모리 구조

Lecture lecture = new GradeLecture(...);

메모리 상의 구조:

┌─────────────────────────────────────┐
│        lecture (참조 변수)            │
│        타입: Lecture                 │
└───────────────┬─────────────────────┘
                │
                ▼
┌─────────────────────────────────────┐
│     GradeLecture 인스턴스             │
│                                     │
│  ┌───────────────────────────────┐  │
│  │  Lecture 인스턴스 (부모)         │  │
│  │                               │  │
│  │  - pass: 70                   │  │
│  │  - title: "객체지향..."         │  │
│  │  - scores: [81,95,75,50,45]   │  │
│  └───────────────────────────────┘  │
│                                     │
│  - grades: [A,B,C,D,F]              │
│                                     │
└─────────────────────────────────────┘

핵심:

✅ 자식 클래스의 인스턴스는
   자동으로 부모 클래스에서 정의한
   모든 인스턴스 변수를 내부에 포함
   
✅ lecture 참조는 GradeLecture를 가리킴
   → Lecture 인스턴스에 직접 접근 불가
   → GradeLecture를 통해서만 접근

2.4 행동 관점의 상속

🎯 개념

상속을 이용하면
부모 클래스에서 정의한 일부 메서드를
자식 클래스의 인터페이스에 포함시킬 수 있다.

핵심:
부모 클래스의 모든 퍼블릭 메서드는
자식 클래스의 퍼블릭 인터페이스에 포함된다.

💡 중요한 사실

부모 클래스의 퍼블릭 인터페이스가
자식 클래스의 퍼블릭 인터페이스에 합쳐지지만

실제로 코드가 복사되어 합쳐지는 것은 아니다!

런타임에 시스템이
자식 클래스에 정의되지 않은 메서드가 있을 경우
이 메서드를 부모 클래스 안에서 탐색한다.

📊 메서드 공유 메커니즘

메서드는 인스턴스 간 공유 가능:

객체의 경우:
- 서로 다른 상태를 저장
- 각 인스턴스별로 독립적인 메모리 할당

메서드의 경우:
- 동일한 클래스의 인스턴스끼리 공유 가능
- 클래스는 한 번만 메모리에 로드
- 각 인스턴스는 클래스를 가리키는 포인터만 보유

메모리 구조:

┌─────────────┐       ┌─────────────┐
│ lecture1    │       │ lecture2    │
│ (Lecture)   │       │ (Lecture)   │
├─────────────┤       ├─────────────┤
│ pass: 70    │       │ pass: 60    │
│ title: "OOP"│       │ title: "DS" │
│ scores: [...│       │ scores: [...│
│             │       │             │
│ class ──────┼───┐   │ class ──────┼───┐
└─────────────┘   │   └─────────────┘   │
                  ▼                     ▼
              ┌─────────────────────────────┐
              │    Lecture 클래스             │
              ├─────────────────────────────┤
              │ + average()                 │
              │ + evaluate()                │
              │ + getScores()               │
              │ - passCount()               │
              │ - failCount()               │
              │                             │
              │ parent ─────────▶ Object    │
              └─────────────────────────────┘

상속 계층에서:

┌─────────────┐
│ lecture     │
│(GradeLecture)
├─────────────┤
│ pass: 70    │
│ title: "OOP"│
│ scores: [...│
│ grades: [...│
│             │
│ class ──────┼──────▶ ┌──────────────────┐
└─────────────┘        │ GradeLecture     │
                       ├──────────────────┤
                       │ + evaluate()     │
                       │ + average(String)│
                       │ - format()       │
                       │ ...              │
                       │ parent ──────────┼──▶ ┌─────────────┐
                       └──────────────────┘    │ Lecture     │
                                               ├─────────────┤
                                               │ + average() │
                                               │ + evaluate()│
                                               │ ...         │
                                               │ parent ─────┼──▶ Object
                                               └─────────────┘

핵심:

✅ class 포인터: 인스턴스 → 자신의 클래스
✅ parent 포인터: 클래스 → 부모 클래스

class + parent 포인터 조합
→ 최상위 부모 클래스까지 모든 클래스 접근 가능

2.5 메서드 오버라이딩 vs 메서드 오버로딩

🔄 메서드 오버라이딩 (Method Overriding)

정의:

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

예시:

public class Lecture {
    public String evaluate() {
        return String.format("Pass:%d Fail:%d", passCount(), failCount());
    }
}

public class GradeLecture extends Lecture {
    @Override  // 동일한 시그니처
    public String evaluate() {
        return super.evaluate() + ", " + gradesStatistics();
    }
}

특징:

✅ 시그니처 동일
   - 메서드 이름
   - 매개변수 타입 및 개수
   - 반환 타입
   
✅ 부모 메서드를 "대체"
   - 자식의 메서드가 우선
   
✅ @Override 어노테이션 권장
   - 컴파일 타임 체크

➕ 메서드 오버로딩 (Method Overloading)

정의:

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

예시:

public class Lecture {
    // 시그니처: average()
    public double average() {
        return scores.stream()
                .mapToInt(Integer::intValue)
                .average()
                .orElse(0);
    }
}

public class GradeLecture extends Lecture {
    // 시그니처: average(String)
    // 이름은 같지만 매개변수가 다름!
    public double average(String gradeName) {
        return grades.stream()
                .filter(each -> each.isName(gradeName))
                .findFirst()
                .map(this::gradeAverage)
                .orElse(0d);
    }
}

특징:

✅ 시그니처 다름
   - 매개변수 타입/개수 다름
   
✅ 부모 메서드와 "공존"
   - 둘 다 사용 가능
   
✅ 서로 다른 메서드
   - average() ≠ average(String)

비교표:

측면 오버라이딩 오버로딩
시그니처 동일 다름
관계 대체 공존
다형성 런타임 컴파일타임
목적 행동 변경 편의성

3. 업캐스팅과 동적 바인딩

📂 코드: Professor.java

3.1 핵심 메커니즘

코드 안에서 선언된 참조 타입과 무관하게
실제로 메시지를 수신하는 객체의 타입에 따라
실행되는 메서드가 달라질 수 있는 것은

업캐스팅 + 동적 바인딩

이라는 메커니즘이 작용하기 때문이다.

3.2 업캐스팅 (Upcasting)

🎯 정의

업캐스팅:

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

💡 왜 가능한가?

상속을 이용하면
부모 클래스의 퍼블릭 인터페이스가
자식 클래스의 퍼블릭 인터페이스에 합쳐진다.

따라서:
부모 클래스의 인스턴스에게 전송할 수 있는 메시지는
자식 클래스의 인스턴스에게도 전송할 수 있다.

컴파일러는
명시적인 타입 변환 없이도
자식 클래스가 부모 클래스를 대체할 수 있게 허용한다.

📝 예시들

기본 업캐스팅:

// 선언: Lecture 타입
// 할당: GradeLecture 인스턴스
Lecture lecture = new GradeLecture(
    "객체지향 프로그래밍",
    70,
    Arrays.asList(
        new Grade("A", 100, 95),
        new Grade("B", 94, 80),
        new Grade("C", 79, 70),
        new Grade("D", 69, 50),
        new Grade("F", 49, 0)
    ),
    Arrays.asList(81, 95, 75, 50, 45)
);

// Lecture 타입이지만 GradeLecture의 evaluate() 실행!
String result = lecture.evaluate();
// "Pass:3 Fail:2, A:1 B:1 C:1 D:1 F:1"

파라미터로 전달:

public class Professor {
    private String name;
    private Lecture lecture;  // Lecture 타입
    
    public Professor(String name, Lecture lecture) {
        this.name = name;
        this.lecture = lecture;
    }
    
    public String compileStatistics() {
        return String.format("[%s] %s - Avg: %.1f", 
                name, 
                lecture.evaluate(),      // 다형성!
                lecture.average());
    }
}

사용:

// GradeLecture 인스턴스를 Lecture 파라미터에 전달
Professor professor = new Professor(
    "홍길동",
    new GradeLecture(...)  // 업캐스팅!
);

String stats = professor.compileStatistics();
// "[홍길동] Pass:3 Fail:2, A:1 B:1 C:1 D:1 F:1 - Avg: 69.2"

3.3 다운캐스팅 (Downcasting)

⚠️ 정의

다운캐스팅:

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

📝 예시

Lecture lecture = new GradeLecture(...);  // 업캐스팅

// 다운캐스팅 (명시적 타입 변환 필요)
GradeLecture gradeLecture = (GradeLecture) lecture;

// 이제 GradeLecture의 메서드 호출 가능
double averageA = gradeLecture.average("A");

⚠️ 주의사항

Lecture lecture = new Lecture(...);  // 실제로 Lecture 인스턴스

// ❌ 런타임 에러!
GradeLecture gradeLecture = (GradeLecture) lecture;
// ClassCastException 발생

// ✅ 안전한 다운캐스팅
if (lecture instanceof GradeLecture) {
    GradeLecture gradeLecture = (GradeLecture) lecture;
    // 안전하게 사용
}

📊 업캐스팅 vs 다운캐스팅

                Object
                  ↑
                  │
               Lecture
                  ↑
                  │
            GradeLecture
                  ↑
                  │
       FormattedGradeLecture
       
       
       업캐스팅 (Upcasting)
       ────────────────────▶
       자동 (암묵적)
       안전
       
       
       ◀────────────────────
       다운캐스팅 (Downcasting)
       명시적 캐스팅 필요
       위험 (ClassCastException 가능)

3.4 동적 바인딩 (Dynamic Binding)

🎯 정의

동적 바인딩:

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

객체지향 시스템이
메시지를 처리할 적절한 메서드를
컴파일 시점이 아니라 실행 시점에 결정한다.

🔄 함수 호출 vs 메시지 전송

함수 호출 (전통적 언어):

함수를 실행하는 방법:
- 함수를 호출

호출될 코드 결정:
- 코드 작성 시점에 결정

바인딩 방식:
- 정적 바인딩 (Static Binding)
- 초기 바인딩 (Early Binding)
- 컴파일타임 바인딩 (Compile-time Binding)
// C 언어 예시
int add(int a, int b) {
    return a + b;
}

int result = add(1, 2);  // 컴파일 시점에 add 함수로 결정됨

메시지 전송 (객체지향 언어):

메서드를 실행하는 방법:
- 메시지를 전송

실행될 메서드 결정:
- 메시지를 수신했을 때 (런타임)

바인딩 방식:
- 동적 바인딩 (Dynamic Binding)
- 지연 바인딩 (Late Binding)
Lecture lecture = ???;  // 컴파일 시점에는 모름
String result = lecture.evaluate();  // 런타임에 결정!

📊 실행 시점 결정

public class Professor {
    public String compileStatistics() {
        return lecture.evaluate();  // 어떤 evaluate()???
    }
}

경우 1: Lecture 인스턴스

Professor prof = new Professor(
    "교수A",
    new Lecture(...)  // Lecture 인스턴스
);

prof.compileStatistics();
→ Lecture.evaluate() 실행"Pass:3 Fail:2"

경우 2: GradeLecture 인스턴스

Professor prof = new Professor(
    "교수B",
    new GradeLecture(...)  // GradeLecture 인스턴스
);

prof.compileStatistics();
→ GradeLecture.evaluate() 실행"Pass:3 Fail:2, A:1 B:1 C:1 D:1 F:1"

핵심:

동일한 코드 (lecture.evaluate())

하지만!

실제 인스턴스 타입에 따라
실행되는 메서드가 다름

이것이 동적 바인딩!

3.5 업캐스팅 + 동적 바인딩 = 다형성

┌─────────────────────────────────────────────────────┐
│                                                     │
│  업캐스팅:                                            │
│  - 부모 타입 변수에 자식 인스턴스 할당 가능                  │
│                                                     │
│  동적 바인딩:                                          │
│  - 실제 인스턴스 타입에 따라 메서드 결정                     │
│                                                     │
│  ═══════════════════════════════                    │
│                                                     │
│  다형성:                                              │
│  - 동일한 메시지, 다른 실행 결과                           │
│  - 코드 변경 없이 기능 확장 가능                           │
│                                                     │
└─────────────────────────────────────────────────────┘

3.6 개방-폐쇄 원칙과의 연결

업캐스팅 + 동적 메서드 탐색
↓
코드 변경 없이 기능 추가
↓
개방-폐쇄 원칙 (OCP)

예시:

// 기존 코드 (변경 없음)
public class Professor {
    public String compileStatistics() {
        return lecture.evaluate();  // 그대로
    }
}

// 새로운 강의 타입 추가 (확장)
public class FormattedGradeLecture extends GradeLecture {
    @Override
    public String evaluate() {
        return "★ " + super.evaluate() + " ★";
    }
}

// 사용
Professor prof = new Professor(
    "교수C",
    new FormattedGradeLecture(...)  // 새로운 타입!
);

// 기존 코드는 그대로인데 새로운 기능 실행!
prof.compileStatistics();
→ "★ Pass:3 Fail:2, A:1 B:1 C:1 D:1 F:1 ★"

4. 동적 메서드 탐색과 다형성

📂 코드: Lecture.java (step02)

4.1 메서드 탐색 규칙

📋 동적 메서드 탐색 과정

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

1. 메시지를 수신한 객체는
   먼저 자신을 생성한 클래스에
   적합한 메서드가 존재하는지 검사한다.
   존재하면 메서드를 실행하고 탐색을 종료한다.

2. 메서드를 찾지 못했다면
   부모 클래스에서 메서드 탐색을 계속한다.
   이 과정은 적합한 메서드를 찾을 때까지
   상속 계층을 따라 올라가며 계속된다.

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

4.2 self 참조 (self reference)

🎯 정의

self 참조:

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

자바에서는 self 참조를 this라고 부른다.

📊 self 참조 동작

1. 메시지 수신
   ↓
2. self 참조 생성
   → 수신 객체를 가리킴
   ↓
3. 동적 메서드 탐색
   → self가 가리키는 객체의 클래스에서 시작
   → 상속 계층의 역방향으로 진행
   ↓
4. 메서드 실행
   ↓
5. self 참조 소멸

4.3 메서드 탐색 예시

📝 시나리오 1: 메서드 오버라이딩

Lecture lecture = new GradeLecture(...);
lecture.evaluate();

탐색 과정:

1. evaluate() 메시지 수신
   ↓
2. self 참조 생성
   → self = GradeLecture 인스턴스
   ↓
3. GradeLecture 클래스에서 evaluate() 탐색
   ↓
4. 발견! → GradeLecture.evaluate() 실행
   ↓
5. 탐색 종료

시각화:

lecture (Lecture)
   │
   ▼
┌─────────────────────┐
│ GradeLecture 인스턴스 │  ◀── self
├─────────────────────┤
│ class ──────────────┼──▶ GradeLecture
└─────────────────────┘         │
                                │ evaluate() ✅ 발견!
                                │
                                ▼
                          ┌─────────────┐
                          │   Lecture   │
                          ├─────────────┤
                          │ evaluate()  │
                          └─────────────┘
                          (탐색하지 않음)

📝 시나리오 2: 메서드 오버로딩

Lecture lecture = new GradeLecture(...);
lecture.average();  // 매개변수 없음

탐색 과정:

1. average() 메시지 수신
   ↓
2. self 참조 생성
   → self = GradeLecture 인스턴스
   ↓
3. GradeLecture 클래스에서 average() 탐색
   → average(String) 발견
   → 하지만 시그니처 다름! (매개변수 없는 버전 필요)
   ↓
4. 부모 클래스 Lecture에서 average() 탐색
   ↓
5. 발견! → Lecture.average() 실행
   ↓
6. 탐색 종료

시각화:

lecture (Lecture)
   │
   ▼
┌─────────────────────┐
│ GradeLecture 인스턴스 │  ◀── self
├─────────────────────┤
│ class ──────────────┼──▶ ┌──────────────────┐
└─────────────────────┘    │  GradeLecture    │
                           ├──────────────────┤
                           │ average(String)  │ ❌ 시그니처 다름
                           │                  │
                           │ parent ──────────┼──▶ ┌─────────────┐
                           └──────────────────┘    │  Lecture    │
                                                   ├─────────────┤
                                                   │ average()   │ ✅ 발견!
                                                   └─────────────┘

4.4 self 전송 (self send)

🎯 개념

self 전송:

자기 자신에게 메시지를 전송하는 것

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

즉, self 참조가 가리키는 객체에게
메시지를 전송하는 것

📝 예시

public class Lecture {
    public String stats() {
        return String.format("Title: %s, Evaluation Method: %s",
                title, getEvaluationMethod());
                //      ^^^^^^^^^^^^^^^^^^^ self 전송!
    }
    
    public String getEvaluationMethod() {
        return "Pass or Fail";
    }
}

코드 해석:

stats() 메서드에서 getEvaluationMethod() 메시지 전송

이것은:
❌ Lecture 클래스의 getEvaluationMethod() 호출이 아님!
✅ self 참조가 가리키는 객체에게
   getEvaluationMethod 메시지 전송

🔄 self 전송의 중요성

GradeLecture 추가:

public class GradeLecture extends Lecture {
    @Override
    public String getEvaluationMethod() {
        return "Grade";
    }
}

시나리오:

Lecture lecture = new GradeLecture(...);
String result = lecture.stats();

탐색 과정:

1. stats() 메시지 수신
   → self = GradeLecture 인스턴스
   ↓
2. GradeLecture에서 stats() 탐색
   → 없음
   ↓
3. Lecture에서 stats() 탐색
   → 발견! → Lecture.stats() 실행
   ↓
4. Lecture.stats() 내부에서
   getEvaluationMethod() 메시지 전송 (self 전송!)
   ↓
5. self가 가리키는 GradeLecture에서 탐색 재시작!
   ↓
6. GradeLecture.getEvaluationMethod() 발견
   → 실행!
   ↓
7. 결과: "Grade" 반환

시각화:

lecture (Lecture)
   │
   ▼
┌─────────────────────┐
│ GradeLecture 인스턴스 │  ◀── self (항상 여기를 가리킴)
├─────────────────────┤
│ class ──────────────┼──▶ ┌───────────────────────────┐
└─────────────────────┘    │    GradeLecture           │
                           ├───────────────────────────┤
                           │ getEvaluationMethod()     │ ✅ 2번째 탐색에서 발견
                           │                           │
                           │ parent ───────────────────┼──▶ ┌──────────────────────┐
                           └───────────────────────────┘    │    Lecture           │
                                                            ├──────────────────────┤
                                                            │ stats() {            │ ✅ 1번째 탐색에서 발견
                                                            │   getEvaluationMethod()   ⬅ self 전송!
                                                            │ }                    │
                                                            │ getEvaluationMethod()│
                                                            └──────────────────────┘

결과:

String result = lecture.stats();
// "Title: 객체지향 프로그래밍, Evaluation Method: Grade"
//                                               ^^^^^ GradeLecture의 메서드!

💡 핵심 통찰

self 전송을 이해하기 위한 핵심:

self 참조가 가리키는 바로 그 객체에서부터
메시지 탐색을 다시 시작한다!

이것이 다형성의 진정한 힘이다.

4.5 자동적인 메시지 위임

🎯 개념

상속을 이용할 경우
메시지를 처리할 방법을 알지 못하면
메시지에 대한 처리를 부모 클래스에 위임한다.

적절한 메서드를 찾을 때까지
상속 계층을 따라 부모 클래스로 처리가 위임된다.

중요:
프로그래머가 메시지 위임과 관련된 코드를
명시적으로 작성할 필요가 없다!
메시지는 상속 계층을 따라 자동적으로 위임된다.

📊 위임 경로

자식 클래스 → 부모 클래스 방향

따라서:
자식 클래스에서 어떤 메서드를 구현하고 있느냐에 따라
부모 클래스에 구현된 메서드의 운명이 결정된다.

메서드 오버라이딩:

자식 클래스의 메서드가
부모 클래스의 메서드를 감춘다.

┌──────────────┐
│ GradeLecture │
├──────────────┤
│ evaluate()   │ ← 이 메서드가 실행됨
└──────────────┘
       ↑
       │
┌──────────────┐
│   Lecture    │
├──────────────┤
│ evaluate()   │ ← 감춰짐 (실행 안 됨)
└──────────────┘

메서드 오버로딩:

자식 클래스의 메서드와
부모 클래스의 메서드가 공존한다.

┌──────────────────┐
│  GradeLecture    │
├──────────────────┤
│ average(String)  │ ← 시그니처 다름
└──────────────────┘
       ↑
       │
┌──────────────────┐
│    Lecture       │
├──────────────────┤
│ average()        │ ← 공존
└──────────────────┘

4.6 동적인 문맥

🎯 핵심

메시지를 수신한 객체가 무엇이냐에 따라
메서드 탐색을 위한 문맥이 동적으로 바뀐다.

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

💡 중요한 사실

동일한 코드라고 하더라도
self 참조가 가리키는 객체가 무엇인지에 따라
메서드 탐색을 위한 상속 계층의 범위가
동적으로 변한다.

self 참조가 가리키는 객체의 타입을 변경함으로써
객체가 실행될 문맥을 동적으로 바꿀 수 있다.

📝 예시: FormattedGradeLecture

public class FormattedGradeLecture extends GradeLecture {
    public FormattedGradeLecture(String name, int pass, 
                                 List<Grade> grades, 
                                 List<Integer> scores) {
        super(name, pass, grades, scores);
    }
    
    public String formatAverage() {
        return String.format("Avg: %1.1f", super.average());
    }
}

시나리오 1: GradeLecture 인스턴스

Lecture lecture = new GradeLecture(...);
lecture.stats();

탐색 범위:

self = GradeLecture 인스턴스

탐색 범위:
GradeLecture → Lecture → Object

시나리오 2: FormattedGradeLecture 인스턴스

Lecture lecture = new FormattedGradeLecture(...);
lecture.stats();

탐색 범위:

self = FormattedGradeLecture 인스턴스

탐색 범위:
FormattedGradeLecture → GradeLecture → Lecture → Object

(더 넓은 범위!)

4.7 이해할 수 없는 메시지

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

정의:

정적 타입 언어:
- 컴파일 시에 변수의 타입이 결정되는 언어
- 예: Java, C, C++, C#

처리 방식:

Lecture lecture = new GradeLecture(...);
lecture.unknownMessage();  // ❌ 컴파일 에러!

탐색 과정:

1. 컴파일 시점에 상속 계층 탐색
   ↓
2. GradeLecture에 unknownMessage() 있는지 확인
   → 없음
   ↓
3. Lecture에 unknownMessage() 있는지 확인
   → 없음
   ↓
4. Object에 unknownMessage() 있는지 확인
   → 없음
   ↓
5. 더 이상 부모 클래스가 없음
   ↓
6. 컴파일 에러 발생!
   "Cannot find symbol: method unknownMessage()"

특징:

✅ 컴파일 타임에 확인
   - 실행 전에 오류 발견
   
✅ 안정성
   - 런타임 오류 가능성 감소
   
❌ 유연성 부족
   - 동적 메서드 추가 불가

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

정의:

동적 타입 언어:
- 런타임 시 자료형이 결정되는 언어
- 예: Python, JavaScript, Ruby

처리 방식 (Ruby 예시):

lecture = GradeLecture.new(...)
lecture.unknown_message  # 실행 시점에 확인

탐색 과정:

1. 런타임에 메시지 수신
   ↓
2. GradeLecture에서 탐색
   → 없음
   ↓
3. Lecture에서 탐색
   → 없음
   ↓
4. Object에서 탐색
   → 없음
   ↓
5. method_missing 메시지 전송!
   (또는 doesNotUnderstand - Smalltalk)

method_missing 활용:

class Lecture
  def method_missing(method_name, *args)
    if method_name.to_s.start_with?('find_by_')
      # 동적으로 메서드 처리
      attribute = method_name.to_s.sub('find_by_', '')
      # ...
    else
      super  # 기본 예외 발생
    end
  end
end

lecture.find_by_title("OOP")  # 동적으로 처리!

특징:

✅ 유연성
   - 동적 메서드 추가 가능
   - 메타프로그래밍 지원
   
✅ 메시지와 구현 분리
   - 인터페이스 ≠ 구현
   
❌ 안정성 부족
   - 런타임 오류 가능성
   
❌ 이해하기 어려움
   - 실제 실행 흐름 파악 어려움

📊 비교표

측면 정적 타입 언어 동적 타입 언어
타입 결정 컴파일타임 런타임
오류 발견 컴파일 시 실행 시
이해할 수 없는 메시지 컴파일 에러 method_missing 등
안정성 높음 낮음
유연성 낮음 높음

4.8 super 참조

🎯 정의

super 참조:

자식 클래스에서 부모 클래스의 인스턴스 변수나
메서드에 접근하기 위해 사용할 수 있는
내부 변수

정확한 의도:
"지금 self 참조가 가리키는 클래스의
 부모 클래스에서부터 메서드 탐색을 시작하세요"

📝 예시

기본 사용:

public class GradeLecture extends Lecture {
    @Override
    public String evaluate() {
        return super.evaluate() + ", " + gradesStatistics();
        //     ^^^^^ super 참조
    }
}

탐색 과정:

super.evaluate() 호출
↓
부모 클래스(Lecture)에서부터 탐색 시작
↓
Lecture.evaluate() 발견
↓
실행

FormattedGradeLecture:

public class FormattedGradeLecture extends GradeLecture {
    public String formatAverage() {
        return String.format("Avg: %1.1f", super.average());
        //                                  ^^^^^ super 참조
    }
}

탐색 과정:

super.average() 호출
↓
부모 클래스(GradeLecture)에서부터 탐색 시작
↓
GradeLecture에 average() 없음
↓
Lecture에서 average() 탐색
↓
Lecture.average() 발견
↓
실행

💡 중요한 특징

super 참조를 통해 실행하고자 하는 메서드가
반드시 부모 클래스에 위치하지 않아도 됨!

그 메서드가 조상 클래스 어딘가에 있기만 하면
성공적으로 탐색됨

→ 유연성 제공

🎯 super 전송 (super send)

정의:

super 참조를 통해 메시지를 전송하는 것을
super 전송(super send)이라고 부른다.

시각화:

┌──────────────────────┐
│FormattedGradeLecture │  ◀── self 참조
├──────────────────────┤
│ formatAverage() {    │
│   super.average()    │ ← super 전송
│ }                    │
│                      │
│ parent ──────────────┼──▶ ┌────────────────┐
└──────────────────────┘    │ GradeLecture   │ ← 여기서부터 탐색 시작
                            ├────────────────┤
                            │ average(String)│ ← 시그니처 다름
                            │                │
                            │ parent ────────┼──▶ ┌─────────────┐
                            └────────────────┘    │  Lecture    │
                                                  ├─────────────┤
                                                  │ average()   │ ✅ 발견!
                                                  └─────────────┘

⚡ super와 컴파일타임 vs 런타임

일반적:

super 참조는 컴파일 시점에 결정됨
- 정적으로 부모 클래스 지정

예외 (믹스인):

스칼라의 traits 같은 믹스인 메커니즘에서는
super의 대상을 믹스인되는 순서에 따라
동적으로 결정함!

따라서:
사용하는 언어의 특성에 따라
컴파일 시점이 아닌 실행 시점에
super의 대상이 결정될 수도 있다.

4.9 메서드 탐색 정리

🎯 핵심 원리

동적 메서드 탐색의 두 가지 원리:

1. 자동적인 메시지 위임
   - 자식 클래스가 이해할 수 없는 메시지를
     자동으로 상속 계층을 따라 부모 클래스에게 위임
     
2. 동적인 문맥 사용
   - 메시지를 수신했을 때
     실제로 어떤 메서드를 실행할지는 런타임에 결정
   - self 참조를 이용해서 결정

📊 탐색 흐름도

메시지 수신
    ↓
self 참조 생성 (수신 객체를 가리킴)
    ↓
self가 가리키는 객체의 클래스에서 탐색 시작
    ↓
메서드 발견? ──── YES ──→ 실행 → 종료
    │
    NO
    ↓
부모 클래스로 이동
    ↓
메서드 발견? ──── YES ──→ 실행 → 종료
    │
    NO
    ↓
더 상위 부모 클래스로 이동
    ↓
    ... (반복)
    ↓
최상위 클래스(Object)까지 도달?
    ↓
    YES
    ↓
예외 발생 (정적 타입: 컴파일 에러)
         (동적 타입: method_missing 등)

5. 상속 대 위임

📂 코드: Lecture.js | Lecture.rb

5.1 위임 관점에서 본 상속

🎯 새로운 관점

전통적 관점:

상속 = 코드 재사용

새로운 관점:

상속 = 자식에서 부모로 self 참조를 전달하는 메커니즘

💡 위임(Delegation)이란?

정의:

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

자신이 정의하지 않거나 처리할 수 없는
속성 또는 메서드의 탐색 과정을
다른 객체로 이동시키는 것

🔄 상속은 위임이다

상속의 본질:
자식 클래스가 자신이 이해할 수 없는 메시지를 받으면
자동으로 부모 클래스에게 처리를 위임한다.

중요:
이때 self 참조도 함께 전달된다!

시각화:

┌──────────────────┐
│   GradeLecture   │  ◀── self (계속 여기를 가리킴)
├──────────────────┤
│ stats() 메시지     │
│   ↓              │
│ 없음!             │
│   ↓              │
│ 부모에게 위임   ───┼──▶  ┌─────────────────┐
└──────────────────┘    │    Lecture      │
                        ├─────────────────┤
                        │ stats() {       │
                        │   ...           │
                        │   getEvaluationMethod() ← self 전송!
                        │ }               │
                        └─────────────────┘
                                │
                                │ self를 따라 다시 GradeLecture로
                                ▼
                        ┌─────────────────┐
                        │  GradeLecture   │
                        ├─────────────────┤
                        │ getEvaluationMethod() ✅
                        └─────────────────┘

5.2 Ruby 예시 (위임의 명시적 구현)

📝 Ruby로 보는 위임

Lecture 클래스:

class Lecture
  def initialize(name, scores)
    @name = name
    @scores = scores
  end
  
  def stats(this)
    "Name: #{@name}, Evaluation Method: #{this.getEvaluationMethod(this)}"
  end
  
  def getEvaluationMethod()
    "Pass or Fail"
  end
end

GradeLecture 클래스 (합성 사용):

class GradeLecture
  def initialize(name, canceled, scores)
    @parent = Lecture.new(name, scores)  # ← 합성!
    @canceled = canceled
  end
  
  def stats(this)
    @parent.stats(this)  # ← 명시적 위임 + self 전달
  end
  
  def getEvaluationMethod()
    "Grade"
  end
end

사용:

lecture = Lecture.new("OOP", [1, 2, 3])
puts lecture.stats(lecture)
# "Name: OOP, Evaluation Method: Pass or Fail"

grade_lecture = GradeLecture.new("OOP", false, [1, 2, 3])
puts grade_lecture.stats(grade_lecture)
# "Name: OOP, Evaluation Method: Grade"

동작 과정:

grade_lecture.stats(grade_lecture) 호출
↓
GradeLecture.stats 실행
↓
@parent.stats(grade_lecture) 호출  ← this = grade_lecture
↓
Lecture.stats 실행
↓
this.getEvaluationMethod(this) 호출
↓
grade_lecture.getEvaluationMethod() 실행  ← this를 따라감!
↓
"Grade" 반환

💡 핵심 통찰

Ruby 예시에서 명시적으로 한 것:
1. @parent를 통한 메시지 위임
2. this를 통한 self 참조 전달

상속은 이것을 자동으로 해준다!

5.3 프로토타입 기반 객체지향

🎯 개념

프로토타입 기반:

클래스가 존재하지 않고
오직 객체만 존재하는 객체지향 언어

상속을 구현하는 유일한 방법:
→ 객체 사이의 위임

대표 언어:

JavaScript

5.4 JavaScript의 prototype

📝 Lecture 함수 (생성자)

function Lecture(name, scores) {
    this.name = name;
    this.scores = scores;
}

// prototype에 메서드 추가
Lecture.prototype.stats = function() {
    return "Name: " + this.name + 
           ", Evaluation Method: " + this.getEvaluationMethod();
}

Lecture.prototype.getEvaluationMethod = function() {
    return "Pass or Fail";
}

📝 GradeLecture 함수

function GradeLecture(name, canceled, scores) {
    Lecture.call(this, name, scores);  // 부모 생성자 호출
    this.canceled = canceled;
}

// prototype 체인 설정
GradeLecture.prototype = new Lecture();
GradeLecture.prototype.constructor = GradeLecture;

// 메서드 오버라이드
GradeLecture.prototype.getEvaluationMethod = function() {
    return "Grade";
}

🔄 사용 및 탐색

객체 생성:

var lecture = new Lecture("OOP", [1, 2, 3]);
console.log(lecture.stats());
// "Name: OOP, Evaluation Method: Pass or Fail"

var gradeLecture = new GradeLecture("OOP", false, [1, 2, 3]);
console.log(gradeLecture.stats());
// "Name: OOP, Evaluation Method: Grade"

메서드 탐색 과정:

gradeLecture.stats() 호출
↓
1. gradeLecture 객체에서 stats 탐색
   → 없음
↓
2. gradeLecture.prototype (Lecture 인스턴스)에서 탐색
   → stats 발견! → 실행
↓
3. stats 내부에서 this.getEvaluationMethod() 호출
   (this = gradeLecture)
↓
4. gradeLecture에서 getEvaluationMethod 탐색
   → 없음
↓
5. gradeLecture.prototype에서 getEvaluationMethod 탐색
   → 발견! → 실행
↓
6. "Grade" 반환

📊 prototype 체인 시각화

gradeLecture 객체
    │
    │ prototype
    ▼
┌────────────────────────────┐
│  new Lecture()             │
│  (GradeLecture.prototype)  │
├────────────────────────────┤
│  getEvaluationMethod()     │ ← 오버라이드된 메서드
│                            │
│  prototype ────────────────┼──▶ ┌─────────────────────┐
└────────────────────────────┘    │  Lecture.prototype  │
                                  ├─────────────────────┤
                                  │  stats()            │
                                  │  getEvaluationMethod()
                                  └─────────────────────┘

5.5 상속 vs 위임 정리

📊 비교표

측면 클래스 기반 상속 프로토타입 기반 위임
구조 클래스 계층 객체 체인
코드 위치 클래스 객체
메서드 탐색 parent 포인터 prototype 포인터
self 전달 자동 자동
특징 정적 계층 동적 연결

💡 공통점

1. 메시지 처리를 위임하기 위한 경로를 정의
   - 상속: parent 포인터
   - 위임: prototype 체인
   
2. 경로의 시작점은 런타임의 self (this)
   - self 참조가 가리키는 객체에서 시작
   
3. 자식이 처리할 수 없는 메시지는 부모에게 위임
   - 자동적인 메시지 위임
   
4. 경로를 정의하는 방법만 다를 뿐
   - 상속: 클래스 계층
   - 위임: 객체 체인

🎯 핵심 통찰

┌─────────────────────────────────────────────────────┐
│                                                     │
│  클래스 기반 객체지향과                                  │
│  객체 기반 객체지향 사이에                                │
│                                                     │
│  같은 기본 개념과 메커니즘이 공유된다!                       │
│                                                     │
│  1. 메시지 위임 경로 정의                                │
│  2. self 참조 기반 탐색                                │
│  3. 자동적인 위임                                      │
│                                                     │
│  → 클래스는 필수가 아니다                                 │
│  → 상속 외에도 다형성 구현 가능                            │
│                                                     │
└─────────────────────────────────────────────────────┘

6. 핵심 정리

🎯 다형성의 본질

다형성은 런타임에
메시지를 처리하기에 적합한 메서드를
동적으로 탐색하는 과정에서 얻어진다.

다형성을 구현하는 방법:
1. 상속 (클래스 기반)
2. 위임 (프로토타입 기반)

📏 상속의 진정한 목적

┌─────────────────────────────────────────────────────┐
│                                                     │
│  상속의 목적은 코드 재사용이 아니다!                        │
│                                                     │
│  상속의 목적은                                         │
│  다형성을 위한 서브타입 계층을 구축하는 것이다.                │
│                                                     │
└─────────────────────────────────────────────────────┘

🔑 핵심 메커니즘

1. 업캐스팅

부모 타입 변수 = 자식 타입 인스턴스

허용 이유:
자식 클래스는 부모 클래스의
모든 퍼블릭 인터페이스를 포함하므로
부모 클래스를 대체 가능

2. 동적 바인딩

선언된 타입 ≠ 실행되는 메서드

실제 인스턴스 타입에 따라
실행 시점에 메서드 결정

컴파일타임 타입 → 런타임 타입

3. 동적 메서드 탐색

탐색 시작: self 참조가 가리키는 객체의 클래스
탐색 방향: 자식 → 부모 (상속 계층 따라)
탐색 종료: 메서드 발견 또는 최상위 클래스 도달

4. self 참조

메시지를 수신한 객체를 가리킴

특징:
- 자동 생성/소멸
- 메서드 탐색의 출발점
- self 전송 시 다시 탐색 시작

5. super 참조

부모 클래스에서부터 탐색 시작

의미:
"self가 가리키는 클래스의
 부모 클래스에서부터 탐색하세요"

📊 메서드 탐색 요약

요소 역할 특징
class 포인터 인스턴스 → 클래스 메서드 공유
parent 포인터 클래스 → 부모 클래스 탐색 경로
self 참조 현재 객체 탐색 시작점
super 참조 부모부터 탐색 명시적 위임

💡 다형성 구현 방법

상속 기반 (Java)

// 클래스 계층
LectureGradeLectureFormattedGradeLecture

// 다형성
Lecture lecture = new GradeLecture(...);
lecture.evaluate();  // GradeLecture.evaluate() 실행

위임 기반 (JavaScript)

// prototype 체인
gradeLecture
   GradeLecture.prototype
     Lecture.prototype

// 다형성
gradeLecture.stats();  // prototype 체인 따라 탐색

🎓 핵심 교훈

1. 상속은 다형성을 위한 도구
   - 코드 재사용은 부수 효과
   
2. 다형성의 핵심은 동적 메서드 탐색
   - self 참조 기반
   - 자동적인 메시지 위임
   
3. 클래스는 필수가 아님
   - 프로토타입으로도 가능
   - 객체 간 위임으로 구현
   
4. 메시지 관점에서 생각하기
   - 누가 메시지를 수신하는가?
   - self는 무엇을 가리키는가?
   - 탐색은 어디서 시작하는가?

🔗 연결고리

이전 장과의 연결

  • Chapter 10: 상속의 문제점 → 다형성의 올바른 이해
  • Chapter 11: 합성 vs 상속 → 다형성 구현의 다양한 방법
  • 상속의 진정한 목적: 타입 계층 구조화

다음 장 예고

  • Chapter 13: 서브클래싱과 서브타이핑
    • 상속의 두 가지 용도
    • 리스코프 치환 원칙 (LSP)
    • 올바른 상속의 조건