devseop08 님의 블로그
[오브젝트] 다형성 본문
- 상속의 목적은 코드 재사용이 아닌, 타입 계층을 구조화하기 위함이다.
- 타입 계층은 다형성의 기반을 제공한다.
- 상속을 단순히 코드를 재사용하기 위한 목적으로 사용하는 것인지, 클라이언트 관점에서 인스턴스들을 동일하게 행동하는 그룹으로 묶기 위한 목적으로 사용하는 것인지 분명하게 구별해야 한다.
- 다형성이 런타임에 메시지를 처리하기에 적절한 메서드를 동적으로 탐색하는 과정을 통해 구현된다는 사실과
- 상속이 이러한 메서드를 찾기 위한 일종의 탐색 경로를 클래스 계층의 형태로 구현하는 방법임을 이해해야 한다.
1. 다형성
다형성의 분류

- 오버로딩 다형성: 하나의 클래스 안에 동일한 이름의 메서드가 존재하는 경우
- 강제 다형성: 동일한 연산자를 다양한 타입에 사용할 수 있는 방식이다.
- ex) 자바의 이항 연산자
+
- ex) 자바의 이항 연산자
- 매개변수 다형성: 제네릭 프로그래밍과 관련이 높은데 클래스의 인스턴스 변수나 메서드의 매개변수 타입을 임의의 타입으로 선언한 후 사용하는 시점에 구체적인 타입으로 지정하는 방식
- ex) 자바의 List 인터페이스
- 포함 다형성: 서브타입 다형성이라고도 부르며, 메시지가 동일하더라도 수신한 객체의 타입에 따라 실제로 수행되는 행동이 달라지는 능력을 의미한다.
포함 다형성
- 객체지향 프로그래밍에서 특별한 언급 없이 다형성이라고 할 때는 일반적으로 포함 다형성을 의미한다.
- 포함 다형성을 구현하는 가장 일반적인 방법은 상속을 사용하는 것이다.
- 두 클래스를 상속 관계로 연결하고 자식 클래스에서 부모 클래스의 메서드를 오버라이딩한 후 클라이언트는 부모 클래스만 참조하면 포함 다형성을 구현할 수 있다.
- 상속의 진정한 목적은 코드 재사용이 아니라 서브타입 계층을 구축하는 것이다.
- 포함 다형성을 위해 상속을 사용하는 가장 큰 이유는 상속이 클래스들을 계층으로 쌓아 올린 후 상황에 따라 적절한 메서드를 선택할 수 있는 메커니즘을 제공하기 때문이다.
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::inValue)
.average().orElse(0);
}
public List<Integer> getScores() {
return Collections.unmodifiableList(scores);
}
public String evaluate() {
return String.forma("Pass: %d Fail: %d", passCount(), failCount());
}
private long passCount() {
return scores.stream().filter(score -> score>=pass).count();
}
private long failCount() {
return scores.size() - passCount();
}
}- 이수(pass) 기준이 70점인 과목의 수강생 5명에 대한 성적 통계를 구하는 코드
Lecture lecture = new Lecture("객체지향 프로그래밍",
70,
Arrays.asList(81,95, 75, 50, 45));
String evaluration = lecture.evaluate(); // "Pass: 3 Fail: 2"상속을 이용해 Lecture 클래스 재사용하기
- Lecture의 출력 결과에 등급별 통계를 추가하기 위해 Lecture 클래스를 상속받은 GradeLecture 클래스
- GradeLecture 클래스에는 Grade 인스턴스들을 리스트로 보관하는 인스턴스 변수 grades를 추가
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.greades = grades;
}
}public class Grade {
private String name;
private int upper, lower;
private Grade(String name, int upper, int lower) {
this.name = name;
this.uppper = 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 클래스에 학생들의 이수 여부와 등급별 통계를 함께 반환하도록 evaluate 메서드를 재정의
public class GradeLecture extends Lecture {
@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();
}
}메서드 오버라이딩
- GradeLecture와 Lecture에 구현된 두 evaluate 메서드으 시그니처가 동일
- 부모 클래스와 자식 클래스에 동일한 시그니처를 가진 메서드가 존재할 경우 자식 클래스의 메서드 우선순위가 더 높다. => 메시지를 수신했을 때 부모 클래스의 메서드가 아닌 자식 클래스의 메서드가 실행된다.
- 결과적으로 동일한 시그니처를 가진 자식 클래스의 메서드가 부모 클래스의 메서드를 가리게 된다.
- 메서드 오버라이딩은 이처럼 자식 클래스 안에 상속받은 메서드와 동일한 시그니처의 메서드를 재정의해서 부모 클래스의 구현을 새로운 구현으로 대체하는 것을 말한다.
메서드 오버로딩
- 자식 클래스에 부모 클래스에는 없던 새로운 메서드를 추가하는 것도 가능하다.
- 등급별 평균 성적을 구하는 average 메서드를 추가할 수 있다.
public class GradeLecture extends Lecture {
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);
}
}- evaluate 메서드와 달리 GradeLecture의 average 메서드는 부모 클래스인 Lecture에 정의된 average 메서드와 이름은 같지만 시그니처는 다르다.
- 따라서 GradeLecture의 average 메서드는 Lecture의 average 메서드를 대체하지 않으며, 결과적으로 두 메서드는 공존할 수 있다.
- 이처럼 부모 클래스에서 정의한 메서드와 이름은 동일하지만 시그니처는 다른 메서드를 자식 클래스에 추가하는 것을 메서드 오버로딩이라고 부른다.
데이터 관점의 상속
- Lecture의 인스턴스 생성
Lecture lecture = new Lecture("객체지향 프로그래밍",
70,
Arrays.asList(81,95,75,50,45));- Lecture의 인스턴스를 생성하면 시스템은 인스턴스 변수 title, pass, scores를 저장할 수 있는 메모리 공간을 할당
- 생성자의 매개변수를 이용해 값을 설정한 후 생성된 인스턴스의 주소를 lecture라는 이름의 변수에 대입한다.
- 메모리 상에 생성된 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));- 메모리 상에 생성된 GradeLecture 인스턴스

- 상속을 인스턴스 관점에서 바라볼 때는 개념적으로 자식 클래스의 인스턴스 안에 부모 클래스의 인스턴스가 포함되는 것으로 생각하는 것이 유용하다.
- 인스턴스를 참조하는 lecture는 GradeLecture의 인스턴스를 가리키기 때문에 특별한 방법을 사용하지 않으면 GradeLecture 안에 포함된 Lecture의 인스턴스에 직접 접근할 수 없다.
- 데이터 관점에서 상속은 자식 클래스의 인스턴스 안에 부모 클래스의 인스턴스를 포함하는 것으로 볼 수 있다. => 자식 클래스의 인스턴스는 자동으로 부모 클래스에서 정의한 모든 인스턴스 변수를 내부에 포함하게 되는 것이다.
행동 관점의 상속
- 행동 관점의 상속은 부모 클래스가 정의한 일부 메서드를 자식 클래스의 메서드로 포함시키는 것을 의미한다.
- 일반적으로 부모 클래스의 모든 퍼블릭 메서드는 자식 클래스의 퍼블릭 인터페이스에 포함된다.
- 따라서 외부의 객체가 부모 클래스의 인스턴스에게 전송할 수 있는 모든 메시지는 자식 클래스의 인스턴스에게도 전송할 수 있다.
- 부모 클래스의 퍼블릭 인터페이스가 자식 클래스의 퍼블릭 인터페이스 포함된다고 해서 실제로 클래스의 코드를 합치거나 복사하는 작업이 수행되지는 않는다.
- 부모 클래스에서 구현한 메서드를 자식 클래스의 인스턴스에서 수행할 수 있는 실제 이유는 런타임에 시스템이 자식 클래스에 정의되지 않은 메서드가 있을 경우 이 메서드를 부모 클래스 안에서 탐색하기 때문이다.
- 두 개의 Lecture 인스턴스를 생성한 후의 메모리 상태

- 객체의 경우에는 서로 다른 상태를 저장할 수 있도록 각 인스턴스 별로 독립적인 메모리를 할당받아야 한다.
- 하지만 메서드의 경우에는 동일한 클래스의 인스턴스끼리 공유가 가능하므로 클래스는 한 번만 메모리에 로드하고 각 인스턴스 별로 클래스를 가리키는 포인터를 갖게 하는 것이 효율적이다.
- 인스턴스는 두 개가 생성됐지만 클래스는 단 하나만 메모리에 로드된다.
- 각 객체는 자신의 클래스인 Lecture의 위치를 가리키는 class라는 이름의 포인터를 가지며 이 포인터를 이용해 자신의 클래스 정보에 접근할 수 있다.
- Lecture 클래스는 자신의 부모 클래스인 Object의 위치를 가리키는 parent라는 이름의 포인터를 가진다.
- GradeLecture 클래스의 인스턴스를 생성했을 때의 메모리 구조

- 메시지를 수신한 객체는 class 포인터로 연결된 자신의 클래스에서 적절한 메서드가 존재하는지를 찾는다.
- 만약 메서드가 존재하지 않으면 클래스의 parent 포인터를 따라 부모 클래스를 차례대로 훑어가면서 적절한 메서드가 존재하는지를 검색한다.
- GradeLecture 인스턴스의 class 포인터를 따라가면 GradeLecture 클래스에 이르고 GradeLecture 클래스의 parent 포인터를 따라가면 부모 클래스인 Lecture 클래스에 이르게 된다.
3. 업캐스팅과 동적 바인딩
같은 메시지, 다른 메서드
- 각 교수별로 강의에 대한 성적 통계를 계산하는 기능을 추가한다.
- 통계를 계산하는 책임은 Professor 클래스가 맡는다.
public class Professor {
private String name;
private 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());
}
}- 다익스트라 교수가 강의하는 알고리즘 과목의 성적 통계를 계산하는 코드
Professor professor = new Professor("다익스트라",
new Lecture("알고리즘",
70,
Arrays.asList(81, 95, 75, 50, 45)));
String statistics = professor.compileStatistics();- Lecture 클래스 대신 자식 클래스인 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)));
String statistics = professor.compileStatistics();- Professor 클래스의 생성자 인자 타입은 Lecture로 선언돼 있지만 GradeLecture의 인스턴스를 전달하더라도 아무 문제 없이 실행된다.
- 코드 안에서 선언된 참조 타입과 무관하게 실제로 메시지를 수신하는 객체의 타입에 따라 실행되는 메서드가 달라질 수 있는 것은 업캐스팅과 동적 바인딩이라는 메커니즘이 작용하기 때문이다.
- 업캐스팅은 서로 다른 클래스의 인스턴스를 동일한 타입에 할당하는 것을 가능하게 해준다. => 부모 클래스에 대해 작성한 코드를 전혀 수정하지 않고도 자식 클래스에 적용 가능
- 동적 바인딩은 부모 클래스의 타입에 대해 메시지를 전송하더라도 실행 시에는 실제 클래스를 기반으로 실제 실행될 메서드가 선택되게 해준다. => 코드를 변경하지 않고도 실행되는 메서드를 변경 가능
개방-폐쇄 원칙과 의존성 역전 원칙
- 업캐스팅과 동적 메서드 탐색은 코드를 변경하지 않고도 기능을 추가할 수 있게 해주며 이것은 개방-폐쇄 원칙의 의도와도 일치한다.
- 개방-폐쇄 원칙이 목적이라면 업캐스팅과 동적 메서드 탐색은 목적에 이르는 방법이다.
- 사실 Professor 클래스는 추상화가 아닌 구체 클래스 Lecture에 의존하기 때문에 의존성 역전 원칙을 따른다고 말하기는 어렵다.
- 또한 Professor 클래스의 코드가 개방-폐쇄 원칙을 따르는 코드를 만들기 위해 상속을 올바르게 사용했다고 말하기도 어려운데 이는 개방-폐쇄 원칙의 중심에는 추상화가 위치하고 있기 때문이다.
업캐스팅
- 모든 객체지향 언어는 명시적으로 타입을 변환하지 않고도 부모 클래스 타입의 참조 변수에 자식 클래스의 인스턴스를 대입할 수 있게 허용한다.
Lecture lecture = new GradeLecture(...);- 부모 클래스 타입으로 선언된 파라미터에 자식 클래스의 인스턴스를 전달하는 것도 가능하다
public class Professor {
public Professor(String name, Lecture lecture) { ... }
}
Professor professor = new Professor("다익스트라", new GradeLecture(...));- 컴파일러의 관점에서 자식 클래스는 아무런 제약 없이 부모 클래스를 대체할 수 있기 때문에 부모 클래스와 협력하는 클라이언트는 다양한 자식 클래스의 인스턴스와도 협력하는 것이 가능하다.
- Lecture의 모든 자식 클래스는 evaluate 메시지를 이해할 수 있기 때문에 professor는 Lecture를 상속받는 어떤 자식 클래스와도 협력할 수 있는 무한한 확장 가능성을 갖는다. => 이 설계는 유연하며 확장이 용이하다.
동적 바인딩
- 함수를 호출하는 전통적인 언어들은 호출될 함수를 컴파일타임에 결정한다.
- 컴파일타임에 호출할 함수를 결정하는 방식을 정적 바인딩, 초기 바인딩, 또는 컴파일타임 바인딩이라고 한다.
- 객체지향 언어에서는 메시지를 수신했을 때 실행될 메서드가 런타임에 결정된다.
- 실행될 메서드를 런타임에 결정하는 방식을 동적 바인딩 또는 지연 바인딩이라고 한다.
- 실행 시점에 어떤 클래스의 인스턴스를 생성해서 전달하는지를 알아야만 실제로 실행되는 메서드를 알 수 있다.
4. 동적 메서드 탐색과 다형성
- 객체지향 언어는 어떤 규칙에 따라 메시지 전송과 메서드 호출을 바인딩하는 것일까?
- 객체지향 시스템은 다음 규칙에 따라 실행할 메서드를 선택한다.
- 메시지를 수신한 객체는 먼저 자신을 생성한 클래스에 적합한 메서드가 존재하는지 검사한다. 존재하면 메서드를 실행하고 탐색을 종료한다.
- 메서드를 찾지 못했다면 부모 클래스에서 메서드 탐색을 계속한다. 이 과정은 적합한 메서드를 찾을 때까지 상속 계층을 따라 올라가며 계속된다.
- 상속 계층의 가장 최상위 클래스에 이르렀지만 메서드를 발견하지 못한 경우 예외를 발생시키며 탐색을 중단한다.
self 참조
- 메시지 탐색과 관련해서 이해해야 하는 중요한 변수로 self 참조가 있다.
- 객체가 메시지를 수신하면 컴파일러는 self 참조라는 임시 변수를 자동으로 생성한 후 메시지를 수신한 객체를 가리키도록 설정한다.
- 동적 메서드 탐색은 self가 가리키는 객체의 클래스에서 시작해서 상속 계층의 역방향으로 이뤄지며 메서드 탐색이 종료되는 순간 self 참조는 자동으로 소멸된다.
- 시스템은 class 포인터와 parent 포인터와 함께 self 참조를 조합해서 메서드를 탐색한다.

- self 참조에서 상속 계층을 따라 이뤄지는 동적 메서드 탐색
- 시스템은 메시지를 처리할 메서드를 탐색하기 위해 self 참조가 가리키는 메모리로 이동
- 이 메모리에는 객체의 현재 상태를 표현하는 데이터와 객체의 클래스를 가리키는 class 포인터가 존재한다.
- class 포인터를 따라 이동하면 메모리에 로드된 GradeLecture 클래스의 정보를 읽을 수 있다.
- 클래스의 정보 안에는 클래스 안에 구현된 전체 메서드의 목록이 포함돼 있다.
- 이 목록 안에 메시지를 처리할 적절한 메서드가 존재하면 해당 메서드를 실행한 후 동적 메서드 탐색을 종료한다
동적 메서드 탐색의 원리
- 자동적인 메시지 위임: 클래스 사이의 위임은 프로그래머의 개입없이 상속 계층을 따라 자동으로 이뤄진다.
- 동적인 문맥: 메시지를 수신했을 때 실제로 어떤 메서드를 실행할지를 결정하는 것은 컴파일 시점이 아닌 실행 시점에 이뤄진다.
- 메시지가 처리되는 문맥을 이해하기 위해서는 정적인 코드를 분석하는 것만으로는 충분하지 않다.
- 런타임에 실제로 메시지를 수신한 객체가 어떤 타입인지를 추적해야 한다. 이 객체의 타입에 따라 메서드를 탐색하는 문맥이 동적으로 결정되며, 여기서 가장 핵심적인 역할을 하는 것이 self 참조다.
자동적인 메시지 위임
- 상속 계층을 정의하는 것은 메서드 탐색 경로를 정의하는 것과 동일하다.
- 메서드 오버라이딩은 자식 클래스의 메서드가 동일한 시그니처를 가진 부모 클래스의 메서드보다 먼저 탐색되기 때문에 벌어지는 현상
- 메서드 오버로딩: 이름만 같고 시그니처가 완전히 동일하지 않은 메서드들은 상속 계층에 걸쳐서 공존
메서드 오버라이딩
- 자식 클래스와 부모 클래스 양쪽 모두에 동일한 시그니처를 가진 메서드가 구현돼 있다면 자식 클래스의 메서드가 먼저 검색된다.
- 자식 클래스의 메서드가 부모 클래스의 메서드를 감추는 것처럼 보이게 된다.
- 자식 클래스가 부모 클래스의 메서드를 오버라이딩하면 자식 클래스에서 부모 클래스로 향하는 메서드 탐색 순서로 인해 자식 클래스의 메서드가 부모 클래스의 메서드를 감추게 된다.
메서드 오버로딩
- 시그니처가 다르기 때문에 동일한 이름의 메서드가 공존하는 경우를 메서드 오버로딩이라고 부른다.
- 메서드 오버라이딩은 메서드를 감추지만 메서드 오버로딩은 사이좋게 공존한다.
- 자바는 하나의 클래스 안에서와 상속 계층 사이에서의 메서드 오버로딩 모두 지원하지만
- C++은 같은 클래스 안에서의 메서드 오버로딩은 허용하지만 상속 계층 사이에서의 메서드 오버로딩은 금지한다.
동적인 문맥
메시지를 수신한 객체가 무엇이냐에 따라 메서드 탐색을 위한 문맥이 동적으로 변경되는데
이 동적인 문맥을 결정하는 것은 바로 메시지를 실제로 수신한 객체를 가리키는 self 참조다.
동일한 코드라고 하더라도 self 참조가 가리키는 객체가 무엇인지에 따라 메서드 탐색을 위한 상속 계층의 범위가 동적으로 변한다. => self 참조가 가리키는 객체의 타입을 변경함으로써 객체가 실행될 문맥을 동적으로 바꿀 수 있다.
self 참조가 동적 문맥을 결정한다는 사실은 종종 어떤 메서드가 실행될지를 예상하기 어렵게 만든다. => 자기 자신에게 다시 메시지를 전송하는 self 전송
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 전송이라고 부른다.
- self 전송을 이해하기 위해서는 self 참조가 가리키는 바로 그 객체에서부터 메시지 탐색을 다시 시작한다는 사실을 기억해야 한다.
public class GradeLecture extends Lecture {
@Override
public String getEvaluationMethod() {
return "Grade";
}
}- self 전송은 자식 클래스에서 부모 클래스 방향으로 진행되는 동적 메서드 탐색 경로를 다시 self 참조가 가리키는 원래의 자식 클래스로 이동시킨다.
- 이로 인해 최약의 경우에는 실제로 실행될 메서드를 이해하기 위해 상속 계층 전체를 훑어가며 코드를 이해해야 하는 상황이 발생할 수도 있다.
이해할 수 없는 메시지
- 이해할 수 없는 메시지를 처리하는 방법은 프로그래밍 언어가 정적 타입 언어에 속하는지, 동적 타입 언어에 속하는지에 따라 달라진다.
정적 타입 언어와 이해할 수 없는 메시지
- 정적 타입 언어에서는 코드를 컴파일할 때 상속 계층 안의 클래스들이 메시지를 이해할 수 있는지 여부를 판단한다.
- 상속 계층 전체를 탐색한 후에도 메시지를 처리할 수 있는 메서드를 발견하지 못했다면 컴파일 에러를 발생시킨다.
동적 타입 언어와 이해할 수 없는 메시지
- 동적 타입 언어에는 컴파일 단계가 존재하지 않기 때문에 실제로 코드를 실행해보기 전에는 메시지 처리 가능 여부를 판단할 수 없다.
- 몇 가지 동적 타입 언어는 최상위 클래스까지 메서드를 탐색한 후에 메서드를 처리할 수 없다는 사실을 발견하면 self 참조가 가리키는 현재 객체에게 메시지를 이해할 수 없다는 메시지를 전송한다.
- 스몰토크에서는 메시지를 찾지 못했을 때 doesNotUnderstand 메시지를 전송
- 루비의 경우 method_missing 메시지를 전송
- 이 메서드들 역시 보통의 메시지처럼 self 참조가 가리키는 객체의 클래스에서부터 시작해서 상속 계층을 거슬러 올라가며 메서드를 탐색
- 만약 상속 계층 안의 어떤 클래스도 메시지를 처리할 수 없다면 메서드 탐색은 다시 한 번 최상위 클래스에 이르게 되고 최종적으로 예외가 던져진다.
- 스몰토크의 최상위 클래스 Object는 doesNotUnderstand 메시지에 대한 기본 처리로 MessageNotUnderstood 예외를 던진다.
- 루비의 최상위 클래스 Object는 method_missing 메시지에 대한 기본 처리로 NoMethodError 예외를 던진다.
- 단, 동적 타입 언어에서는 이해할 수 없는 메시지에 대해 예외를 던지는 것 외에도 선택할 수 있는 방법이 있다. => doseNotUnderstood나 method_missing 메시지에 응답할 수 있는 메서드를 구현
- 이 경우 객체는 자신의 인터페이스에 정의되지 않은 메시지를 처리 가능 => 메시지가 선언되는 인터페이스와 메서드가 정의되는 구현을 더 명확하게 분리하여 순수한 객체지향의 이상에 좀 더 가까워진다.
- self vs super(self 전송 vs super 전송)
- 대부분의 객체지향 언어들은 자식 클래스에서 부모 클래스의 인스턴스 변수나 메서드에 접근하기 위해 사용할 수 있는 super 참조라는 내부 변수를 제공한다.
- super 참조의 용도는 부모 클래스에 정의된 메서드를 실행하기 위한 것이 아니다.
- super 참조의 정확한 의도는 '지금 이 클래스의 부모 클래스에서부터 메서드 탐색을 시작하세요'다. => super 참조를 통해 실행하고자 하는 메서드가 반드시 부모 클래스에 위치하지 않아도 되는 유연성을 제공
- self 전송이 메시지를 수신하는 객체의 클래스에 따라 메서드를 탐색할 시작 위치를 동적으로 결정하는 데 비해 super 전송은 항상 메시지를 전송하는 클래스의 부모 클래스에서부터 시작된다.
- self 전송의 경우 메서드 탐색을 시작할 클래스를 반드시 실행 시점에 동적으로 결정해야 하지만
- super 전송의 경우에는 메서드 탐색을 시작할 클래스를 컴파일 시점에 미리 결정해 놓을 수 있다.
5. 상속 대 위임
위임과 self 참조
- 자신이 수신한 메시지를 다른 객체에게 동일하게 전달해서 처리를 요청하는 것을 위임이라고 부른다.
- 위임은 본질적으로는 자신이 정의하지 않거나 처리할 수 없는 속성 또는 메서드의 탐색 과정을 다른 객체로 이동시키기 위해 사용한다.
- 이를 위해 위임은 항상 현재의 실행 문맥을 가리키는 self 참조를 인자로 전달한다. => 이것이 포워딩과 위임의 차이점이다.
- 포워딩과 위임
- 객체가 다른 객체에게 요청을 처리할 때 인자로 self를 전달하지 않을 수도 있다.
- 이는 요청을 전달받은 최초의 객체에 다시 메시지를 전송할 필요는 없고 단순히 코드를 재사용하고 싶은 경우이다.
- 처리를 요청할 때 self 참조를 전달하지 않는 경우를 포워딩이라고 부른다.
- 이와 달리 self 참조를 전달하는 경우에는 위임이라고 부른다.
- 위임의 정확한 용도는 클래스를 이용한 상속 관계를 객체 사이의 합성 관계로 대체해서 다형성을 구현하는 것이다.
- 위임은 객체 사이의 동적인 연결 관계를 이용해 상속을 구현하는 방법이다.
- 상속이 매력적인 이유는 직접 구현해야 하는 번잡한 위임 과정을 자동으로 처리해주기 때문이다
- self 참조의 전달은 결과적으로 자식 클래스의 인스턴스와 부모 클래스의 인스턴스 사이에 동일한 실행 문맥을 공유할 수 있게 해준다.
프로토타입 기반의 객체지향 언어
- 클래스가 아닌 객체를 이용해서도 상속을 흉내낼 수 있다.
- 클래스가 존재하지 않고 오직 객체만 존재하는 프로토타입 기반의 객체지향 언어에서 상속을 구현하는 유일한 방법은 객체 사이의 위임을 이용하는 것이다.
- 클래스 기반의 객체지향 언어들이 상속을 이용해 클래스 사이에 self 참조를 자동으로 전달하는 것처럼
- 프로토타입 기반의 객체지향 언어들 역시 위임을 이용해 객체 사이에 self 참조를 자동으로 전달한다.
- 프로토타입 기반의 객체지향 언어, 자바스크립트에서의 prototype
- 자바스크립트의 모든 객체들은 다른 객체를 가리키는 용도로 사용되는 prototype이라는 이름의 링크를 갖는다.
- prototype은 위의 위임을 직접 구현하는 경우에서의 @parent와 동일한 것으로 볼 수 있다.
- 다만 차이점은 prototype은 언어 차원에서 제공되기 때문에 위임을 직접 구현과 같이 self 참조를 직접 전달하거 메시지 포워딩을 직접 구현할 필요가 없다는 것이다.
- 자바스크립트의 prototype을 이용하면 self 참조는 자동으로 전달된다.
function Lecture(name, scores) {
this.name = name;
this.scores = scores;
}
Lecture.prototype.stats = function() {
return "Name: " + this.name +
"Evaluation Method: " +
this.getEvaluationMethod();
}
Lecture.prototype.getEvaluationMethod = function() {
return "Pass or Fail"
}- 메서드를 Lecture의 prototype이 참조하는 객체에 정의
- Lecture를 이용해서 생성된 모든 객체들은 prototype 객체에 정의된 메서드를 상속받는다.
- 특별한 작업을 하지 않는 한 prototype에 할당되는 객체는 자바스크립트의 최상위 객체 타입인 Object
- 따라서 Lecture를 이용해서 생성되는 모든 객체들은 prototype이 참조하는 Object에 정의된 모든 속성과 메서드를 상속받는다.
function GradeLecture(name, canceled, scores) {
Lecture.call(this, name, scores);
this.canceled = canceled;
}
GradeLecture.prototype = new Lecture();
GradeLecture.protype.constructor = GradeLecture;
GradeLecture.prototype.getEvaluationMethod = function () {
return "Grade"
}- GradeLecture의 prototype에 Lecture 인스턴스를 할당함으로써 GradeLecture를 이용해 생성된 모든 객체들이 prototype을 통해 Lecture에 정의된 모든 속성과 함수에 접근할 수 있게 된다.
- 이제 메시지를 전송하면 prototype으로 연결된 객체 사이의 경로를 통해 객체 사이의 메서드 탐색이 자동으로 이뤄진다.
var grade_lecture
= new GradeLecture("OOP", false, [1, 2, 3]);
grade_lecture.stats();- 메시지를 수신한 인스턴스는 먼저 GradeLecture에 stats 메서드가 존재하는지 검사한다.
- GradeLecture에는 stats 메서드가 존재하지 않기 때문에 다시 prototype을 따라 Lecture의 인스턴스에 접근한 후 stats 메서드가 존재하는지 살펴본다.
- Lecture의 stats 메서드를 실행하는 도중에 this.getEvaluationMethod() 발견 => 상속과 마찬가지로 self 참조가 가리키는 현재 객체에서부터 다시 메서드 탐색을 시작
- 정적인 클래스 간의 관계가 아니라 동적인 객체 사이의 위임을 통해 상속을 구현한다.
- prototype으로 연결된 객체들의 체인을 거슬러 올라가며 자동적인 메시지 위임을 처리
- 자바스크립트에는 클래스가 존재하지 않기 때문에 오직 객체들 사이의 메시지 위임만을 이용해 다형성을 구현 => 상속 이외의 방법으로도 다형성을 구현할 수 있다.
상속과 다형성의 본질은 클래스가 아니다.
클래스는 단지 도구 중 하나일 뿐이다.
'Architecture > 객체지향설계' 카테고리의 다른 글
| [오브젝트] 합성과 유연한 설계 (1) | 2025.07.21 |
|---|---|
| [오브젝트] 상속과 코드 재사용 (0) | 2025.07.20 |
| [오브젝트] 유연한 설계 (0) | 2025.07.16 |
| [오브젝트] 의존성 관리하기 (1) | 2025.07.10 |
| [오브젝트] 객체 분해 (0) | 2025.07.06 |