상속은 관계가 명확하지 않을 때 사용하면, 여러 문제가 발생할 수 있다. 그렇기 때문에 이런 경우에 단순히 코드 추출 또는 재사용을 위해 상속을 하려고 한다면, 컴포지션을 사용하는것이 더 좋을 수 있다.
프로그레스바를 출력하는 아래 예시를 보자
class ProfileLoader {
fun load() {
// 프로그레스 바를 보여줌
// 프로파일을 읽어 드림
// 프로그레스 바를 숨김
}
}
class ImageLoader {
fun load() {
// 프로그레스 바를 보여줌
// 프로파일을 읽어 드림
// 프로그레스 바를 숨김
}
}
위와 같은 기능을 만들어야 한다고 할 때, 많은 개발자들이 슈퍼클래스를 만들어 공통되는 행위를 추출한다.
abstract class LoaderWithProgress {
fun load() {
// 프로그레스바를 보여 줌
innerLoad()
// 프로그레스 바를 숨김
}
abstract fun innerLoad()
}
class ProfileLoader:LoaderWithProgress() {
override fun innerLoad(){
//프로필을 읽음
}
}
class ImageLoader:LoaderWithProgress() {
override fun innerLoad(){
//이미지를 읽음
}
}
이 코드는 간단한 경우에는 문제가 없지만, 몇가지 단점이 있다.
위와 같은 문제에 대안으로 사용할 수 있는것이 컴포지션이다. 컴포지션을 사용한다는 것은 객체를 프로퍼티로 갖고, 함수를 호출하는 형태로 재사용하는 것을 의미한다.
class ProfileLoader {
val progress = Progress()
fun load() {
progress. showProgress()
progress.hideProgress()
}
}
class ImageLoader {
val progress = Progress ()
fun load() {
progress.showProgress()
progress.hideProgress()
}
}
장점
단점
컴포지션을 사용하면 복잡한 계층 구조를 만들지 않아도 된다. 예를 들어 3개의 클래스가 프로그레스바와 경고창을 만드는 슈퍼클래스를 상속 받는데, 2개의 서브클래스에서는 경고창을 사용하지만, 다른 1개의 서브 클래스에서는 경고창이 필요 없을 때 상속을 사용하면 복잡한 구조가 만들어지게 된다. 이를 해결하기 위해서는 상속에서는 파라미터가 있는 생성자를 사용해 해결할 수 있다.