Favor composition over inheritance.
객체지향에는 상속이라는 특징이 있다. 상속은 기존 객체를 토대로 새로운 객체를 생성하는 것으로 기존 객체의 특징을 웬만하면 전부 가지고 있고 (접근 제한자 public이라는 가정하에는 전부, protected는 해당 패키지의 클래스를 상속했다면 가능, default는 해당 패키지 안에서만, private은 불가), 기존 객체를 토대로 자신만의 특성을 확장하여 사용이 가능하다. 근데, 이거 evil이라고 하더라.
객체지향의 꽃인 다형성을 피우기 위해 상속이라는 개념은 객체지향에서 매우 중요하다고 어디선가 본거 같은데 정작 상속이 evil이라니? 그럼 애당초 패러다임에 문제가 있는 게 아닌가? 상속할 수 있어서 상속받아 사용했는데 왜 나빠?
암튼 이런 생각을 가지고 상속은 정말 나쁜지, 아니면 나쁜 상황이 있는지 그리고 대체 가능한 것은 무엇인지 찾아보다 내 고민을 해결해준 글을 읽게 되어 정리하는 차원에서 이번 TIL을 적게 되었다.
# 상속을 하면 메모리에서 발생하는 일
![]() |
![]() |
- 부모와 자식은 계층 구조를 이룬다
- heap -> 자식이 생성되면 부모도 같이 생성된다 -> 자식 A가 메모리에 올라갈 때 parent도 같이 올라간다.
- static(method) -> 자식의 설계도가 올라가면 부모의 설계도도 같이 올라간다.
- 생성된 객체의 참조 주소는 부모의 주소다.
- static에 올라간 타입의 메서드만 호출할 수 있다.
A a2 = new A();
a2를 보면 static 영역에 올라간 A타입은 parent 타입의 자식임으로 a2가 메모리에 올라가면 부모도 같이 올라간다. heap영역에 a2객체가 생성되면 부모 객체도 같이 생성되며 레퍼런스 a2는 부모의 주소를 참조한다
Parent d8 = new A();
d8.원()을 호출하면 자식의 원() 메서드가 호출된다. 이것은 자식이 부모의 메서드를 오버라이딩했을 때 자동으로 부모의 메서드를 이용해 자식의 메서드를 호출하는 메서드 다형성이 된다. 원리는 부모 메서드가 자식의 메서드를 자동 호출하는 것. VMI(Virtual Method Invocation)
# 상속의 문제점
- 상속은 코드 재사용을 가능하게 하는 강력한 방법이지만 항상 가장 좋은 방법만은 아니고 상속을 적절하게 사용하지 않는다면 소프트웨어를 매우 취약하게 만들 수 있다고 한다. -> 상위 클래스와 하위 클래스의 관계가 컴파일 시점에 결정되어 구현에 의존하기 때문.
- 상속을 사용해도 안전한 경우는 하위 클래스와 상위 클래스 구현이 동일한 프로그래머의 제어 하에 있는 패키지 내에서 사용하는 경우와, 확장을 위해 특별히 설계되고 문서화된 클래스를 사용할 때. -> 상속도 좋다, 다만 혼자서 프로그램을 작성할 때까지 혹은 치밀한 설계를 통해 extends를 위한 클래스를 만들고 문서화가 잘 되어있다면.
- 하위 클래스는 적절한 기능을 위해 상위 클래스의 구현에 의존하며 이것은 캡슐화를 위반한다. 하위 클래스는 상위 클래스에 의존하기 때문에 상위 클래스의 메서드 구현이 변경되면 상위 클래스를 상속하고 있는 모든 하위 클래스에서 메서드를 변경해줘야 한다. -> 상위 클래스의 메서드 이름이 run()에서 walk()로 변경되었다면 해당 메서드를 overiding 한 하위 클래스는 모두 walk()로 변경해야 하고, 내부 구현이 변경되었다면 따라서 변경해줘야 함.
- 상위 클래스는 후속 릴리스에서 새로운 매서드가 생길 수 있다 하위 클래스는 설계될 당시에 해당 메서드에 대한 정보가 없기 때문에 메서드 이름이 겹칠 수도 있고 새로 생긴 메서드를 재정의 하는 것도 문제가 생길 수 있으며, 단순히 호출하는 것도 문제가 생길 수 있다.
- 새로 생긴 메서드를 재정의 하지 않고 사용하는 것은 조금 더 안전하긴 하지만 상위 클래스의 새로운 메서드가 하위 클래스의 어떤 메서드와 같은 메서드 시그니처를 가지고 다른 리턴 값을 가진 경우에는 하위 클래스는 더 이상 컴파일되지 않는다.
- 유연성이 떨어져 필요 이상으로 많은 수의 클래스를 추가하는 클래스 폭발 문제가 발생할 수 있다.
# 위임 Delegation
자바는 위임을 언어적으로 지원해주지 않아 객체끼리 컴포지션(합성)을 시키는 방식으로 위임을 구현한다. -> 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 메서드를 호출하며, 원하지 않는 메서드는 사용을 하지 않으면 된다.
기능 확장이 필요할 경우 상속보다 컴포지션을 통한 위임 패턴을 사용하는 것이 좋다. 상속은 컴파일 시점에 상위 클래스와 하위 클래스의 코드가 강하게 결합되는 반면에 컴포지션을 이용하면 구현을 효과적으로 캡슐화할 수 있다. 또 의존하는 객체를 교체하는 것이 비교적 쉬우므로 설계도 유연해진다. -> 상속은 클래스를 통해 강하게 결합되지만 컴포지션은 메시지를 통해 느슨하게 결합되기 때문.
# 언제 상속을 쓰고 언제 위임을 쓰면 좋을까?
- 클래스의 객체가 다른 클래스의 객체가 들어갈 자리에 쓰여야 한다면 상속을 사용.
- 타입 계층을 구현하는 경우 상속을 사용.
- 부모와 자식 클래스가 Is-A 관계인 경우와 행동 호환성이 만족하는 경우 -> Bird 클래스의 fly() 메서드는 penguin이 사용하지 못 함 -> Bird , cantFlyingBird extends Bird, penguin extends....
- 클래스의 객체가 단순 다른 클래스의 객체를 사용만 한다면 위임을 사용.
- 타입 문제로 하위 클래스를 만들어 다형성에 이용해야 하는 경우는 interface를 이용한 컴포지션으로 해결 가능.
출처 : https://blogs.oracle.com/javamagazine/post/java-inheritance-composition