Composition over inheritance
우선 동영상 하나 보고 가실게요.
What's wrong with inheritance
?
Object Oriented Programming (이하 OOP) 에서 polymorphic (다형성) 을 구현하기 위해서 사용되는 방법 중 가장 대표적인 것이 아마 inheritance
일 것이다. 하지만, 지속적으로 inheritance 의 문제점이 지적되어 왔다. 대표적인 것이 이른바 고릴라 바나나 역설 이다.
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
이쯤 읽었으면 Composition 의 장점이 확실하게 드러났을 것이라고 생각한다. 그렇다면 언제 Composition 을 사용해야 할까? 많은 개발자들이 이 질문에 항상
이라고 대답하고 있다. Pure Function 으로 작성한 walker
, talker
는 부작용도 없고, 테스트 코드를 작성하기에도 좋다. 모든 상속된 객체가 같은 특성을 공유할 이유도 그럴 필요도 없다. javascript 에서 구현이 안되는 private property 를 객체 지향 하에 손쉽게 구현할 수 있다.
뭔가 더 글을 남겨야 될것 같은 기분이 들지만 여기서 줄인다. composition 이라는게 그만큼 간단한 것이니까.