[오브젝트] #12. 다형성

bien·2024년 12월 1일
0

오브젝트

목록 보기
12/13
  • 상속의 목적
    • 코드의 재사용
      • 상속을 사용하려는 목적이 코드 재사용인가?
    • 타입 계층의 구조화 ⭕️
      • 클라이언트 관점에서 인스턴스를 동일하게 행동하는 그룹으로 묶기 위해서인가?

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

이번장에서는 다음을 알아보자!

  • 타입 계층의 개념
  • 다형적인 타입 계층을 구현하는 방법
  • 올바른 타입 계층을 구성하기 위해 고려해야 하는 원칙

01. 다형성

  • 다형성(Polymorphism)
    • 많은(poly) + 형태(morph) = 많은 형태를 가질 수 있는 능력
    • from 컴퓨터 과학
      1. 하나의 추상 인터페이스에 대해 코드를 작성하고
      2. 이 추상 인터페이스에 대해 서로 다른 구현을 연결할 수 있는 능력
    • 여러 타입을 대상으로 동작할 수 있는 코드를 작성할 수 있는 방법

객체지향의 다형성 분류

  1. 오버로딩 다형성
    • 하나의 클래스 안에 동일한 이름의 메서드가 존재하는 경우
    • 유사한 작업을 수행하는 메서드의 이름을 통일하여 외워야하는 이름의 수를 극적으로 줄일 수 있다.
public class Money {
	public Money plus(Money amount) { ... }
    public Money plus(BigDecimal amount) { ... }
    public Money plus(long amount) { ... }
}
  1. 강제 다형성
    • 언어가 지원하는 자동적인 타입변환 혹은 사용자가 직접 구현한 타입 변환을 이용해 동일한 연산자를 다양한 타입에 사용할 수 있는 방식
      • 정수 + 문자열
        • 덧셈연산자(+)가 연결연산자로 동작한다.
        • 이때 정수형 피연산자는 문자열 타입으로 강제 형변환된다.
  1. 매개변수 다형성 (= 제네릭 프로그래밍)
    • 클래스의 인스턴스 변수나 메서드의 매개변수 타입을 임의의 변수로 선언 후, 사용하는 시점에 구체적인 타입으로 지정
    • 자바의 List 인터페이스 (List<T>); 인스턴스 생성 시점에 T를 구체적인 타입으로 지정한다.
      • 다양한 타입의 요소를 다루기 위해 동일한 오퍼레이션을 사용할 수 있다.
  1. 포함 다형성 (= 서브타입(Subtype) 다형성)
    • 메시지가 동일하더라도 수신한 객체의 타입에 따라 실제로 수행되는 행동이 달라지는 능력
    • 객체지향 프로그래밍에서 가장 널리 알러진 형태의 다형성으로, 일반적으로 다형성이라고 하면 포함 다형성을 가리킨다.
public class Movie {
	private DiscountPolicy discountPolicy;
    
    public Money calculateMovieFee(Screening screening) {
    	return fee.minus(discountPolicy.calculateDiscountAmount(screening));
    }
}       
  • 포함 다형성을 위한 전제조건: 자식 클래스가 부모 클래스의 서브 타입일 것
    • 상속의 진정한 목적: 다형성을 위한 서브타입 계층을 구축하는 것

상속은 클래스들을 계층으로 쌓아 올린 후 상황에 따라 적절한 메서드를 선택할 수 있는 메커니즘을 제공한다.
객체가 메시지를 수신하면 객체지향 시스템은 메서드를 처리할 적절한 메서드를 상속 계층 안에서 탐색한다.
실행할 메서드의 선택은 아래의 영향요소에 의해 달라진다.

  1. 어떤 메시지를 수신했는지
  2. 어떤 클래스의 인스턴스인지
  3. 상속계층이 어떻게 구성되어 있는지

이번 장에서는

  • 포함 다형성의 관점에서 런타임에 상속 계층 안에서 적절한 메서드를 선택하는 방법을 이해해보자.
  • 이번 장에서 다루는 내용은 (상속 이외에도) 포함 다형성을 구현할 수 있는 다양한 방법에 공통적으로 적용할 수 있는 개념이다.

02. 상속의 양면성

  • 객체지향의 핵심 아이디어: 데이터행동객체라고 불리는 하나의 실행단위 안으로 통합하는 것.
  • 상속
    • 데이터 관점: 부모 클래스에 정의한 모든 데이터를 자식 클래스의 인스턴스에 자동으로 포함시킨다.
    • 행동 관점: 부모 클래스에서 정의한 일부 메서드를 자식 클래스에 포함시킨다.
      • 단순히 데이터와 행동의 관점에서, 상속은 부모 클래스의 코드를 자식과 공유하는 재사용 메커니즘으로 보일 것이다.
  • 상속의 목적
    • 프로그램을 구성하는 개념들을 기반으로 다형성을 가능하게 하는 타입 계층을 구축하기 위함
  • 상속 메커니즘 이해를 위해 필요한 개념들
    • 업캐스팅, 동적 메서드 탐색, 동적 바인딩, self 참조, super 참조

상속을 사용한 강의 평가

Lecture 클래스 살펴보기

📋 요구사항

  • 수강생들의 성적을 계산하는 간단한 예제 프로그램
  • 다음과 같은 형식으로 전체 수강생들의 성적 통계를 출력한다.
    • Pass:3 Fail:2, A:1 B:1 C:1 D:0 F:2
      • (앞부분)Pass:3 Fail:2: 강의를 이수한 학생의 수, 낙제한 학생의 수
        • 5명 중 3명이 강의 이수, 2명이 낙제
      • (뒷부분)A:1 B:1 C:1 D:0 F:2: 등급별 학생 분포 현황
        • A, B, C 학점이 각각 1명, F학점이 2명
  • 앞부분을 출력하는 Lecture라는 클래스가 먼저 구현되어 있는 상황.

Lecture.java (기존 코드)

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.unmodifieableList(scores);
    }
    
    public String evaluate() {
    	return String.format("Pass:%d Fail:%d", passCount(), failCount());
    }
    
    private long passCount() {
    	return scores.stream().filter(scroe -> score >= pass).count();
    }
    
    private long failCount() {
    	return scores.size() - passCount();
    }

}   

클라이언트 코드

  • 이수 기준이 70점인 객체지향 프로그래밍 과목의 수강생 5명에 대한 성적 통계
Lecture lecture = new Lecture("객체지향 프로그래밍",
								70,
                                Arrays.asList(81, 95, 75, 50, 45));
String evaluration = lecture.evaluate(); // 결과 => "Pass:3 Fail:2"

상속을 이용해 Lecture 클래스 재사용하기

  • GradeLecture
    • 원하는 기능: Lecture의 출력결과에 등급별 통계를 추가하는 것
      • Lecture를 상속받아 재사용
    • evaluate 오버라이딩: 학생들의 이수여부 + 등급별 통계

super

  • 자식 클래스 내부에서 부모 클래스의 인스턴스 변수나 메서드에 접근하는데 사용된다.
    • 정확하게는, 가시성의 public이나 protected인 인스턴스 변수와 메서드에만 접근 가능하다.
    • 가시성이 private인 경우에는 접근할 수 없다.

GradeLecture.java

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;
    }
    
    // 부모 메서드와 시그니처 동일: 오버라이딩(overriding)
    @Override
    public String evaluate() { 
     	return super.evaluate() + ", " + gradesStatistics(); //** super 호출
	}
    
    @Override
    private String gradesStatistics() {
    	return grades.stream()
        			.map(grade -> format(grade))
                    .collect(joining(" "));
	}
    
    private String format(Grade grade) {
    	return String.format("%s:%d"m grad.egetName(), gradeCount(grade));
    }
    
    private long gradeCount(Grade grade) {
    	return getScores().stream()
        					.filter(grade::include)
                            .count();
	}
    
    // 부모 메서드와 메서드명 동일, 파라미터 다름: 오버로딩(overloading)
    public double average(String gradeName) { 
    	return grades.stream()
        			.filter(each -> each.isName(gradeName))
                    .findFirst()
                    .map(this::gradeAvarage)
                    .orElse(0d);
	}
    
    private double gradeAverage(Grade grade) {
    	return getScores().stream()
        				.filter(grade::include)
                        .mapToInt(Integer::intValue)
                        .average()
                        .orElse(0);
	} 
    
}    

Grade.java

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;
    }

}
  • 메서드 오버라이딩(Overriding)
    • 자식 클래스에서 상속받은 메서드와 동일한 시그니처의 메서드를 재정의해서 부모 클래스의 구현을 새로운 구현으로 대체하는 것
    • 동일한 시그니처를 가진 자식 클래스의 메서드가 부모 클래스의 메서드를 가리게 된다.
      • 즉, 자식 클래스의 메서드가 부모 클래스의 메서드보다 우선 순위가 높다.
 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));

// 결과 => "Pass:3 Fail:2, A:1 B:1 C:1 D:1 F:1"
lecture.evaluate();
  • 메서드 오버로딩(Overloading)
    • 부모 클래스에서 정의한 메서드와 이름은 동일하지만 시그니처는 다른 메서드를 자식 클래스에 추가하는 것.
      • LectureaverageGradeLectureaverage

데이터 관점의 상속

Lecture 인스턴스 생성

Lecture lecture = new Lecture("객체지향 프로그래밍",
								70,
                                Arrays.asList(81, 95, 75, 50, 45));
  • 인스턴스 생성 시 시스템이 하는 일
    1. 인스턴스 변수(title, pass, scores)를 저장할 메모리 공간 할당
    2. 생성자의 매개변수를 이용해 값 설정
    3. 생성된 인스턴스의 주소를 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));
  • 인스턴스 관점에서 바라본 상속
    • 자식 클래스의 인스턴스 안에 부모 클래스의 인스턴스가 포함되는 개념
      • 인스턴스를 참조하는 lectureGradeLecture의 인스턴스를 가리키기 때문에, 특별한 방법 없이는 내부의 Lecture에 접근할 수 없다.
  • 자식 클래스의 인스턴스에서 부모 클래스의 인스턴스로 접근 가능한 링크가 존재한다고 생각해도 무방하다.

📌 요약

  • 데이터 관점에서의 상속
    • 자식 클래스의 인스턴스 안에 부모 클래스의 인스턴스를 포함하는 것.

행동 관점의 상속

  • 행동 관점의 상속
    • 부모 클래스가 정의한 일부 메서드를 자식 클래스의 메서드로 포함시키는 것
      • 부모 클래스의 모든 퍼블릭 메서드는 자식 클래스의 퍼블릭 인터페이스에 포함된다.
      • 이를 통해, 외부의 객체가 부모 클래스의 인스턴스에게 전송할 수 있는 모든 메시지는, 자식 클래스의 인스턴스에게도 전송할 수 있다.
  • 어떻게 부모 클래스의 구현 메서드를 자식 클래스의 인스턴스에서 수행할 수 있는 것일까?
    (행동 측면에서 유산상속은 어떻게 구현되는 걸까?)
    • 런타임에 시스템이 자식 클래스에 정의되지 않은 메서드가 있을 경우 이 메서드를 부모 클래스 안에서 탐색하기 때문이다.
    • 행동 관점에서 상속과 다형성의 기본적인 개념을 이해하기 위해서는, 상속관계 클래스 사이의 메서드 탐색 과정을 이해하는 것이 가장 중요하다.

클래스와 인스턴스

  • 인스턴스(객체): 서로 다른 상태를 저장해야 한다.
    • 각 인스턴스별로 독립적인 메모리를 할당받는다.
  • 클래스: 메서드는 동일한 클래스의 인스턴스끼리 공유가 가능하다.
    • 한번만 메모리에 로드
    • 인스턴스별 자신의 클래스를 가리키는 포인터 소유

클래스와 인스턴스의 관계; Lecture

그림에서 인스턴스는 2개가 생성되었으나, 클래스는 단 하나만 메모리에 로드되었다.

  1. 각 객체는 자신의 클래스인Lecture의 위치를 가리키는 class라는 이름의 포인터를 가진다.
    • 이 포인터를 이용해 자신의 클래스 정보에 접근할 수 있다.
  2. Lecture는 자신의 부모 클래스인 Object의 위치를 가리키는 parent라는 이름의 포인터를 가진다.
    • 이 포인터를 이용해 상속 계층을 따라 부모 클래스의 정의로 이동할 수 있다.

상속; GradeLecture

그림은 어떻게 클래스의 인스턴스를 통해 부모 클래스에 정의된 메서드를 실행할 수 있는지 보여준다.

  1. 메시지 수신 객체는 class 포인터로 연결된 자신의 클래스에 적절한 메서드가 존재하는지 찾는다.
  2. 만약 메서드가 존재하지 않으면 parent 포인터를 따라 부모 클래스를 훑어, 적절한 메서드 존재여부를 검색한다.
  • 자식 클래스에서 부모 클래스로의 메서드 탐색이 가능
    • 자식 클래스가 부모 클래스에 구현된 메서드의 복사본을 갖고 있는 것 처럼 보임
    • class 포인터와 parent 포인터를 조합하면, 최상위 부모 클래스에 이르기까지 모든 부모 클래스에 접근 가능.

⚠️ 주의사항

  • 그림들은 상속을 이해하기 쉽도록 표현한 개념적인 그림이다.
    • 구체적인 구현 방법과 메모리 구조는 언어와 플렛폼에 따라 다르다.

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

같은 메시지, 다른 메서드

💻 예시 코드

  • 📋 요구사항 추가
    • 교수별 강의에 대한 성적 통계 계산 기능

Professor.java

  • 통계 계산의 책임
  • 통계 정보를 생성하기 위해 Lectureevaluageaverage를 호출한다.
public class Professor {
	private String name;
    private Lecture lecture;
    
    public Professor(String name, Lecture lecture) { // ** 생성자에서는 Lecture를 받는다.
    	this.name = name;
        this.lecture = lecture;
    }
    
    public String compileStatistics() {
    	return String.format("[%s] %s - Avg: %.1f", name, 
        	lecture.evaluate, lecture.average());
	}
    
}    

클라이언트 코드

다익스트라 교수가 강의하는 알고리즘 과목의 성적 통계 계산 코드

  • Lecture를 이용하는 경우
    • 결과물: [다익스트라] Pass:3 Fail:2 - Avg: 69.2
Professor professor = new Professor("다익스트라",
							new Lecture("알고리즘",
                            	70,
                                Arrays.asList(81, 95, 75, 50, 45)));
// 결과 => "[다익스트라] Pass:3 Fail:2 - Avg: 69.2"
String statistics = professor.compileStatistics();
  • GradeLecture를 이용하는 경우
    • 결과물
Professor professor = new Professor("다익스트라",
							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)));

// 결과 => "[다익스트라] Pass:3 Fail:2, A:1 B:1 C:1 D:1 F:1 - Avg: 69.2"
String statistics = professor.compileStatistics();

생성자는 Lecture로 선언되어 있지만, GradeLecture를 전달해도 문제없이 실행된다.
위 예제는 lecture에 대해 동일한 evaluate 메시지를 전송했을 때, 동일한 코드안에서 서로 다른 클래스 안에 구현된 메서드를 실행할 수 있다는 사실을 보여준다.

🔎 분석

  • 어떻게 참조타입과 무관하게 실제 메시지 수신하는 객체의 타입에 따라 실행되는 메서드가 달라질 수 있는걸까?
    • 업캐스팅동적 바인딩이라는 메커니즘 덕분이다.
  • 업캐스팅
    • 부모 클래스(Lecture) 타입으로 선언된 변수에 자식 클래스(GradeLecture)의 인스턴스를 할당하는 것.
      • 서로 다른 클래스의 인스턴스를 동일한 타입에 할당하는 것을 가능하게 해준다.
  • 동적 바인딩
    • 선언된 변수의 타입이 아니라, 수신하는 객체의 타입에 따라 실행되는 메서드가 결정되는 것.
    • 객체지향 시스템이 메시지를 처리할 적절한 메서드를 컴파일 시점이 아니라 실행 시점에 결정하기 때문에 가능하다.
      • 부모 클래스 타입에 메시지를 전송하더라도, 실행 시 실제 클래스 기반으로 실행될 메서드가 선택되게 해준다.

업캐스팅, 동적 바인딩 덕분에 코드를 변경하지 않고서도 메시지에 실행되는 실제 메서드를 변경할 수 있다.

개방-폐쇄 & 의존성 역전

  • 최종적 목표: 코드를 변경하지 않는 기능 추가
    • 개방-폐쇄 원칙: 확장 가능한 코드를 만들기 위해 의존관계를 구조화하는 방법을 설명
    • 업케스팅, 동적 메서드 탐색: (상속을 이용한 개방-폐쇄 원칙을 따르는 코드 작성 시) 하부에서 동작하는 기술적 내부 메커니즘을 설명
      • 개방-폐쇄 원칙목적이라면, 업캐스팅, 동적 메서드 탐색목적에 이르는 방법이다.
  • 현재 Professor는 구체 클래스인 Lecture에 의존하므로, 의존성 역전 원칙을 따르는 상황은 아니다.
    • 개방-폐쇄 원칙의 중심에는 추상화가 있기 때문이다.

업캐스팅

상속을 이용하면...

  • 부모 퍼블릭 인터페이스 + 자식 클래스의 퍼블릭 인터페이스 (얘가 추가됨)
    • 부모에게 전송할 수 있는 메시지를 자식 인스턴스에게도 전송 가능하다.
      • 부모 대신 자식을 이용하더라도 메시지 처리에는 문제가 없다.
      • 컴파일러는 명시적 타입변환 없이도 자식 클래스가 부모 클래스를 대처할 수 있도록 허용한다.

모든 객체지향 언어는 명시적으로 타입변환을 하지 않아도 부모 클래스 타입의 참조변수에 자식 클래스의 인스턴스를 대입 가능하도록 허용한다.

  • 활용 예 1. 대입문
Lecture lecture = new GradeLecture(...);
  • 활용 예2. 메서드의 파라미터 타입
public class Professor {
	public Professor(String name, Lecture lecture) { ... }
}

// 파라미터 중 Lecture 자리에 GradeLecture 전송
Professor professor = new Professor("다익스트라", new GradeLecture(...)); 

다운 캐스팅

  • 다운 캐스팅
    • 명시적인 타입 변환을 이용해 부모 클래스의 인스턴스를 자식 클래스 타입으로 변환시키는 것.
Lecture lecture = new GradeLecture(...);
GradeLecture GradeLecture = (GradeLecture)lecture;
  • 자식 클래스는 아무런 제약 없이 부모 클래스를 대체할 수 있다.
    • 따라서, 부모 클래스와 협력하는 클라이언트다양한 자식 클래스의 인스턴스와도 협력이 가능하다.
    • 여기서 자식 클래스: 현재 상속 계층에 존재하는 자식 클래스 +) 앞으로 추가될 미래의 자식 클래스들
  • 예) Lecture모든 자식 클래스는 evaluate 메시지를 이해할 수 있으므로(이 전제조건이 존재할 때),
    • ProfessorLecture를 상속받는 어떤 자식 클래스와도 협력할 수 있는 무한한 확장 가능성(이 이점이 존재한다)을 가진다.

동적 바인딩

  • 전통적인 언어에서의 함수 실행: 함수를 호출하는 것
  • 객체지향 언어에서의 메서드 실행: 메시지를 전송하는 것
    • 둘의 매우 큰 차이점
      • (함수 호출 구문)과 (실제 실행 코드) 간 연결의 언어적 메커니즘이 완전히 다르다.
  • 정적 바인딩(static binding): 전통적인 언어의 바인딩 방법
    • = 초기 바인딩(early binidng), 컴파일타임 바인딩(compile-time binding)
    • 호출될 함수를 컴파일 타임, 즉 코드 작성 시점에 결정하는 것
      • 코드 상에서 bar함수를 호출하는 구문이 나타나면, 실제 실행되는 코드는 바로 그 bar함수다. 이외의 어떤 코드도 아니다.
  • 동적 바인딩(dynamic binding): 객체지향 언어의 바인딩 방법
    • = 지연 바인딩(late binding)
    • 메시지 수신해 실제 실행할 메서드가 런타임에 결정된다.
      • foo.bar()라는 코드에서 실제 어떤 클래스의 어떤 메서드가 실행될지 판단하기 어렵다.
      • 1. foo가 가르키는 객체가 가르키는 인스턴스인지, 2. bar메서드가 해당 클래스의 상속 계층의 어디에 위치하는지를 알아야 한다.


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

객체지향 언어는 어떤 규칙에 따라 메서드 전송메서드 호출을 바인딩하는 걸까?

객체지향의 실행 메서드 선택 규칙

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

메서드 탐색; by. self 참조

  • self 참조(self reference)
    • 객체가 메시지를 수신하면 컴파일러가 self 참조라는 임시 변수를 자동으로 생성한다.
    • 이후 이 변수가 메시지를 수신한 객체를 가리키도록 설정한다.
  • 동적 메서드 탐색
    1. self가 가리키는 객체의 클래스에서 시작
    2. 상속 계층의 역방향으로 이뤄짐
    3. 메서드 탐색이 종료되는 순간, self 참조는 자동 소멸
  • 시스템은 class 포인터, parent 포인터, self 참조를 조합하여 메서드를 탐색한다.

📌 결론

메서드 탐색은 자식클래스에서 부모 클래스의 방향 (자식 -> 부모)으로 진행된다.
항상 자식 클래스의 메서드가 먼저 탐색되므로, 자식 클래스에 선언된 메서드가 부모보다 더 높은 우선순위를 가지게 된다.

self 와 this

  • 정적 타입 언어(C++, 자바, C#)
    • self 참조를 this라고 부른다.
  • 동적 타입 언어(스몰토크, 루비)
    • self 참조르 나타내는 키워드로 self를 사용한다.

💻 메서드 탐색; GradeLecture 예시

  • 메시지 수신 시점의 GradeLecture 인스턴스의 메모리 상태
    • self 참조에서 상속 계층을 따라 이뤄지는 동적 메서드 탐색을 표현
  1. self 참조가 가리키는 메모리
    • 객체의 현재 상태를 표현하는 데이터 + 객체의 클래스를 가리키는 class 포인터
  2. class 포인터 따라 이동
    • 메모리에 로드된 GradeLecture 클래스의 정보를 읽을 수 있다.
      • 클래스의 정보: 클래스안에 구현된 전체 메서드의 목록을 포함.
    • 메서드 목록 안에 메시지를 처리할 적절한 메서드가 존재하면, 해당 메서드 실행 후 동적 메서드 탐색을 종료한다.
  3. (적절한 메서드를 찾지 못한 경우) parent 포인터를 따라 부모 클래스로 이동
    • 최상위 클래스인 OBject에 이르러서도 적절한 메서드를 찾지 못한 경우, 에러를 발생시키고 메서드를 종료한다.

동적 메서드 탐색

  1. 첫 번째 원리: 자동적인 메서드 위임
    • 자식 클래스는 자신이 이해할 수 없는 메시지를 전송받은 경우, 상속 계층을 따라 부모 클래스에게 처리를 위임한다.
    • 클래스 사이의 위임은 프로그래머의 개입 없이 상속계층에 따라 자동으로 이뤄진다.
  2. 두 번째 원리: 메서드 탐색을 위해 동적인 문맥을 사용
    • 메시지 수신 시 실제로 어떤 메서드를 실행할지 결정하는 것은 실행 시점에 이뤄진다.
    • 메서드를 탐색하는 경로는 self 참조를 이용해서 결정한다.

자동적인 메서드 위임

  • 동적 메서드 탐색에게 상속 계층
    • 메시지를 수신한 객체가 자신이 이해할 수 없는 메시지를 부모 클래스에게 전달하기 위한 물리적 경로를 정의하는 것.
      • 즉, 위임 경로를 만드는 것이다.
  • 상속 사용 시, 프로그래머는 메시지 위임과 관련된 코드를 직접 작성할 필요없다.
    • 메시지는 상속 계층을 따라 부모 클래스에게 자동으로 위임된다.
    • 즉, 상속 계층의 정의 = 메서드 탐색 경로의 정의
  • 이 위임의 방법은 언어마다 상속이 아닌 다른 방법으로도 구현될 수 있다.
    • 루비: 모듈(module)
    • 스몰토크, 스칼라: 트레이트(trait)
    • 스위프트: 프로토콜(protocol), 확장(extension)
  • 위임 = 자식 -> 부모 방향
    • 자식은 항상 우선권을 가지고, 부모 메서드의 운명을 결정한다.
    • 메서드 오버라이딩
      • 자식 클래스의 메서드가 동일한 시그니처를 가진 부모 메서드보다 먼저 탐색되어 벌어지는 현상
  • 오버라이딩 vs 오버로딩
    • 메서드 오버라이딩(Overriding): 부모의 메서드와 공존하지 않고 감춤
    • 메서드 오버로딩(Overloading): 시그니처가 완전히 동일하지 않아 상속계층 전반에서 공존

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

GradeLecture의 evaluate

Lecture lecture = new GradeLecture(...);
lecture.evaluate();
  • 자식 클래스가 부모 클래스의 메서드와 시그니처가 동일한 메서드를 재정의한다. (오버라이딩 한다.)
    • 자식 클래스가 우선권을 가지므로, 결과적으로 부모 클래스의 메서드를 감추게 된다.

메서드 오버로딩 (Method Overloading)

GradeLecture의 average

Lecture lecture = new GradeLecture(...);
lecture.average();
  • 자식 클래스에서 적절한 메서드를 찾지 못했으므로, 부모 클래스인 Lecture에서 메서드를 조회한다.
    • average()average*(String gradeName)은 메서드 이름은 동일하나 메서드 시그니처가 다르다.
  • 메서드 오버로딩: 시그니처가 다르기 때문에 동일한 이름의 메서드가 공존하는 경우
    • 클라이언트 관점에서 오버로딩된 모든 메서드를 호출할 수 있다.
    • 상속 계층 안에서 같은 이름을 가진 메서드를 정의하는 것 역시 오버로딩에 포함된다.

메서드 오버로딩 비제공; C++

일부 언어에서는 메서드 오버로딩을 제공하지 않는다.

Lecture

class Lecture
{
public:
	virtual int average();
    virtual int average(std::string grade);
    virtual int average(std::string grade, int base);

};

GradeLecture

class GradeLecture: public Lecture
{
public:
	virtual int average(char grade);
};    

클라이언트에서 부모 클래스의 메서드 호출

부모 클래스에서 선언된 메서드를 호출하면 에러가 발생한다.

GradeLecture *lecture = new GradeLecture();
lecture->average('A');
lecture->average(); 		// 에러!
lecture->average("A");		// 에러!
lecture->average("A", 70);	// 에러!
  • 이름 숨기기 (name hiding)
    • C++에서 상속 계층 안에서 동일한 이름을 가진 메서드가 공존해 발생하는 혼론을 방지하기 위해, 부모 클래스에 선언된 이름이 동일한 메서드 전체를 숨겨 클라이언트가 호출하지 못하도록 막는 것.
    • 이를 해결하기 위해서는, 부모 클래스에 정의된 모든 메서드를 자식 클래스에서 오버로딩하면 된다.
class GradeLecture: public Lecture
{
public:	
	virtual int average();
    virtual int average(str::string grade); // 부모의 메서드
    virtual int average(int base); // 부모의 메서드
    virtual int average(char grade); // 부모의 메서드
};

또는 부모 클래스에 정의된 average라는 이름을 자식 클래스의 네임스페이스에 합칠 수도 있다.

class GradeLecture: public Lecture
{
public:
	using Lecture::average;
    virtual int average(char grade);
};

⚠️ 주의사항

동적 메서드 탐색과 관련된 규칙이 언어마다 다를 수 있다.
사용하는 언어의 문법과 메서드 탐색 규칙을 주의 깊게 살펴보자!

동적인 문맥

  • 메시지 전송 코드만으로는 어떤 클래스의 어떤 메서드가 실행될지 알 수 없다.
    • 메시지를 수신한 객체가 무엇이냐에 따라 메서드 탐색을 위한 문맥이 동적으로 바뀐다.
    • 이 동적인 문맥은, (메시지를 수신할 객체를 가리키는) self 참조가 결정한다.
  • self 참조 = Lecture 인스턴스
    • 메서드 탐색 문맥 = Lecture(시작) ~ Object(종료) 상속계층
  • self 참조 = GradeLecture 인스턴스
    • 메서드 탐색 문맥 = GradeLecture(시작) ~ Object(종료) 상속계층
      • 동일한 코드이더라도, self가 가리키는 객체에 따라 상속 계층의 범위가 동적으로 변한다.
  • self 참조가 동적 문맥을 결정한다는 사실은 종종 어떤 메서드가 실행될지를 예상하기 어렵게 만든다.
    • [대표적인 예시] self 전송(self send): 자신에게 메시지 전송

💻 self 참조 예시코드 (상속 X)

Lecture.java

  1. Lecture에 평가 기준에 대한 정보를 반환하는 stats 메서드를 추가
  2. Lecturestats는 자신의 getEvaluationMethod()를 호출한다.
public class Lecture {
	public String stats() {
    	return String.format("Title: %s, Evaluation Method: %s", title, getEvalulationMehotd());
	}
    
    public String getEvaluationMethod() {
    	return "Pass or Fail";
    }

}
  • getEvaluationMethod()
    • 현재 클래스의 메서드를 호출하는 것
    • 현재 객체에게 getEvaluationMethod 메시지를 전송하는 것 ⭕️

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

  • 현재 객체
    • = self 참조가 가리키는 객체
    • = 처음에 stats 메시지를 수신한 그 긱채
  • self 전송
    • = self 참조가 가리키는 자기 자신에게 메시지를 전송하는 것을 말한다.

stats 메서드 탐색 과정

  • stats 실행 전
    1. Lecture의 인스턴스가 stats 메시지 수신 시, self 참조는 메시지를 수신한 Lecture 인스턴스를 가리키도록 자동으로 할당된다.
    2. 시스템은 객체의 클래스인 Lecture에서 stats를 발견하고 이를 실행시킨다.
  • stats 실행 후
    1. stats 실행 중 getEvaluationMethod 호출 구문을 발견한다.
    2. 시스템은 (self 참조가 가리키는) 현재 객체에게 메시지를 전송해야 한다고 판단한다.
      • status를 수신한 동일한 객체에게 getEvaluationMethod를 전송한다.
    3. self 참조가 가리키는 Lecture부터 다시 탐색을 시작하고, Lecture내부의 getEvaluationMethod 실행 후 탐색을 종료한다.

  • 📢 중요!) getEvaluationMethod()의 의미
    • Lecture의 getEvaluatinoMethod 메서드를 실행시켜라
    • self가 참조하는 현재 객체에 getEvaluatinoMethod 메시지를 보내라 ⭕️

💻 self 참조 예시코드 (상속 O)

GradeLecture.java

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

stats 메서드 탐색 과정

  • stats 실행 전
    1. GradeLecture 인스턴스가 stats 메시지 수신 시, self 참조는 GradeLecture를 가리키도록 설정된다.
    2. GradeLecture에는 stats를 처리할 수 있는 코드가 없으므로, 부모 클래스인 Lecture에서 메서드를 탐색하고, Lecturestats가 실행된다.
  • stats 실행 후
    1. stats 실행 중 getEvaluationMethod() 호출 구문을 발견한다.
    2. 메서드 탐색은 self가 참조하는 객체에서 시작되는데, 현재 GradeLecture로 설정되어 있다.
    3. GradeLecturegetEvaluationMethod가 실행되고 메서드 탐색이 종료된다.
  • 결과
    • Lecture 클래스의 statsGradeLecturegetEvaluationMethod의 실행 결과가 조합된 문자열이 반환된다.

📌 결론

  • self 전송
    • 자식 -> 부모로 진행되는 동적 메서드 탐색 경로를 다시 self 참조가 가리키는 원래 호출 지점으로 복귀시킨다.
  • 최악의 경우, 실제 실행될 메서드를 파악하기 위해 전체 상속 구조를 훑어가며 코드를 이해해야 할 수도 있다.
  • self 전송 + 깊은 상속 계층 + 메서드 오버라이딩 => 극단적으로 난해한 코드

이해할 수 없는 메시지

상속 계층의 정상에서 자신이 메시지를 처리할 수 없다는 사실을 알게 되는 경우, 어떻게 대처해야 할까?
즉, 객체가 메시지를 이해할 수 없다면 어떻게 할까?

이해할 수 없는 메시지를 다루는 방식은, 언어가 정적 타입 언어인지 동적 타입 언어인지에 따라 다르다.

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

  • 코드를 컴파일 할 때, 상속 계층 안의 클래스들이 메시지를 이해할 수 있는지 확인한다.
    • 상속 계층 전체에서 메시지를 처리할 수 없는 경우, 컴파일 에러를 발생시킨다.

정적 타입 언어인 자바의 경우,

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

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

  • 동적 타입 언어는 컴파일 단계가 존재하지 않으므로, 실제 코드를 실행해보기 전에는 메시지 처리 가능 여부를 판단할 수 없다.
  • 최상위 클래스까지 메서드를 처리할 수 없는 경우, self 참조가 가리키는 현재 객체에게 메시지를 이해할 수 없다는 메시지를 전송한다.
    • 이 메시지 역시 보통 메시지처럼 self 참조 객체부터 최상위 객체까지 위임된다.
    • 이후 최종적으로 예외가 던져진다.
  • 이해할 수 없다는 메시지를 다시 전송하므로 동적 타입언어는 추가적인 선택권을 가진다.
    • 이해할 수 없다라는 메시지에 응답하는 메서드를 구현하는 것이다.
    • 이 경우 객체는 자신의 인터페이스에 정의되지 않은 메시지를 처리하는 것이 가능해진다.
  • 이해할 수 없는 메시지를 처리할 수 있다는 점에서, 동적 타입 언어는 더 순수한 객체지향 패러다임을 구현한다고 볼 수 있다.
    • 협력을 위해 메시지를 전송하는 객체는, (수신 객체의 내부 구현을 모른 채) 메시지를 처리할 수 있다 믿고 메시지를 전송한다.
    • 클라이언트는 1. 객체가 메서드를 구현하고 있건, 2. 구현할 수 없어 오류 처리를 했건, 단지 전송한 메시지가 성공적으로 처리됐다는 사실만 알 수 있을 뿐이다.
  • 동적 타입 언어: 이해할 수 없는 메시지를 처리할 수 있는 능력을 가짐
    • 메시지가 선언된 인터페이스와 메서드가 정의된 구현을 분리 가능
      • 메시지 기반 협력의 자율적인 객체라는 순수 객체지향의 이상에 더 가깝다.
  • 단점
    • 이같은 동적 타입 언어의 도적인 특성 & 유연성
      • 코드 이해의 어려움
      • 디버깅 과정의 복잡성
    • cf. 반대로, 정적 타입 언어는 유연성은 부족하지만 더 안정적이다.

이해할 수 없는 메시지와 도메인-특화 언어

  • 동적 타입 언어의 특징 = 이해할 수 없는 메시지 처리 가능
    • 메타 프로그래밍 영역에서 진가를 발휘함.
    • 이 특성으로인해, 도메인-특화 언어(Domain-Specific Language, DSL) 개발에 더 쉽고 강력하다.

self 대 super

  • self 참조
    • 메시지를 수신한 객체의 클래스에 따라 메서드 탐색을 위한 문맥을 실행 시점에 결정한다.
      • 즉, 동적이다. (가장 큰 특징)
    • super 참조(super reference)는 이와 대조적인 특징을 가진다.
  • super 참조(super reference)
    • 자식 클래스에서 부모 클래스의 구현을 재사용해야 하는 경우, 부모 클래스의 인스턴스 변수나 메서드에 접근하기 위해 사용
    • super 참조를 이용한 전송은 부모 클래스에서부터 메서드 탐색을 시작하게 한다.

super 참조는 메시지를 전송한다.

  • super 참조
    • 부모 메서드 호출
    • 부모에게 메시지를 전송 ⭕️
      • 실제 호출되는 메서드는 부모 클래스의 메서드가 아니라 더 상위에 위치한 조상 클래스의 메서드일 수도 있다.

GradeLecture.java

  • super 참조로 인해 부모 클래스에게 evaluate 메시지가 전송된다. (스스로의 것이 아닌)
public class GradeLecture extends Lecture {
	@Override
    public String evaluate() {
    	return super.evaluate() + ", " + gradeStatistics();
    }
}

FormattedGradeLecture.java

  • super참조로 인해 부모 클래스인 GradeLecture가 아니라, 그의 부모인 Lectureaverage()가 호출된다.
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());
    }

}

super 참조 핵심 특징

  • super 참조의 정확한 의도
    • 지금 이 클래스의 부모 클래스부터 메서드 탐색을 시작하세요
      • 부모 클래스에 원하는 메서드가 없으면 더 상위의 부모 클래스에서 탐색.
      • 이는 super를 통해 실행되어야 하는 메서드가 반드시 부모 클래스에 위치하지 않아도 되는 유연성을 제공한다.
  • super 전송(super send)
    • super 참조를 통한 메시지 전송은 부모 클래스의 인스턴스에게 메시지를 전송하는 것으로 보여, super 전송이라 부른다.

실제 동작 메커니즘

  • self 전송
    • 메시지 수신 객체의 실제 클래스에 따라 동적으로 메서드 탐색 위치 결정
  • super 전송
    • 항상 메시지 전송 클래스의 부모 클래스에서 메서드 탐색 시작
    • 고정된, 정적인 메서드 탐색 경로

05. 상속 대 위임

  • self 참조동적인 문맥을 결정한다.
    • 위의 사실은 상속을 바라보는 또 다른 매커니즘을 제공
      • 상속 = 자식 클래스에서 부모 클래스로 self 참조를 전달하는 것

위임과 self 참조

  • GradeLecture 인스턴스 입장에서 self참조 = GradeLecture 인스턴스 자신
  • Lecture 인스턴스 입장에서 self 참조 = GradeLecture 인스턴스
    • self 참조는 항상 메시지를 수신한 객체를 가리키기 때문
  • 메서드 탐색 중 자식 클래스의 인스턴스와 부모 클래스의 인스턴스가 동일한 self 참조를 공유한다.
  • 포함 관계로 표현된 인스턴스: 인스턴스 사이의 링크를 가진 연결관계로 표현할 수 있다.
  • 상속 계층을 공유하는 객체들: self 참조를 공유
    • self라는 변수를 포함하는 것 처럼 표현할 수 잇다.

💻 self 참조 예시 코드

GradeLecture에서 Lecture로 self 참조가 공유되는 과정을 루비를 통해 알아보자

Lecture.ruby

  • stats의 인자로 this를 전달받고 있다.
    • this에는 수신할 객체를 가리키는 self 참조가 보관된다.
  • statsthis에게 getEvaluationMethod 메서드를 전송한다.
    • 실제로 실행되는 메서드가 LecturegetEvaluationMethod가 아닐 수 있음을 명시적으로 드러낸다.
    • this에 전달되는 인스턴스가 Lecture가 아니라 다른 인스턴스가 전달된다면, 해당 객체의 메서드가 실행될 수 있다.
class Lecture
	def initialize(name, scores)
		@name = name
        @scores = scores
    end
    
    def stats(this) # self 참조 보관
    	"Name: #{@name}, Evaluation Method: #{this.getEvaluationMethod()}"
    end
    
    def getEvaluationMethod() 
    	"Pass or Fail"
    end
end
  • 클라이언트 코드
    • stats로 전달된 LecturegetEvaluationMethod가 실행되게 된다.
lecture = Lecture.new("OOP", [1,2,3])
puts lecture.stats(lecture)

GradeLecture.ruby

전통적인 상속관계를 나타내는 구문을 사용하지 않고, 자식 클래스의 인스턴스가 부모 클래스의 인스턴스에 대한 링크를 포함하는 것으로 상속관계를 흉내냈다.
인스턴스 변수인 @parent에 부모 클래스인 Lecture의 인스턴스를 할당한다.

class GradeLecture
	def initialize(name, canceled, scores)
    	@parent = Lecture.new(name, scores) # 1. 받은 this를  저장 => 링크 역할
        @canceled = canceled
    end
    
    def stats(this)
    	@parent.stats(this) # 2. @parent에게 요청 전달
    end
    
    def getEvaluationMethod() # 3. 자신만의 메서드 구현 (메서드 오버라이딩)
    	"Grade"
    end 
end
  • 클라이언트 코드
grade_lecture = GradeLecture.new("OOP", false, [1,2,3])
puts grade_lecture.stats(grade_lecture)
  • 핵심요소
    1. GraeLectrue@parentLecture의 인스턴스를 생성해서 저장한다.
      • GradeLecutre에서 Lecutre 인스턴스로 이동할 수 있는 명시적인 링크가 추가된다.
      • 링크: 컴파일러가 제공해주던 동적 메서드 탐색 메커니즘 직접 구현
    2. GradeLecturestats는 추가 작업 없이 @parent에게 요청을 그대로 전달
      • 자식 클래스에 메서드가 존재하지 않는 경우, 부모 클래스에서 메서드를 탐색하는 동적 메서드 탐색 과정을 흉내
      • 부모 클래스와 동일한 메시지를 수신하기 위해, 부모 클래스의 퍼블릭 메서드를 그대로 선언하고 요청을 전달
      • 실행 문맥을 자식 -> 부모로 전달하는 상속 관계를 흉내내기 위해, 인자로 받은 this를 그대로 전달한다.
    3. GradeLecturegetEvaluationMethod는 요청을 @parent에게 전달하지 않고, 자신만의 방법으로 메서드를 구현한다.
      • 상속에서의 메서드 오버라이딩을 의미한다.
    4. GradeLecturestats는 메시지를 직접 처리하지 않고 Lecturestats에게 요청을 전달
      • Lecturestatsthis에게 getEvaluationMethod 수행을 위임하므로, GradeLecturegetEvaluationMethod가 실행된다.
  • 위임(delegation)
    • 자신이 수신한 메시지를 다른 객체에게 동일하게 전달해서 처리를 요청하는 것
    • 자신이 정의하지 않거나 처리할 수 없는 속성 또는 메서드의 탐색 과정을 다른 객체에게 이동시키기 위해 사용한다.
    • 항상 현재의 실행 문맥을 가리키는 self 참조를 인자로 전달한다.
      • self 참조를 전달하지 않는 포워딩과의 차이점
  • 위임: 객체 사이의 동적인 연결 관계를 이용해 상속을 구현하는 방법
    • 상속은 우리가 직접 구현해야 하는 번잡한 과정을 자동으로 처리해준다.
      • GradeLectureLecture의 자식 클래스로 선언하는 것 만으로 이 과정들을 설정해준다.
      • 즉, 상속으로 설정 시 self 참조가 자동으로 전달된다.
  • self 참조의 전달은 결과적으로 자식과 부모 인스턴스 사이에 동일한 실행문맥을 공유하도록 한다.

포워딩과 위임

  • 포워딩
    • 처리를 요청할 때 self 참조를 전달하지 않는 경우
    • 요청을 전달받은 최초의 객체에 다시 메시지를 전송할 필요는 없고 단순히 코드를 재사용하고 싶은 경우
  • 위임
    • self 참조를 전달하는 경우
    • 위임의 용도
      • 클래스를 이용한 상속 관계를 객체 사이의 합성 관계로 대체해 다형성을 구현하는 것

📌 결론

  • 상속 관계로 연결된 클래스 사이에는 자동적인 메서드 위임이 발생한다.
    • 상속은 동적인 메서드 탐색을 위해 현재의 실행 문맥을 가지고 있는 self 참조를 전달한다.
    • 객체들 간의 메시지 전달 과정은 자동으로 일어난다.

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

클래스가 아닌 객체를 이용해서도 상속을 흉내낼 수있다.
클래스 없이 객체만 존재하는 프로토타입 기반의 객체지향 언어에서 상속을 구현하는 방법은, 객체사이의 위임을 이용하는 것이다.

프로토타입 기반의 객체지향 언어인 자바스크립트를 통해 객체 사이의 상속이 어떻게 이루어지는지 알아보자!

자바스크립트의 프로토타입

  • prototype
    • 자바스크립트의 모든 객체가 다른 객체를 가리키는 용도로 사용하는 링크
    • 앞서 부모 객체를 가리키기 위해 사용했던 인스턴스 변수 @parent와 동일
    • 언어 차원에서 제공되므로 직접 구현할 필요가 없다.
  • 자바스크립트의 메시지 수신
    1. 자바스크립트의 인스턴스가 메시지를 수신하면, 먼저 수신한 객체의 prototype 안에서 메시지에 응답할 적절한 메서드가 존재하는지 검사한다.
    2. 메서드가 존재하지 않으면 prototype이 가리키는 객체를 따라 메시지를 자동으로 위임한다.
      • 이 위임 과정을 prototype 체인이라고 부른다.
      • 자바스크립트는 prototype 체인으로 연결된 객체 사이에 메시지를 위임하는 것으로 상속을 구현한다.

💻 예시코드

  • 생성자 함수에 대해 new 연산자를 호출해 객체를 생성한다.
  • 메서드를 Lectureprototype이 참조하는 객체에 정의했다.
    • Lecture를 이용해 생성한 모든 객체들은 prototype 객체에 정의된 메서드를 상속받는다.
  • 특별한 작업이 없다면, prototype에 할당되는 객체는 자바스크립트의 최상위 객체 타입인 Object다.
    • Lecture를 이용해 생성되는 모든 객체들은 prototype이 참조하는 Object에 정의된 모든 속성과 메서드를 상속받는다.

Lecture.javascript

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

Lecture.prototype.stats = function() {
  return "Name: " + this.name + ", Evaluation Method: " + this.getEvalutationMethod();
}

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

GradeLecture.javascript

  • GradeLecture의 prototype 에 Lecture의 인스턴스를 할당했다.
    • GradeLecture를 이용해 생성된 모든 객체들이 prototype을 통해 Lecture에 정의된 모든 속성과 함수에 접근할 수 있다.
      • 즉, GradeLecture모든 인스턴스는 Lecture의 특성을 자동으로 상속받게 된다.
    • 이제 메시지를 전송하면 prototype으로 연결된 객체 사이의 경로를 통해 객체 사이의 메서드 탐색이 자동으로 이뤄진다.
function GradeLecture(name, canceled, scores) {
  Lecture.call(this, name, scores);
  this.canceled = canceled;
}

// GradeLecture에 Lecture 인스턴스를 할당했다.
GradeLecture.prototype = new Lecture();

GradeLecture.prototype.constructor = GradeLecture;

GradeLecture.prototype.getEvaluationMethod = function() {
  return "Grade"
}

클라이언트 코드

var grade_lecture = new GradeLecture("OOP", false, [1,2,3]);
grade_lecture.stats();

  1. 메시지를 수신한 인스턴스는 GradeLecturestats 메서드가 존재하는지 검사한다.
  2. GradeLecturestats가 존재하지 않으므로 prototype을 따라 Lecture의 인스턴스에 접근한 후 stats 메서드가 존재하는지 살펴본다.
    • 메서드를 발견해 Lecturestats가 실행된다.
  3. 메서드 실행 도중 this.getEvaluationMethod() 문장을 발견한다.
    • 상속과 마찬가지로 self 참조가 가리키는 현재 객체에서부터 다시 메서드 탐색을 시작한다.
  4. 현재 객체의 prototype이 참조하는 GradeLecture의 인스턴스에서 getEvaluationMethod 메서드를 발견하고 이 메서드를 실행해 동적 메서드 탐색이 종료된다.

💡 요점

  • 프로토타입으로 구현하는 상속
    • 동적인 객체 사이의 위임을 통해 메서드 탐색 과정을 표현하고, 이를 통해 상속을 구현하고 있다.
      • 자바스크립트는 prototype으로 연결된 객체들의 체인을 거슬러 올라가며 자동으로 메시지에 대한 위임을 처리한다.
    • 객체 사이에 self 참조가 전달된다.
      • LecturestatsthisLecture인스턴스가 아니다. 메시지를 수신한 현재 객체를 가리킨다.
  • 프로토타입으로도 상속을 구현할 수 있다. 객체지향에서 클래스는 필수요소가 아니다.
    • 자바스크립트는 클래스가 존재하지 않아 오직 객체들 사이의 메시지 위임만을 이용하여 다형성을 구현한다.

📌 결론

객체지향은 객체를 지향하는 것이다.
클래스는 객체를 편리하게 정의하고 생성하기 위해 제공되는 프로그래밍 구성 요소일 뿐이며, 클래스 없이도 객체 사이의 협력 관계를 구축하는 것이 가능하며 상속 없이도 다형성을 구현하는 것이 가능하다.

객체지향은 다양한 방법으로 도달할 수 있는 하나의 지향점이다.
객체지향을 구현하는 클래스라는 개념에 매몰되지 말고, 메시지와의 협력에 보다 초점을 맞춰야 한다.


Reference

  • 오브젝트 | 조영호
profile
Good Luck!

0개의 댓글