자바스크립트 Class 와 편의문법, 그리고 믹스인(Mixins) 기법

목이·2019년 12월 14일
13

기법

목록 보기
2/2
post-thumbnail

자바스크립트 Class 에 관한 오해

자바스크립트는 ES2015(ES6) 부터 클래스 문법이 도입되었습니다. class 키워드의 등장은 코드 작성자 입장에서 아주 반가운 일이지만, 실은 클래스처럼 동작하게끔 만들어주는 편의문법(syntactical sugar)이라는 점에 유의해야 합니다. 내부적으로 기존 prototype 기반의 상속구조 변환되기에 사용자가 작성한 클래스는 결국 new 키워드와 함께 호출되기를 기다리는 자바스크립트 함수(function)로 바뀌게 됩니다.

JavaScript classes, introduced in ECMAScript 2015, are primarily syntactical sugar over JavaScript's existing prototype-based inheritance. The class syntax does not introduce a new object-oriented inheritance model to JavaScript. MDN - Classes

하지만, Syntactical Sugar 라는 표현으로만 폄하되기에는 새롭게 추가된 클래스 문법은 많은 장점을 가져다 줍니다. 자바스크립트를 작성하는 모든 이들에게 통용될 수 있는 공식 표준문법의 등장은 다소 괴팍한(?) 형태를 지녔던 기존 prototype 기반의 상속모델보다 훨씬 깔끔하고 명확하게 객체 상속구조를 정의할 수 있게 해줍니다. 메서드안에서 this 키워드는 항상 메서드를 담은 클래스의 인스턴스를 참조한다는 점도 코드 작성자에게 매우 유용한 변화입니다.

자바스크립트 Class 선언과 상속

ES2015 클래스 문법이 제공하는 classextends 키워드를 이용하면 클래스 계통을 명확하게 표현할 수 있습니다.

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

class Duck extends Animal {
	flyTo(destination){
    	console.log(`${this.name} is flying to the ${destination}`);
    }

    eat(food){
    	console.log(`${this.name} is eating ${food}`);
    }

    swimAt(place){
    	console.log(`${this.name} is swiming at the ${place}`)
    }
};

const duck = new Duck('Donald duck');
duck.flyTo('home');   // Donald duck is flying to the home
duck.eat('fish');     // Donald duck is eating fish
duck.swimAt('river'); // Donald duck is swiming at the river

부모(Animal)에서 유래된 Duck 클래스는 어딘가로 날 수 있고(flyTo), 먹고(eat), 헤엄칠 수 있습니다(swimAt). 새롭게 추가된 클래스 문법에서는 생성자(constructor)* 를 제외할 수 있습니다. 이 경우 상부 클래스의 생성자가 호출됩니다. new 키워드를 통해 생성된 Duck 의 인스턴스는 부모 생성자 호출을 통해 'Donald duck' 이라는 name 속성값을 지니게됩니다.

생성자를 명시적으로 선언을 하고싶다면, *전개연산자(Spread Operator) 구문을 이용하여 부모에 전달할 파라미터를 아래와 같이 편리하게 작성할 수 있습니다.

class Duck extends Animal {
	// 전개(...) 연산자
	constructor(...args){
    	super(...args);
    }
}

이제 새로운 Swan 클래스를 설계에 추가해봅시다. Swan 은 Animal 에서 유래되었고, Duck 처럼 flyTo, eat, swimAt 행위를 할 수 있습니다. 하지만 Duck 이 그랬듯이 Swan 도 세가지 행위에 관한 메서드들을 자신이 직접 담고 있어야 합니다. 이는 합리적으로 보이지 않습니다. 동일한 행위를 하는 메서드가 클래스별로 선언되야 한다면 추후 행위에 변경작업이 필요할 때, 이를 담고있는 모든 클래스를 수정해야합니다.

상속구조가 갖는 미덕은 코드의 재사용입니다. 세가지 행위들을 상부 클래스로 옮기고 하부 클래스는 이를 자동으로 물려받아 사용하면 됩니다.

class Animal {
	constructor(name){
    	this.name = name;
    }
  	
	// 부모로 이동된 flyTo, eat, swimAt 메서드
	flyTo(destination){
    	console.log(`${this.name} is flying to the ${destination}`);
    }

    eat(food){
    	console.log(`${this.name} is eating ${food}`);
    }

    swimAt(place){
    	console.log(`${this.name} is swiming at the ${place}`)
    }
}

class Duck extends Animal {};
class Swan extends Animal {};

부모 클래스에 책임과 기능이 누적되었지만, 이제 Animal 에서 유래된 객체들은 직접 메서드를 구현할 필요가 없이 부모의 것을 물려받아 사용하면 됩니다. 앞으로 동일한 행위를 하는 수백, 수천개의 새로운 객체를 추가하는데 드는 비용은 거의 공짜인셈입니다.

모든게 완벽해 보입니다. 이 녀석이 등장하기 전까지는 말이죠.

클래스 상속구조의 붕괴

미키 마우스를 생성하려면 Animal 에서 파생된 Mouse 클래스가 필요합니다. Mouse 는 Animal 의 메서드를 상속받기에 먹고(eat), 헤엄치는 행위(swimAt)를 할 수 있습니다. 문제는 상부 클래스로부터 하늘을 나는 행위(flyTo)도 고스란히 물려받는다는 점입니다.

미키 마우스는 하늘을 날 수도 없고, 날아서도 안됩니다. 설계 논리가 붕괴되는 시점입니다. 이를 해결하고 클래스 계통을 바로 세우려면, Animal 로 부터 flyTo 행위를 제거하고 이를 사용하던 기존의 모든 클래스에 하나씩 붙여나가는 작업을 해야합니다. 잘못된 상속구조 설계로 인해 이제 값비싼 비용을 지불하게 되었습니다.

이는 과연 설계자의 잘못일까요? 클래스 계통을 설계하는 과정에서 미래에 닥쳐올 변화를 감지하지 못한 것은 아쉽지만 쉽지 않은 일입니다. 비싼 비용을 지불하고 클래스 수정작업을 일일이 마친 후에야, 외부정책의 변화로 하늘을 나는 마이티 마우스를 만들어달라는 요구사항을 전달받을지 모릅니다.

코드 재사용을 목적으로 상부 클래스 계통이 지나치게 많은 책임과 기능을 부여하게되면 미래의 변화에 대응할 유연성을 잃게 됩니다. 변화에 대응하려면 전체 클래스 계통의 영향도를 고려해야하는데 이는 굉장히 고통스러운 작업입니다.

만약 행위(기능)들이 클래스 계통에서 분리되어 있다면 어떨까요? 새로운 행위가 추가되고 수정되야 할 때 전체 클래스 계통을 살펴볼 필요가 없어집니다.

행위의 분리 - 믹스인(Mixins)

필요한 행위를 클래스 계통과 상관없이 독립된 클래스로부터 얻어와 행위를 탑재할 수 있다면 훨씬 유연한 코드 재사용 패턴이 됩니다. 소프트웨어 업계의 경험많은 선배들은 비슷한 문제를 이미 경험했고 후배 세대들을 위해 해결책을 남겨놓았습니다. 그들은 이를 믹스인(Mixins) 이라 불렀습니다.

믹스인은 특정 기능(행위)만을 담당하는 클래스로, 단독 사용(Stand-alone use)이 아닌 다른 클래스에 탑재되어 사용될 목적으로 작성된 (조각) 클래스를 의미합니다.

믹스인 클래스를 다른 클래스에 담는 방법은 프로그래밍 언어별로 다양합니다. 컴파일 언어와 인터프리터 언어는 믹스인 적용방식에 있어서 구조적으로 큰 차이를 보입니다. 같은 언어라 할지라도 사용하는 객체와 선호하는 구문에 따라 다양한 기법이 만들어지게 됩니다. 자바스크립트의 경우, 클래스 문법을 이용할 수도 있고, 기존 prototype 기반 모델을 이용할 수도 있으며, Object.assign 를 이용해 객체에 직접 행위를 붙이는 작업을 할 수도 있습니다.

믹스인(Mixins) 구현하기

ES2015에 새롭게 도입된 클래스 문법을 이용하면 믹스인 클래스들을 손쉽게 정의할 수 있습니다.

// 나는 행위를 담당하는 Mixin
const FlyToMixin = (superclass) => class extends superclass {
	flyTo(destination){
        console.log(`${this.name} is flying to the ${destination}`);
    }
}

// 먹는 행위를 담당하는 Mixin
const EatMixin = (superclass) => class extends superclass {
	eat(food){
        console.log(`${this.name} is eating ${food}`);
    }
}

// 헤엄치는 행위를 담당하는 Mixin
const SwimAtMixin = (superclass) => class extends superclass {
	swimAt(place){
        console.log(`${this.name} is swiming at the ${place}`)
    }
}

Animal 에서 유래된 Mouse 클래스는 위의 믹스인 클래스들 중에서 필요한 행위를 가져와 탑재할 수 있습니다.

// 믹스인을 탑재한 Mouse
class Mouse extends SwimAtMixin(EatMixin(Animal)) { /*...*/ }

const mickyMouse = new Mouse('Micky Mouse');
mickyMouse.swimAt('river');

이제 Mouse 클래스에서 새로운 행위를 탑재하거나 빼야할 때 클래스 상속구조에 아무런 영향을 끼치지 않게 되었습니다. 하늘을 나는 마이티 마우스를 추가하는 것도 너무 쉬운일이 되버렸습니다. 마이티 마우스는 어느 클래스 계통에서 유래되었냐를 따질 필요가 없어졌으니까요. 단지 하늘을 나는 믹스인을 붙여주기만 하면 됩니다.

class MightyMouse extends FlyToMixin(Mouse){ /*...*/ };

맺음말

GOF의 디자인패턴 에서 얘기했듯이, 유연하고 재사용 가능한 설계를 처음부터 정확하게 하기는 불가능하거나 매우 어려운 일입니다. 위대한 선배들 역시 이미 만들어진 설계를 여러번에 걸쳐서 재사용해보려고 시도했고, 그때마다 재설계의 노력이 필요하다는 사실을 경험했습니다.

어쩌면 최소한의 수정을 통해 다시 사용할 수 있는 설계가 가장 훌륭한 설계라고 볼 수 있습니다. 믹스인은 설계의 유연성을 가져다 주는 중요한 기법 중 하나입니다.

이 글에 담지 못했던 몇가지 참고사항들을 정리해봅니다.

  1. 믹스인이 많아질 수록 클래스 선언부에 기술해야하는 양이 많아집니다. 이때 lodash 의 compose 기능을 이용하면 편리하게 믹스인을 조합할 수 있습니다. Class Composition in JavaScript
import compose from 'lodash/fp/compose';

const behaviors = compose(FlyToMixin, EatMixin, SwimAtMixin)(Animal);
class Duck extends behaviors {/*...*/}
  1. 믹스인을 활용해 장식자(Decorator) 패턴을 구현할 수 있습니다. 실제 필드에서 장식자 패턴을 적용해보며 느낀 점을 정리한 글입니다. Javascript 장식자(decorator) 패턴
profile
코딩꾼

3개의 댓글

comment-user-thumbnail
2019년 12월 14일

계속 올려주세요!

답글 달기
comment-user-thumbnail
2020년 3월 28일

아 감사합니다. 너무 쉽게 설명해주셔서 읽고나니 이해가 되네요.

1개의 답글