이번 포스팅에서는 자바스크립트 클래스에 대해서 정리해 보려고 한다.
클래스 이론
클래스와 상속은 특정 형태의 코드와 구조를 형성하며 실생활 영역의 문제를 소프트웨어로 모델링 하기 위한 방법이다. 객체 지향 또는 클래스 지향 프로그래밍에서 데이터는 자신을 기반으로 하는 실행되는 작동(behavior)과 연관되므로 데이터와 작동을 함께 잘 감싸는(캡슐화) 것이 올바른 설계라고 강조한다.
예를 들어, 일련의 문자들은 보통 String이라는 단어를 나타낸다. 여기서 데이터는 문자들인데, 우리는 이 데이터를 가지고 원하는 작업을 하는 것이 관심사이므로 데이터에 적용 가능한 작동들을 모두 String 클래스의 메서드로 설계한다. 그래서 어떤 문자열이 주어지더라도 데이터와 작동이 잘 포장된 String 클래스의 인스턴스로 나타낼 수 있다.
클래스는 특정 자료 구조를 분류(classify) 하는 용도로 쓴다. 즉, 일반적인 기준 정의에서 세부적이고 구체적인 변형으로서의 자료 구조를 도출하는 것이다. 자동차(Car)와 탈것(Vehicle) 클래스를 예를 들어서 설명을 하면, 매번 '사람을 운송하는 기능'을 재정의 한다면 이는 올바른 설계가 아니다. 이 때 Vehicle을 정의해 두고 Car는 Vehicle에 있는 기반 정의를 상속 받아서 정의를 한다. Car가 Vehicle을 세분화한 셈이다.
다형성(polymorphism)은 또 다른 클래스의 핵심 계념으로 부모 클래스에 뭉뚱그려 정의된 작동을 자식 클래스에서 좀 더 구체화 하여 오버라이드(재정의)하는 것을 뜻한다. 클래스 이론에서는 어떤 작동이 담긴 메서드의 이름을 부모와 자식 클래스 모두 똑같이 공유하여 자식 클래스 메서드가 부모 클래스 메서드를 다르게 오버라이드하라고 권장한다. 하지만 자바스크립트에서는 이 방법은 권장하지 않는다.
자바스크립트는 클래스가 있는 것인가? ES6에서는 class 키워드가 명세에 추가되었다. 하지만 이 질문에 대한 답은 '아니다'이다. 클래스는 디자인 패턴이므로 자바스크립트를 통해 고전적인 클래스 기능과 얼추 비슷하게 구현할 수 있는 것은 사실이다. 그 동안 자바스크립트는 적어도 외관상으로 클래스처럼 비슷하게 생긴 구문을 제공하여 클래스 디자인 패턴을 실현하려는 욕구를 충족시키고자 많은 변신을 해 왔다. 그러나 클래스처럼 보이는 구문일 뿐이며 개발자들이 클래스 디자인 패턴으로 코딩할 수 있도록 자바스크립트 체계를 억지로 고친 것에 불과하다. 자바스크립트 클래스에 대해 가지고 있는 잘못된 생각들을 천천히 바로잡아 보고자 한다.
클래스 체계
건축으로 비유를 하면 클래스는 건축물의 청사진이고 인스턴스는 그 청사진을 보고 만든 건축물이라고 이해를 하면 쉽다. 건물과 청사진의 관계는 간접적이다. 청사진은 건축을 위한 계획일 뿐, 청사진만으로 사람이 들어와서 앉고 할 건물을 만들어낼 수는 없다. 그래서 실제로 건물을 올리는 작업은 시공사에 의뢰해야 하는데, 일감을 수주받은 시공사는 정확히 청사진에 따라 건물을 짓는다. 사실 청사진을 작성한 아키텍트가 의도했던 특성 그대로 물리적인 건물을 복사하는 셈이다. 건물을 어떻게 지어야 할지 청사진을 보고 자세히 검토해야 하지만 건물을 다 짓고 직접 살펴봐야 알 수 있는 부분에 대해선 부족함을 느낄 수 밖에 없다.
클래스는 복사 과정을 거쳐 객체 형태로 인스턴스화한다. 인스턴스는 보통 클래스명과 같은 이름의 생성자라는 특별한 메서드로 생성한다. 생성자의 임무는 인스턴스에 필요한 정보를 초기화하는 일이다. 생성자는 클래스에 속한 메서드로, 클래스명과 같게 명명하는 것이 일반적이다. 그리고 새로운 클래스 인스턴스를 생성할 거라는 신호를 엔진이 인지할 수 있도록 항상 new 키워드를 앞에 붙여 생성자를 호출한다.
클래스 상속
자식 클래스는 부모 클래스에서 완전히 떨어진 별개의 클래스로 정의된다. 부모로부터 복사된 초기 버전의 작동을 고스란히 간직하고 있지만 물려받은 작동을 전혀 새로운 방식으로 오버라이드 할 수 있다. 자식 클래스가 부모 클래스로부터 상속받은 메서드를 같은 이름으로 오버라이드해서 자식 클래스에서 사용하는 기법을 다형성이라고 한다.
상속 관점에서 자바스크립트의 유일한 생성자는 객체뿐이다. 각각의 객체는 [[Prototype]]이라는 은닉(private) 속성을 가지는데 자신의 프로토타입이 되는 다른 객체를 가리킨다. 그 객체의 프로토타입 또한 프로토타입을 가지고 있고 이것이 반복되다가, 결국 null을 프로토타입으로 가지는 오브젝트에서 끝난다. null은 더이상 프로토타입이 없다고 정의되며, 프로토타입 체인의 종점 역할을 한다. 프로토타입에 대해서는 JS #5 프로토타입 포스팅에서 좀 더 자세히 다루도록 한다.
일부 클래스 지향 언어에서는 여러 부모 클래스에서 상속받을 수가 있다. 이를 다중 상속(multiple inheritance)이라고 하는데, 부모 클래스 각각의 정의가 자식 클래스에 복사된다는 의미이다. 하지만 자바스크립트에서는 다중 상속을 지원하지 않는다.
믹스인
자바스크립트 객체는 상속받거나 인스턴스화해도 자동으로 복사 작업이 일어나지는 않는다. 쉽게 말하면 자바스크립트엔 인스턴스로 만들 '클래스'라는 개념 자체가 없고 오직 객체만 있다. 그리고 객체는 다른 객체에 복사되는 것이 아니라 서로 연결된다. 믹스인(Mixin)은 다른 언어와 달리 자바스크립트에선 누락된 클래스 기능을 복사 기능으로 흉내낸 것으로, 명시적 믹스인과 암시적 믹스인 두 타입이 있다.
Vehicle/Car 예제를 보면, 자바스크립트 엔진은 Vehicle의 작동을 Car로 알아서 복사하지 않으므로 일일이 수동으로 복사하는 유틸리티를 대신 작성하면 된다. 이러한 유틸리티를 여러 자바스크립트 라이브러리와 프레임워크에서는 extend()라고 명명하는데, 여기서는 mixin()이라고 한다.
Car에는 Vehicle에서 복사한 프로퍼티와 함수 사본이 있다. 엄밀히 본다면 함수가 복사된 것이 아니라 원본 함수를 가리키는 레퍼런스만 복사된 것이다. 따라서 Car에는 ignition() 함수의 사본 레퍼런스인 ignition 프로퍼티와 Vehicle에서 복사한 1이란 값이 할당된 engines 프로퍼티가 있다. Car에는 이미 자체 drive 프로퍼티(함수)가 있으므로 이 프로퍼티 레퍼런스는 오버라이드 되지 않는다.
Vehicle.drive.call( this )와 같은 코드를 명시적 의사다형성(Explicit Pseudopolymorphism)이라 부른다. 자바스크립트는 ES6 이전에는 상대적 다형성을 제공하지 않는다. 따라서 drive라는 이름의 함수가 Vehicle과 Car 양쪽에 모두 있을 때 이 둘을 구별해서 호출하려면 절대적인 레퍼런스를 이용할 수 밖에 없고 그래서 명시적으로 Vehicle 객체의 이름을 지정하여 drive() 함수를 호출한 것이다. 하지만 Vehicle.drive()로 함수를 호출하면 this는 Car 객체가 아닌 Vehicle 객체와 바인딩되는데(this에 대한 모든것 포스팅 참고) 이는 애초에 의도했던 바가 아니다. 그래서 불가피하게 .call( this )를 덧붙여 drive()를 Car 객체의 콘텍스트로 실행하도록 강제한 것이다.
복사가 끝나면 Car는 Vehicle과 별개로 움직인다. Car에 프로퍼티를 추가해도 Vehicle엔 아무런 영향이 없고 그 반대 역시 마찬가지다. 공용 함수의 레퍼런스는 두 객체 모두 같이 쓰기 때문에 수동으로 객체 간에 함수를 일일이 복사(믹스인) 하더라도 다른 클래스 지향 언어처럼 100% [클래스 -> 인스턴스]의 복사는 어렵다. 사실 자바스크립트 함수는 (표준적인 방법으로 확실하게) 복사할 수는 없다. 복사되는 건 같은 공유 함수 객체를 가리키는 사본 레퍼런스(Duplicated Reference)이다.
명시적 믹스인은 코드 가독성에 도움이 될 때만 조심하여 사용하되 점점 코드가 추적하기 어려워지거나 불필요하고 난해한 객체 간 의존 관계가 양산될 기미가 보이면 사용을 중단하는 편이 좋다.
암시적 믹스인은 앞서 설명한 명시적 의사다형성과 밀접한 관계가 있으므로 사용할 때 주의해야 한다.
가장 일반적인 생성자 호출 또는 메서드 호출 시 Something.cool.call( this )를 하면 Something.cool() 함수를 본질적으로 '빌려와서' Another 콘텍스트로 호출한다. 결국 Something.cool()의 할당은 Something이 아닌 Another다. 따라서 Something의 작동을 Another와 섞은 셈이다.
this 재바인딩을 십분 활용한 이런 유형의 테크닉은 Something.cool.call( this ) 같은 호출이 상대적 레퍼런스가 되지 않아 불안정하므로 사용할 때 신중히 처리해야 한다. 대부분은 깔끔하고 관리하기 쉬운 코드를 유지하기 위해 쓰지 않는 편이 좋다.
정리하기
- 클래스는 디자인 패턴의 일종이다. 많은 언어에서 클래스 지향 소프트웨어 디자인이 가능한 구문을 처음부터 제공하는데, 자바스크립트에도 역시 유사한 구문이 있다. 그러나 자바스크립트에서 클래스의 의미는 다른 언어들과 다르다.
- 클래스는 복사를 의미한다. 전통적인 클래스는 인스턴스화하면 [클래스 -> 인스턴스]로 복사가 일어난다. 클래스를 상속하면 역시 [부모 -> 자식] 방향으로 복사된다. 다형성(같은 이름을 가진 복수의 함수가 상속 연쇄의 여러 수준에 존재)은 얼핏 보면 [자식 -> 부모] 방향의 상대적 레퍼런스가 아닐까 싶지만 그냥 복사 작업의 결과물일 뿐이다.
- 자바스크립트는 (클래스의 취지에 맞게) 객체 간 사본을 자동으로 생성하지 않는다. 믹스인 패턴(명시적/암시적)은 클래스의 복사 기능을 모방하기 위해 종종 쓰이지만 대부분 명시적 의사다형성처럼 보기 싫고 취약한 구문이 되어 가독성이 점점 더 떨어지고 유지보수도 어려운 코드가 된다.
- 명시적 믹스인은 클래스의 복사 기능과 같지 않다. 이는 객체(그리고 함수) 그 자체가 아니라 단지 공유된 레퍼런스만 복사하기 때문이다. 이런 부분에 유의하지 않으면 이상한 함정에 빠져 고생할 수 있다.
참고자료
- <YOU DON'T KNOW JS (this와 객체 프로토타입, 비동기와 성능)> 카일 심슨 저
- https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Inheritance_and_the_prototype_chain
'Prog. Langs & Tools > JavaScript' 카테고리의 다른 글
JS #6. 자바스크립트 작동 위임(behavior delegation) (0) | 2020.05.06 |
---|---|
JS #5. 자바스크립트 프로토타입(Prototype) (0) | 2020.04.27 |
JS #3. 자바스크립트 객체(Object) (0) | 2020.04.15 |
JS #2. this에 대한 모든 것(ES5, ES6) (0) | 2020.04.08 |
JS #1. ES6에서 import/export 올바르게 이해하고 사용하기 (2) | 2020.01.28 |