상속은 관계가 명확하지 않을 때 사용하면, 여러 문제가 발생할 수 있다. 그렇기 때문에 이런 경우에 단순히 코드 추출 또는 재사용을 위해 상속을 하려고 한다면, 컴포지션을 사용하는것이 더 좋을 수 있다.

간단한 행위 재사용

프로그레스바를 출력하는 아래 예시를 보자


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개의 서브 클래스에서는 경고창이 필요 없을 때 상속을 사용하면 복잡한 구조가 만들어지게 된다. 이를 해결하기 위해서는 상속에서는 파라미터가 있는 생성자를 사용해 해결할 수 있다.