메서드를 재정의하면 어떤 일이 일어나는지를 정확히 정리하여 문서로 남겨야 한다. 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지(자기 사용 패턴) 문서로 남겨야 한다.
자기 사용 패턴 : 클래스 내부의 한 메서드가 같은 클래스의 다른 메서드를 호출하는 패턴. 자기 사용 패턴이 상속과 결합될 때 문제가 발생할 수 있다. 슈퍼클래스가 자신의 메서드를 호출할 때 실제로 실행되는 메서드가 서브클래스에서 오버라이드된 메서드일 수 있다.
아래는 API 문서의 메서드 설명 예시이다.
설명 끝 부분에 "Implementation Requirements"로 시작되는 부분은 그 메서드의 내부 동작 방식을 설명하는 곳이다.
public boolean remove(Object o)
주어진 원소가 이 컬렉션 안에 있다면 그 인스턴스를 하나 제거한다(선택적 동작). 더 정확
하게 말하면, 이 컬렉션 안에 'Object.equals(o, e)가 참인 원소' e가 하나 이상 있다면 그
중 하나를 제거한다. 주어진 원소가 컬렉션 안에 있었다면(즉, 호출 결과 이 컬렉션이 변경
됐다면) true를 반환한다.
Implementation Requirements: 이 메서드는 컬렉션을 순회하며 주어진 원소를 찾도록
구현되었다. 주어진 원소를 찾으면 반복자의 remove 메서드를 사용해 컬렉션에서 제거한다.
이 컬렉션이 주어진 객체를 갖고 있으나, 이 컬렉션의 iterator 메서드가 반환한 반복자가
remove 메서드를 구현하지 않았다면 UnsupportedOperationException을 던지니 주의하자.
설명에 따르면 iterator 메서드를 재정의하면 remove 메서드의 동작에 영향을 줌을 알 수 있다. iterator 메서드로 얻은 반복자의 동작이 remove메서드의 동작에 주는 영향도 설명했다. 이런식으로 클래스를 안전하게 상속할 수 있도록 하려면 내부 구현 방식을 설명해주어야 한다.
하지만 내부 매커니즘을 문서로 남기는 것만이 상속을 위한 설계의 전부는 아니다. 효율적인 하위 클래스를 만들 수 있게 하려면 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 protected메서드 형태로 공개해야 할 수도 있다.
훅 메서드 : 상위 클래스에서 정의되지만, 기본적으로 아무 작업도 수행하지 않거나 기본 동작만 수행하는 메서드이다. 이 메서드들을 오버라이드(재정의)함으로써, 상위 클래스의 동작 과정 중 특정 지점에서 원하는 동작을 알고리즘 흐름을 변경하지 않으면서 추가하거나 변경할 수 있다.
public abstract class Beverage {
// 이 메서드는 알고리즘의 템플릿을 제공합니다.
// 즉, 음료를 준비하는 일련의 단계를 정의합니다.
final void prepareRecipe() {
boilWater();
brew();
pourInCup();
if (customerWantsCondiments()) { // 훅(hook). 하위 클래스에서 이 메서드를 오버라이드할 수 있습니다.
addCondiments();
}
}
abstract void brew();
abstract void addCondiments();
void boilWater() {
System.out.println("물을 끓입니다.");
}
void pourInCup() {
System.out.println("컵에 붓습니다.");
}
// 이것이 바로 훅 메서드입니다. 기본적으로 이 메서드는 'true'를 반환하지만,
// 하위 클래스에서 이 메서드의 동작을 변경할 수 있습니다.
protected boolean customerWantsCondiments() {
return true;
}
}
// 하위 클래스 예시: 커피 준비하기
public class Coffee extends Beverage {
@Override
void brew() {
System.out.println("필터를 통해 커피를 우려냅니다.");
}
@Override
void addCondiments() {
System.out.println("설탕과 우유를 추가합니다.");
}
@Override
protected boolean customerWantsCondiments() {
// 여기서는 사용자가 조미료(설탕, 우유)를 추가하길 원하는지에 대한 로직을 구현할 수 있습니다.
// 예를 들어, 사용자의 입력을 받아 결정할 수 있습니다.
// 이 예제에서는 단순화를 위해 항상 'true'를 반환하도록 합니다.
return true;
}
}
이처럼 hook 메서드를 잘 활용하면 내부 동작 과정 중 중간에 끼어들어 수정할 수 있다. 어떤 메서드를 protected로 노출해야 할지는 그냥 심사숙고해서 잘 예측해보고, 실제로 하위 클래스를 만들어 보는것이 최선이다. 반드시 하위 클래스를 만들어 검증해야 한다.
클래스를 상속용으로 설계하려면 엄청난 노력이 들고 그 클래스에 안기는 제약도 상당하기 때문에 절대 가볍게 생각하고 정할 문제가 아니다. 상속을 허용하는게 명확히 정당한 상황이 있고, 아닌 상황이 있다. 이런 문제를 해결하는 가장 좋은 방법은 상속용으로 설계하지 않은 클래스는 상속을 금지하는 것이다.
결론 : 상속용 클래슬르 설계하려면 클래스 내부에서 스스로를 어떻게 사용하는지(자기 사용 패턴) 모두 문서로 남겨야 하며, 일단 문서화 한것은 그 클래스가 쓰이는 한 반드시 지켜야 한다. 그렇지 않으면 그 내부 구현 방식을 믿고 활용하던 하위 클래스를 오동작하게 만들 수 있다. 다른 이가 효율 좋은 하위 클래스를 만들 수 있도록 일부 메서드를 protected로 제공해야할 수도 있다. 그러니 클래스를 확장해야 할 명확한 이유가 떠오르지 않으면 상속을 금지하는 편이 낫다. 상속을 금지하려면 클래스를 final로 선언하거나 생성자 모두를 외부에서 접근할 수 없도록 만들면 된다.
'이펙티브 자바' 카테고리의 다른 글
[이펙티브 자바] 아이템 21. 인터페이스는 구현하는 쪽을 생각해 설계하라 (0) | 2024.03.22 |
---|---|
[이펙티브 자바] 아이템 20. 추상 클래스보다는 인터페이스를 우선하라 (0) | 2024.03.21 |
[이펙티브 자바] 아이템 18. 상속보다는 컴포지션을 사용하라 (0) | 2024.03.21 |
[이펙티브 자바] 아이템 17. 변경 가능성을 최소화하라 (0) | 2024.03.19 |
[이펙티브 자바] 아이템 16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 (0) | 2024.03.18 |