오브젝트 Ch.10, 12 — 상속과 코드 재사용, 다형성
코드 재사용을 위한 상속이 위험한 이유와, 동적 메서드 탐색과 self 참조로 다형성이 동작하는 방식을 정리한다.
시리즈 · BackEnd 서적16 / 17
- SQL 레벨업 Ch.1 — DBMS 아키텍처
- SQL 레벨업 Ch.3 — SQL의 조건 분기
- SQL 레벨업 Ch.4~5 — 집약과 자르기, 반복문
- SQL 레벨업 Ch.6 — 결합
- SQL 레벨업 Ch.7 — 서브쿼리
- SQL 레벨업 Ch.8 — SQL의 순서
- SQL 레벨업 Ch.9 — 갱신과 데이터 모델
- SQL 레벨업 Ch.10 — 인덱스 사용
- 오브젝트 Ch.1 — 객체, 설계
- 오브젝트 Ch.2 — 객체지향 프로그래밍
- 오브젝트 Ch.3~4 — 역할, 책임, 협력 / 설계 품질과 트레이드오프
- 오브젝트 Ch.5 — 책임 할당하기
- 오브젝트 Ch.6 — 메시지와 인터페이스
- 오브젝트 Ch.8 — 의존성 관리하기
- 오브젝트 Ch.9 — 유연한 설계
- 오브젝트 Ch.10, 12 — 상속과 코드 재사용, 다형성
- 오브젝트 Ch.13 — 서브클래싱과 서브타이핑
10장은 상속을 코드 재사용 수단으로 쓸 때의 문제를, 12장은 상속의 진짜 목적인 다형성이 어떻게 동작하는지를 다룬다. 10장은 코드 변화를 따라가며 설명하는 장이라 요점만 적고, 12장과 묶어 정리한다.
상속과 코드 재사용 (Ch.10)
DRY 원칙
중복 코드란 요구사항이 변경됐을 때 함께 수정해야 하는 코드다. 중복이 늘어날수록 애플리케이션은 변경에 취약해지고, 버그가 늘며, 코드를 변경하는 속도는 느려진다. 앤드류 헌트와 데이비드 토마스는 이를 Don’t Repeat Yourself, DRY 원칙으로 정리했다.
중복을 제거하는 대표적인 방법이 상속이다. 그러나 객체지향에 대한 이해가 깊어지면서, 코드 재사용을 위해 맹목적으로 상속을 사용하는 것이 위험하다는 사실이 알려지기 시작했다.
상속을 위한 경고
- 자식 클래스의 메서드 안에서
super로 부모 클래스의 메서드를 직접 호출하면 두 클래스는 강하게 결합된다.super호출을 제거할 방법을 찾는다. - 상속받은 부모 클래스의 메서드가 자식 클래스의 내부 구조에 대한 규칙을 깨트릴 수 있다. Vector를 상속한 Stack이 대표적인 예다.
- 자식 클래스가 부모 클래스의 메서드를 오버라이딩하면, 부모 클래스가 자신의 메서드를 사용하는 방법에 자식 클래스가 결합될 수 있다.
- 클래스를 상속하면 결합도 때문에 두 클래스의 구현을 영원히 변경하지 않거나, 둘을 동시에 변경하거나 중 하나를 선택할 수밖에 없다.
상속은 자식 클래스를 점진적으로 추가해 기능을 확장하기에는 좋지만, 높은 결합도로 인해 부모 클래스를 점진적으로 개선하기는 어렵게 만든다. 상속의 단점을 피하면서 코드를 재사용하는 더 좋은 방법은 합성이다.
다형성 (Ch.12)
다형성(polymorphism)은 ’많은’을 뜻하는 poly와 ’형태’를 뜻하는 morph의 합성어로, 많은 형태를 가질 수 있는 능력을 의미한다. 프로그래밍에서는 여러 타입을 대상으로 동작할 수 있는 코드를 작성하는 방법이다.
다형성의 분류
- 유니버설 다형성
- 매개변수 다형성: 제네릭 프로그래밍과 관련이 높다.
List<T>처럼 타입을 임의의 T로 두고, 인스턴스를 생성하는 시점에 구체적인 타입으로 지정한다. - 포함 다형성(서브타입 다형성): 메시지가 동일하더라도 수신한 객체의 타입에 따라 실제로 수행되는 행동이 달라진다. 일반적으로 다형성이라고 하면 이것을 가리킨다.
- 매개변수 다형성: 제네릭 프로그래밍과 관련이 높다.
- 임시 다형성
- 오버로딩 다형성: 메서드 오버로딩으로 동일한 메서드 이름을 여러 시그니처에 사용한다.
- 강제 다형성: 형변환을 통해 동작한다.
// DiscountPolicy를 구현하는 어떤 인스턴스와도 조합할 수 있다
public class Movie {
private DiscountPolicy discountPolicy;
}
상속의 진정한 목적은 코드 재사용이 아니라 다형성을 위한 서브타입 계층을 구축하는 것이다.
동적 메서드 탐색
메시지를 수신한 객체가 실행할 메서드를 선택하는 과정은 다음과 같다.
- self 참조가 가리키는 객체의 클래스에서 적합한 메서드가 있는지 검사한다. 있으면 실행하고 탐색을 종료한다.
- 없으면 parent 참조를 따라 부모 클래스에서 탐색을 계속한다. 적합한 메서드를 찾을 때까지 상속 계층을 따라 올라간다.
- 최상위 클래스(Object)에 이르러서도 찾지 못하면 예외를 발생시키며 탐색을 중단한다.
동적 메서드 탐색의 입장에서 상속 계층은 객체가 자신이 이해할 수 없는 메시지를 부모 클래스에게 전달하기 위한 물리적인 경로다. 메시지는 이 경로를 따라 자동으로 위임된다.
참고로 동적 타입 언어에서는 예외를 던지는 대신 doesNotUnderstand나 method_missing 같은 메시지에 응답하는 메서드를 구현해 둘 수 있다.
self 참조와 동적인 문맥
public class GradeLecture extends Lecture {
@Override
public String getEvaluationMethod() {
return "Grade";
}
}
stats 메서드는 부모인 Lecture에만 있고, 그 안에서 getEvaluationMethod를 호출한다고 하자.
- GradeLecture 인스턴스에
stats메시지를 전송한다. - 탐색은 GradeLecture에서 시작해 부모 클래스인 Lecture에서
stats를 발견하고 실행한다. - Lecture의
stats를 실행하는 중에 self 참조가 가리키는 객체에게getEvaluationMethod메시지를 전송한다. - 이 탐색은 Lecture가 아니라 self 참조가 가리키는 GradeLecture에서 다시 시작된다.
- 따라서 GradeLecture가 오버라이딩한 메서드가 실행된다.
같은 코드라도 self가 무엇을 가리키느냐에 따라 실행되는 메서드가 달라진다. 최악의 경우 실제로 실행될 메서드를 알기 위해 상속 계층 전체를 훑어야 하므로, 이해하기 매우 어려운 코드가 될 수 있다.
super의 의미
super는 단순히 ’부모 클래스에 정의된 메서드를 실행하라’가 아니다. ’지금 이 클래스의 부모 클래스에서부터 메서드 탐색을 시작하라’는 뜻이다. 부모 클래스에 없다면 그 위로 계속 탐색한다.
포워딩과 위임
- 포워딩: 다른 객체에게 요청을 전달할 때 self 참조를 전달하지 않는다.
- 위임: self 참조를 함께 전달한다.
위임의 정확한 용도는 클래스 사이의 상속 관계를 객체 사이의 합성 관계로 대체해서 다형성을 구현하는 것이다.