본문 바로가기
프로그래밍 놀이터/Kotlin, Coroutine

[Effective Kotlin] Item 36 : Prefer composition over inheritance

by 돼지왕 왕돼지 2022. 5. 16.
반응형

이 글은 Effective Java 를 완독하고, Kotlin 을 상용으로 사용하는 개발자 입장에서
Effective Kotlin 글 중 새로운 내용, remind 할 필요 있는 부분, 핵심 내용 등만 추려 정리한 내용입니다.

 

#
상속은 강력한 기능이다. 하지만 이것은 is-a 관계의 계층을 만들기 위해 디자인 되었다는 것을 명심하고 사용해야 한다. 이 관계가 확실하지 않으면 상속은 문제를 야기할 수 있고 위험하다. 대안으로 class composition 이 선호된다.

 

 

Simple behavior reuse

#
상속은 다음의 문제를 갖는다.

  1. 1개의 class 만 상속 가능하다.
  2. 상속하면, 해당 class 의 모든 것을 가져온다. (불필요한것 포함)
  3. 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 관계가 있을 때만 상속을 사용해야 한다.

 

 

 

반응형

댓글