[Effective Kotlin] 아이템 36. 상속보다는 컴포지션을 활용하라

1
post-thumbnail

상속은 'is A' 관계의 객체 계층구조를 만들기 위해 설계되었기 때문에,
단순하게 코드 추출 또는 재사용을 위해 상속을 하려한다면, 일반적으로 컴포지션을 사용하는게 좋다.

간단한 행위 재사용

프로그레스바를 처리후 숨기는 로직이다.

class ProfileLoader{
	fun load(){
    	
   	} 

}

class ImageLoader{
	fun load(){
    
    }

}

많은 개발자들이 슈퍼클래스를 사용하여 공통되는 행위를 추출하지만, 몇가지 단점이 있다.

  • 많은 함수를 갖는 거대한 BaseXXX 클래스를 만들게 되고, 굉장히 깊고 복잡한 계층구조가 만들어진다.

  • 상속은 클래스의 모든것을 가져오게 되기 때문에 불필요한 함수를 갖는 클래스가 만들어 질수 있다.(인터페이스 분리 원칙 위반)

  • 상속은 이해하기 어렵다.

인터페이스 분리 원칙이란?

이런경우, 사용할수 있는것이 컴포지션이다.
컴포지션이란 객체를 프로퍼티로 갖고, 함수로 호출하여 재사용 하는것을 의미한다.

class Progress{
	fun showProgress(){}
    fun hideProgress(){}
}


class ProfileLoader{
	val progress = Progress()
    
    fun load(){
    	progress.showProgress()
    }
}

프로그레스 바를 관리 하는 개체를 모든 객체에서 갖고 활용하는 추가코드가 필요하다.
어려울수 있지만, 실행을 더 명확하게 예측할 수 있고, 프로그레스 바를 훨씬 자유롭게 사용할 수 있다.

또한 컴포지션을 활용하면, 클래스 내부에서 여러 기능을 재사용할 수 있다.

class ImageLoader{
	private val progress = Progress()
    private val finishedAlter = FinishedAlert()
    
	fun load(){
		progress.showProgress()
    	progress.hideProgress()
    	finishedAlert.show()
	}

}

이를 상속을 통해서는 구현하기가 어려운데, 2개의 서브 클래스에서는 경고창을 사용하지만, 다른 1개의 서브클래스에서는 경고창이 필요없는데 어떻게 구현할까?

한가지 방법은 파라미터가 있는 생성자를 사용하는것이다.

abstract class InternetLoader(val showAlert : Boolean){
	fun load(){
    	
        innerLoad()
        
        if(showAlert){
        
        }
    
    }

	abstract fun innerLoad(){

	}
}


class ProfileLoader : InternetLoader(showAlert = true){
	override fun innerLoad(){
    
    }

}

하지만 필요없는 서브클래스의 기능을 갖고 단순하게 이를 차단하는것 이라면 문제가 발생할 수 있기 때문에 좋지 않다.

모든것을 가져올 수 밖에 없는 상속

상속은 모든것을 가져오기떄문에 계층구조를 타날때 좋은 도구이다.

하지만, 재사용하기 위한 목적으로는 적합하지 않다.

abstract class Dog{
	open fun bark() {}
    //냄새 맡기
    open fun sniff() {}

}

만약 로봇강아지를 만들고 싶은데 sniff()는 못하게 하려면 상속이 아닌 컴포지션을 통해 해결한다.

class RobotDog : Dog(){
	//인터페이스 분리 원칙 에러
	override fun sniff(){
    	throw Error("operation not ")
    }
}

캡슐화를 깨는 상속

상속을 활용할때는 외부에서 이를 어떻게 활용하는지도 중요하지만, 내부적으로 이를 어떻게 활용하는지도 중요하다.

클래스의 캡슐화가 깨질수도 있다.

class CounterSet<T> : HashSet<T>(){
	var elementsAdded : Int = 0
    private set
    
    override fun add(element : T) : Boolean{
    
    }
    
    override fun addAll() : Boolean{
    elementsAdded += elements.size
    return super.addAll(elements)
    
    }

}
val counterLust = CounerSet<String>()
counterList.addAll(listOf("A", "B", "C"))

해당은 제대로 작동하지 않는데, 1super.addAll(elements)에서 add()를 사용하기 때문이다.

그래서 override fun addAll()을 지우면 해결되지만,

만약 자바가 addAll을 최적화하고 내부적으로 add를 호출하지 않는순간, 예상치 못한 형태로 동작하게 된다.

자바 개발자들도 이런 문제를 알기때문에 구현을 변경할때는 굉장히 신중을 가한다.

이러한 문제를 해결하기 위해선, 상속 대신 컴포지션을 사용하면 된다.

class CounterSet<T>{
	private val innerSet = HashSet<T>
    var elementsAdded : Int = 0
    private set
    
    fun add(element: T){
    	elementsAdded++
        innerSet.add(element)
    }

}

하지만 CounterSet은 더이상 Set이 아니기 때문에, 이를 유지하고 싶다면 위임 패턴을 사용한다.

위임 패턴은 Class가 Interface를 상속받게 하고, 객체의 메서드를 활용해서 인터페이스를 정의한 메서드를 구현하는 패턴이다.

이를 포워딩 메서드라고 부른다.

하지만 이를 만들기 위해서는 구현해야 하는 포워딩 메서드가 너무 많아지는데, 코틀린에서는 위임 패턴을 쉽게 구현할 수 있는 문법을 제공한다.

class CounterSet<T>(
	private val innerSet : MutableSet<T> = mutableSetOf()
) : MutableSet<T> by innerSet {

	var elementsAdded : Int = 0
	///
}

만약 상속된 메서드를 직접 활용하는것이 위험하다면, 위임패턴을 사용하는것이 좋다.

하지만 다형성이 필요없다면 컴포지션이 훨씬더 이해하기 쉽고 유연하다.

상속으로 캡슐화를 꺨수 있다는 사실은 보안문제고, 이러한 행위는 규약으로 지정되어 있거나 서브 클래스에 의존할 필요가 없는 경우이다.(메서드가 상속을 위해서 설계뙨 경우이다.)

오버라이딩 제한하기

상속용으로 설계되지 않은 클래스를 상속하지 못하게 하려면, final을 사용하면 된다.

하지만 상속은 허용하고 오버라이드는 허용하지않을때, 메서드에 open class의 open 메서드만 오버라이드 가능하다.

open class Parent{
	fun a() {}
    open fun b() {}
}

class Child : Parent(){
	override fun a() {} // 오류
    override fun b() {}
}

정리

  • 컴포지션은 내부적인 구현에 의존하지 않기때문에 더 안전하다.(모든것을 가져올 수 밖에 없는 상속 참고)

  • 상속은 슈퍼클래스의 동작을 변경하면, 서브클래스의 동작도 큰 영향을 받지만, 컴포지션은 아니다.

  • 컴포지션은 명시적이다. 컴포지션은 리시버를 명시적으로 활용할 수 있기때문에 어디에있는건지 알 수 있다.

  • 컴포지션은 생각보다 번거롭다.
    클래스의 일부 기능을 추가 할때, 이를 포함하는 객체의 코드를 변경해야한다.

  • 상속은 다형성을 활용할 수 있다.
    Dog은 반드시 Animal로 동작해야 하기 때문에, 슈퍼클래스와 서브 클래스의 규약을 항상 잘 지켜야 한다.

상속이 필요할때에는 슈퍼클래스를 상속하는 모든 서브 클래스는 슈퍼클래스로도 동작할 수 있어야 한다.(리스코프 치환원칙)

profile
쉽게 가르칠수 있도록 노력하자

0개의 댓글