이 포스트에서 다루는 상속은 구현 상속이며, 인터페이스 상속과는 무관하다
상위 클래스가 어떻게 구현되는냐에 따라 하위 클래스의 동작에 이상이 생길 수 있다. 상위 클래스의 변경이 코드 한 줄 바꾸지 않은 하위 클래스의 동작에 영향을 미친다는 이야기이다. 예를 들어 상위 클래스가 메소드 B
를 self-use
하는 메소드 A
가 있다면, 메소드 A
를 super
을 통해 call하게 되면 메소드 A
에서 하위 클래스에서 오버라이드한 메소드 B
가 불리게 된다. 이 때 오작동할 가능성이 다분하다.
상위 클래스에 새로운 메소드가 추가되었을 때 문제가 생길 수 있다. 하위 클래스에서 오버라이드하지 못한 메소드로 인하여 원하지 않는 동작을 허용하게 될 수 있다.
위 두 문제 모두 오버라이드를 하지 않고 새로운 메소드를 정의하여 사용하면 될 것이라 생각할 수 있다. 그러나 운 없게 상위 클래스에서 새로운 메소드가 마침 새롭게 정의한 메소드와 이름이 같지만 반환타입이 다르면 컴파일이 안된다. 시그니처가 같다면 오버라이드이니 위 와같은 문제가 그대로 발생하게 된다.
개념적으로 상위 클래스에 기능을 확장을 하고 싶으니 일차적으로 상속을 생각했을 것이다. 그러나 상속이 강한 결합인 만큼 코드 변경에 취약하다는 것을 알 수 있었다. 그래서 컴포지션과 전달을 통하여 이상의 문제들을 회피하면서 상위 클래스 기능을 확장하는 것이다.
기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하는 것이다.
새 클래스의 인스턴스 메소드들은 기존 클래스의 대응하는 메소드를 호출해 그 결과를 반환한다. 이 방식을 전달(forwarding)이라 하며, 새 클래스의 메소드들을 전달 메소드(forwarding method)라 한다.
책의 예제에서 전달 부분과 기능 확장 부분을 분리하였는데 이 부분을 상속을 통하여 연결하였다. 예제의 extends 키워드가 있어 컴포지션이 아니라 상속 아닌가 순간 헷갈릴 수 있으나, 확장 대상을 private 필드로 가지고 있는 전달 클래스를 상속하고 있으므로 기존 클래스 상속과는 다른 컴포지션이 맞다. 분리한 이유는 책에 언급되었듯 재사용성을 위함이다.
위 와같은 구성은 컴포지션으로 참조하는 것 뿐이기에 기존 클래스의 내부 구현 방식의 영향에서 벗어나며, 전달 클래스로 원하는 메소드만 전달하기에 기존 클래스에 새로운 메소드가 추가된다 한들 전달 클래스에서 전달하지 않으면 새로운 메소드로 인하여 원하지 않는 동작을 할 우려도 없다.
다른 인스턴스를 감싸고(wrap)있으므로 래퍼 클래스라 하며, 다른 기능을 덧씌운다는 뜻에서 데코레이터 패턴이라고 한다.
컴포지션과 전달의 조합은 넓은 의미로 위임(delegation)이라 부른다. 위임은 컴포지션을 전제로 한다. 그래서 그런지 컴포지션과 위임을 같은 의미로 많이 사용한다.
콜백 프레임워크에서 발생할 수 있는 문제이다. 콜백 프레임워크에서는 자기 자신의 참조를 다른 객체에 넘겨서 다음 호출 때 사용하도록 한다. 내부 객체는 자신이 래핑되어있는지 모르기때문에 자신의 참조를 넘기고, 콜백때는 래퍼 클래스가 아닌 내부 클래스인 자기 자신을 부르게 된다.
반드시 하위 클래스가 상위클래스의 진짜 하위 타입인 상황에서만 쓰여야한다. 즉, IS-A 관계에서만 상속해야 한다. 상속은 상위클래스의 결함까지도 상속하기 때문이다.
상속은 강력하지만 캡슐화를 해친다. 상속은 상위 클래스와 하위 클래스가 순수한 IS-A 관계일 때만 사용해야한다. 상속의 취약점을 피하려면 컴포지션과 전달을 이용하자. 특히 래퍼 클래스로 구현할 적당한 인터페이스가 있다면 더욱 그렇다. 래퍼 클래스는 하위 클래스보다 견고하고 강력하다.
지금까지 상위 클래스에서 자기 자신의 메소드를 부르면 상속을 했어도 자기 자신 메소드만 부르는 줄 알았다...
상속은 했어도 super를 잘 사용해 본 적이 없어서 몰랐다.
public class TestA {
private int count;
public TestA(){
}
public void countUp(){
System.out.println("TestA countUp");
count++;
}
public void countUpThree(){
System.out.println("TestA countUpTree");
countUp();
countUp();
countUp();
}
}
public class TestB extends TestA{
private int ccc;
@Override
public void countUp(){
System.out.println("TestB countUp");
ccc++;
super.countUp();
}
@Override
public void countUpThree(){
System.out.println("TestB countUpThree");
ccc+= 3;
super.countUpThree();
}
public int getCcc(){
return ccc;
}
}
// call TestB countUpThree()
TestB countUpThree
TestA countUpTree
TestB countUp
TestA countUp
TestB countUp
TestA countUp
TestB countUp
TestA countUp