본문 바로가기
📚 Kotlin

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

by GroovyArea 2023. 1. 30.

[이펙티브 자바] 를 읽었던 기억을 떠올렸을 때 '상속보다는 컴포지션을 사용하라' 라는 아이템이 상당히 인상 깊었다.

하지만 컴포지션 적용시키면서 다형성을 만족시키려면 인터페이스를 상속 받아야 하는데, 모두 구현해야 하므로
코드가 참 더러워지는 것을 확인할 수 있었다.

코틀린을 이를 기가 막히게 해결했다.

다시 상속과 컴포지션의 예를 살펴보고 이를 코틀린이 어떻게 간단히 해결할 수 있는지 살펴 보자.

 

상속의 문제점

  • 모든 것을 가져올 수밖에 없음
    • 슈퍼클래스의 상태, 메서드, 행위 등 모든 것을 가져온다.
      • 계층 구조를 나타낼 때 굉장히 좋은 도구
    • 그렇기에 일부분만을 사용하기에 부적합한 방법
    • 이를 위해서 컴포지션을 사용한다.
  • 캡슐화를 깨는 상속
    • 내부적인 구현 방법의 변경으로 캡슐화가 깨진다.

 

컴포지션

슈퍼클래스를 상속 받기 보다 필요한 부분만을 취하기 위해,
프로퍼티로 취하면 된다.

이를 테면,

class CounterSet<T> : HashSet<T>() {

	//....
}

// 위의 클래스처럼 슈퍼클래스를 직접 상속 받는 것이 아닌,

class CounterSet<T> {
	private val innerSet = HashSet<T>()
    
    // ...
}

// 이러한 형태로 프로퍼티로 가져와서 적절히 필요한 행위만 사용한다.

이렇게 하면 HashSet의 모든 것을 가져오기 보다는 CounterSet 객체가 필요한 부분만을 컴포지션을 통해 얻은 innerSet 프로퍼티로 활용할 수 있게 되었다.

하지만,
컴포지션을 활용하게 되면 다형성의 이점을 활용하지 못할 경우가 생긴다.

이럴 경우에는 포워딩 메서드를 통해 극복할 수 있다.

class CounterSet<T> : MutableSet<T>{
	private val innerSet = HashSet<T> ()
    
    //,..
    
    // 하지만 MutableSet 인터페이스의 모든 추상 메서드를 innerSet을 이용하여 오버라이드 해야 한다.
    // 개귀찮.. 코드 개더럽..
}

코드가 필요 이상으로 길어지므로 주석만 작성했다.
MutableSet의 추상 메서드가 얼마나 많은데, 그걸 어떻게 다 구현할까?
코드가 필요 이상으로 너무 길어진다. (자그마치 9개..)

코틀린을 이를 Fancy 하게 해결했다.
바로 위임 패턴을 통해서.

class CounterSet<T> (
	private val innerSet = HashSet<T>(),
) : MutableSet<T> by innerSet {
	
    //...
    
    // 이제는 by 키워드로 인해 프로퍼티인 innerSet을 이용해 구현을 위임할 수 있다.
    // by 키워드 하나로..!
}

참 Fancy 하다.

정리

하지만 다형성까지 필요한 경우가 거의 없을 것이다.
웬만한 경우는 컴포지션으로 해결할 일이 많을 것이다.

컴포지션을 활용하면, 코드를 이해하기 쉬우며, 유연하게 확장할 수 있다.
굳이 다형성을 가져가기 위해 위임을 사용할 필요 없이.

상속을 하지 못하게 하려면 final 키워드를 사용하면 된다.

하지만 상속하려는 클래스의 특정 메서드들의 오버라이딩을 막으려면 open 키워드를 붙이지 않으면 된다.
코틀린은 참 간단하다. (물론 이건 자바와 비슷하지만)

마지막으로 OOP 지향 방법은 상속보다는 컴포지션 사용이 best라는 점을 인지하면서 마무리한다.

반응형