Composition over inheritance

Composition over inheritance

우선 동영상 하나 보고 가실게요.

What's wrong with inheritance?

Object Oriented Programming (이하 OOP) 에서 polymorphic (다형성) 을 구현하기 위해서 사용되는 방법 중 가장 대표적인 것이 아마 inheritance 일 것이다. 하지만, 지속적으로 inheritance 의 문제점이 지적되어 왔다. 대표적인 것이 이른바 고릴라 바나나 역설 이다.

the entire jungle 이 아주 찰지다

I think the lack of reusability comes in object-oriented languages, not functional languages. Because the problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle.

If you have referentially transparent code, if you have pure functions — all the data comes in its input arguments and everything goes out and leave no state behind — it’s incredibly reusable.

Joe Armstrong from Coders at Work

Erlang 을 만든 Joe Armstrong 의 이 문장은 상속으로 인해서 불필요한 메소드의 복제(?)가 많이 일어나고, 이에 따른 비용 혹은 부작용이 발생한다고 정리할 수 있을 것 같다. 다음 예제를 보자.

class Animal {
  constructor(name) {
    this.name = name
  }

  walk() {
    console.log('I can walk')
  }
}

class Human extends Animal {
  this.talk = () => console.log('I can even talk! My name is', this.name)
}

이런 샘플 코드만 봤을때는 상당히 직관적이고 당연한 것이라 볼 수 있지만, 어디 실제 코딩 상황에서 이런가?

class Title extends Component {
  render() {
    <h1>{this.props.label}</h1>
  }
}

const Title = (props) => {
  const { label } = props

  return (
    <h1>{label}</h1>
  )
}

React 의 경우 일반적으로는 컴포넌트를 두가지 방식[1]으로 선언하게 된다. 위의 Component 를 extends 하면 componentDidMount 같은 life cycle method 나 this.setState 와 같은 상태 함수를 사용할 수 있다. 두번째는 보통 Stateless Component 라고 불리는데 컴포넌트 내부에서 상태 변화를 감지해야 할 필요가 없을때 주로 쓰인다.[2]

상태 변화를 체크해야할 필요가 없는 DOM object 에 굳이 추가적인 메소드를 넣어 메모리를 낭비해야할 이유가 별로 없지 않은가? 특히나 여러 부모를 가진 자식 class 인 경우 실제 구조가 어떻게 되어있는지 트래킹 하는 것 조차 큰 일이 될 것이다.

Dawn of Composition

최근 그래서 대두되고 있는 개념이 composition 이다. 특히나 Composition over inheritance 라는 위키피디아 페이지까지 생길 정도로 최근 디자인 패턴의 주류 라고 생각해도 좋을 것 같다.

지난번 포스트에서 작성했듯이 Javascript (이하 JS) 는 prototype 이라는 독특한 방식으로 OOP 를 구현했기 때문에, object 생성 방식에서 부터 다른 언어에 비해 복잡한 것이 사실이다. 또한 JS 는 객체 내부에 private property 를 가질 수가 없기 때문에, 부작용이 발생할 여지도 커 inheritance 를 추천하지 않는다.

위에서 설명한 Animal 과 Human 을 composition 형태로 다시 작성해보자.

const walker = () => {
  return {
    walk: () => console.log('I can walk')
  }
}

const talker = (state) => {
  return {
    talk: () => console.log('I can even talk! My name is', state.name),
  }
}

const animal = (name) => {
  const state = {
    name,
  }
  
  return Object.assign(
    {},
    walker(),
  )
}

const human = (name) => {
  const state = {
    name,
  }

  return Object.assign(
    {},
    walker(),
    talker(state),
  )
}

human('Seokjun').talk()

> I can even talk! My name is – "Seokjun"

코드가 좀 길어진 것 같지만 Object.assign 이 어떻게 동작한다는지 안다는 가정 하에 상당히 가독성이 좋고 추가적으로 JS의 핵심적인 특성인 Closure 를 사용해서 Functional Programming 을 효과적으로 구현할 수 있다.

매번 작성하기가 번거로우니, Array.reduce 를 활용해서 composer 함수를 만들 수도 있다.

const composer = (state, ...funcs) => {
  return funcs.reduce((obj, func) => Object.assign(obj, func(state)), {})  
}

const animal = (name) => composer({ name }, walker)
const human = (name) => composer({ name }, walker, talker)

human('Seokjun').talk()

> I can even talk! My name is – "Seokjun"

Composition over inheritance

Do not use inheritance

이쯤 읽었으면 Composition 의 장점이 확실하게 드러났을 것이라고 생각한다. 그렇다면 언제 Composition 을 사용해야 할까? 많은 개발자들이 이 질문에 항상 이라고 대답하고 있다. Pure Function 으로 작성한 walker, talker 는 부작용도 없고, 테스트 코드를 작성하기에도 좋다. 모든 상속된 객체가 같은 특성을 공유할 이유도 그럴 필요도 없다. javascript 에서 구현이 안되는 private property 를 객체 지향 하에 손쉽게 구현할 수 있다.

뭔가 더 글을 남겨야 될것 같은 기분이 들지만 여기서 줄인다. composition 이라는게 그만큼 간단한 것이니까.



  1. 실제로는 PureComponent 나 React.createClass 등 몇가지 방법이 더 있다. ↩︎

  2. 하지만 Stateless Component 역시 실제로는 똑같이 React.Component 를 extends 한 것으로 트랜스파일링 된다. ↩︎