이 글은 Effective Java 를 완독하고, Kotlin 을 상용으로 사용하는 개발자 입장에서
Effective Kotlin 글 중 새로운 내용, remind 할 필요 있는 부분, 핵심 내용 등만 추려 정리한 내용입니다.
#
상속은 강력한 기능이다. 하지만 이것은 is-a 관계의 계층을 만들기 위해 디자인 되었다는 것을 명심하고 사용해야 한다. 이 관계가 확실하지 않으면 상속은 문제를 야기할 수 있고 위험하다. 대안으로 class composition 이 선호된다.
Simple behavior reuse
#
상속은 다음의 문제를 갖는다.
- 1개의 class 만 상속 가능하다.
- 상속하면, 해당 class 의 모든 것을 가져온다. (불필요한것 포함)
- superclass 의 기능 사용의 명시성이 떨어진다. (그래서 사용자가 super class 의 구현상태를 보러 자주 들어간다.)
Taking the whole package
#
상속은 superclass 의 모든 것을 물려 받는다. 함수, 기대(계약), 동작까지.
그래서 계층적 구조를 표현하기에는 좋은 도구이지만, 그냥 공통된 부분을 재사용하기 위해서는 사용하면 안 된다.
단순 공통 부분 재사용을 위해서라면 composition 을 사용해야 한다.
#
abstract class Dog{
open fun bark() { /../ }
open fun aniff() { /../ }
}
class Labrador : Dog()
class RobotDog : Dog(){
override fun sniff(){
throw Error("Operation not supported")
}
}
위 예시는 RobotDog 이 필요하지 않은 함수를 가지고 있으므로 interface-segregation principle 에 위배된다. 또한 superclass 의 동작을 파괴하기 때문에 Liskov Substitution Principle 에도 위배된다.
만약 RobotDog 이 calculate 라는 함수를 가져야 한다면..
다중 상속이 불가하기 때문에 아래와 같이 할 수 없다.
abstract class Robot {
open fun calculate() { /../ }
}
class RobotDog : Dog(), Robot() // Error
Inheritance breaks encapsulation
#
class CounterSet: HashSet(){
var elementsAdded: Int = 0
private set
override fun add(element: T): Boolean{
elementsAdded++
return super.add(element)
}
override fun addAll(elements: Collection<T>): Boolean{
elementsAdded += elements.size
return super.addAll(elements)
}
}
val counterList = CounterSet()
counterList.addAll(listOf("A", "B", "C"))
print(counterList.elementsAdded) // 6
#
Composition 을 사용하면 다형성을 상실할 수 있다. 이 경우 delegate 를 사용하면 좋다.
class CounterSet<T>(private val innerSet: MutableSet<T> = setOf()) : MutableSet<T> by innerSet {
var elementsAdded: Int = 0
private set
override fun add(element: T): Boolean{
elementsAdded++
return innerSet.add(element)
}
override fun addAll(elements: Collection<T>): Boolean{
elementsAdded += elements.size
return innerSet.addAll(elements)
}
}
그러나 대부분 다형성이 필요 없는 경우가 많기 때문에, delegate 를 사용하지 않는다.
Restricting overriding
#
상속을 위해 디자인되지 않은 class 는, 상속을 막기 위해 기본적으로 final로 정의된다.
함수 역시 기본적으로 final 이다.
상속을 허용하기 위해서는 open 을 붙여주어야 한다.
이 때 필요한 부분에 한해서만 open 을 붙여주는 것이 중요하다.
그리고 override 하면서 그 이하의 subclass 들에 대해서 override 못 하도록 final 을 붙여줄 수도 있다.
Summary
#
Composition 은 더 안정적이다. 외부에서 관측 가능한 것에만 의존하고 어떻게 구현되었는지에 의존하지 않는다.
Composition 은 더 유연하다. 다중상속의 효과를 낼 수 있다. super & sub class 의 의존도 없다.
Composition 이 더 명시적이다. 어떤 object 에 대한 함수 호출인지 더 명확하다.
Composition 은 더 요구한다. 사용 객체의 변화에 대해 추가 구현이 있을 수 있다.
상속은 더 강력한 다형성을 제공한다. 이는 장점이기도 하지만, 제약이 강해진다는 단점도 있다.
#
일반적인 OOP 규칙에서는 상속보다는 composition 이 추천된다.
Kotlin 에서는 특히 그렇다. 그래서 class 와 function 모두 기본 final 로 정의되며, delegation interface 를 기본으로 제공한다.
#
is-a 관계가 있을 때만 상속을 사용해야 한다.
끝
댓글