[js] 클래스 기반 언어 vs 자바스크립트 1/3

HTML코더나 디자이너 출신들이 자바스크립트를 사용할 때는 프로그래밍적인 개념이 부족하기 때문에 적당한(?) 수준까지만 언어를 사용하게 됩니다. 하지만 이런 경우 처음 배우는 언어가 자바스크립트라 별 위화감 없이 그게 당연한가 보다 싶은 형태로 언어를 받아들입니다.
하지만 복잡한 구조를 가진 대규모 자바스크립트를 개발하기 위해서는 역시 기존의 개발자를 불러들이게 됩니다. 기존의 개발자란 구체적으로 말해 c, c++, java, c# 언어를 사용하는 개발자입니다.

이 언어를 사용하는 개발자들의 특징은 정의하고 찍어낸다라는 개념에 매우 익숙하다는 것입니다. 먼저 무언가를 정의하고 정의한 것을 바탕으로 만들어내는 거죠. 이러한 형태는 넓은 범주로 보면 형식 정의 언어로 볼 수 있습니다. 자바스크립트도 물론 무언가를 사용하려면 정의를 해야 하지만 앞의 언어들과 큰 차이가 있습니다. 앞의 언어들은 실제로 사용할 대상이 어떤 존재인가를 정의합니다. 자바스크립트의 경우는 실제 사용할 대상에게 정의를 합니다. 이 미묘한 차이가 언어 사용 시 완전히 다른 양상으로 나타나게 되어 기존 개발자에게 어려운 언어가 됩니다. 클래스를 사용하지 않는 자바스크립트의 특징을 차근차근 살펴보도록 하죠.

이 주제는 꽤나 방대하기 때문에 총 3회에 걸쳐서 알아볼 예정입니다. 이 번 포스트에서는 기초적인 개념과 자바스크립트에서의 new에 대한 의미까지를 고찰해보죠.

클래스의 정의와 인스턴스

클래스 기반의 언어에서는 어떤 일을 시킬 실질적인 객체를 만들기 위해 먼저 그 틀이 되는 클래스를 제작합니다.

프로그램의 흐름이나 구조를 간단히 나누자면

  1. 클래스를 정의하는 곳과
  2. 그 클래스를 이용하여 인스턴스를 생성한 뒤,
  3. 인스턴스를 통해 실제 액션을 실현하는 곳으로 나눌 수 있습니다.

예를 들어 alert이란 클래스를 생각해보죠.

class Alert{

    private String _msg;

    public alert( String $msg ){
        _msg = $msg;
    }

    public void action(){
        System.out.print( _msg );
    }
}

위의 간단한 클래스는 인스턴스를 만들 때 표시할 문구를 넘겨주고 인스턴스의 action을 호출함으로써 그 문자열을 표시하게 됩니다. 바로 이 부분까지 위에서 언급한 클래스를 정의하는 곳입니다. 이제 인스턴스를 생성해 실제로 사용하는 쪽의 코드를 보죠.

Alert a = new Alert( "test" );
a.action(); // test출력

이 과정이 바로 2번에 해당되는 인스턴스를 만들고 실제로 액션을 실현하는 곳입니다. 이러한 코드를 호스트 코드라고 합니다.

이 간단한 예제에서 주는 교훈을 생각해보면 다음과 같습니다.

  1. 실제로 필요한 작업을 처리하는 곳은 인스턴스를 만들어 사용하는 호스트코드다.
  2. 인스턴스가 어떤 일을 할지 설계하여 정의하는 곳이 클래스다.
  3. 따라서 하고 싶은 일이 무엇인지를 미리 생각해보고 예측하여 준비하는 것이 클래스다.

간단히 말해 클래스의 가치는 인스턴스가 실제 일을 할 때 어떤 능력을 갖고 있냐에 달려있는 것이지, 클래스 자체가 의미를 갖고 있는 것이 아닙니다.
하지만 초창기에는 클래스를 정교하게 설계하고 관계를 정의함으로써 인스턴스가 어떻게 작동할지를 미리 다 예측하고 인스턴스 간의 알고리즘도 통제할 수 있다고 믿었습니다.

물론 가능한 사람도 있죠. 근데 이게 무지하게 복잡하고 실수하기 쉽습니다. 인스턴스를 만들고 나서 어떻게 사용할지는 사실 그 시점의 개발자 마음입니다.
근데 그걸 모두 예측하여 정해진 대로만 쓰게 만들거나 그렇게 해야만 정상적으로 작동하는 클래스라는 건 너무 망가지기 쉬운 구조입니다.
현대 애자일 개발론에서는 바로 이 점에 초점을 두고 있습니다. 즉 먼저 인스턴스를 사용하는 측의 코드를 미리 짜고 거기에 부합하도록 클래스를 수정해가는 방식이죠. 충분히 인스턴스를 사용하는 코드를 검토해가며 거기에 맞춰 지속적으로 클래스를 수정해가는 형태로 진행하여 클래스가 현실과 동떨어지지 않고 사용하는 방식이나 목적과 부합하도록 고쳐가는 것입니다.

뭐 여기까지 적용하고 있다고 해도 결국 클래스 언어의 경우 아래와 같은 개발 원칙이 바뀌지 않습니다.

  1. 실제로 작업을 하려면 반드시 클래스를 정의해야 한다
  2. 인스턴스는 오직 클래스가 정의된 범위 내에서만 작동할 수 있다

즉 클래스 기반 언어를 사용하는 개발자가 몸에 들인 습관은 호스트코드를 작성하려면 클래스를 정의해야 한다 입니다. 바로 이 점이 매우 자바스크립트를 이해하기 어렵게 만듭니다.

 

인스턴스 밖에 없는 자바스크립트

중요한 포인트는 자바스크립트에 클래스가 없다는 점입니다. 즉 어떤 인스턴스를 만들기 위한 클래스는 존재하지 않습니다.
설령 자바와 비슷한 문법을 보거나 new를 보셔도 클래스와는 무관하다는 사실을 항상 기억해야 합니다.
상당히 과격해 보이는 이 개념은 사실 클래스 언어가 어떻게 작동하는지 이해한다면 그리 생소하지 않을 수도 있습니다.
이를 이해하기 위해서 메서드의 정체를 확인할 필요가 있습니다. 메서드란 특정 인스턴스의 값만 처리하는 전용 함수 정도로 이해할 수 있습니다. 클래스가 없는 언어에서 그 개념을 강제적으로 구현해보죠.

var instance1 = {x:10, y:10};

function move( $instance, $x, $y ){
    $instance.x = $x;
    $instance.y = $y;
}

move( instance1, 50, 50 );

이렇게 하면 move라는 함수를 이용해 instance1의 값을 바꾸게 되는데 대략 메서드와 비슷한 구조가 됩니다. 사실 초창기 c++은 일종의 매크로였는데 클래스 구문을 위와 같이 번역해줬습니다. 만약 위의 예에서 move가 받는 첫 번째 인자를 대체할 수 있다면 보다 객체지향적인 메서드로 보일 것 입니다.

var instance1 = {x:10, y:10};

function move( $x, $y ){
    instance1.x = $x;
    instance1.y = $y;
}

move( 50, 50 );

머 이 경우도 잘 작동하긴 하지만 이게 잘 작동하는 이유는 좀 다릅니다. 이건 함수가 생성될 당시의 클로저에서 instance1을 잡아뒀기 때문입니다. 만약 이 방법을 사용해 첫 번째 인자를 없애려고 하면 모든 인스턴스 마다 함수 객체를 따로 만들어야 합니다. 그렇게 하면 너무 많은 함수가 만들어져 메모리가 감당할 수 없게 됩니다. 하나의 함수가 여러 개의 인스턴스에 적용되려면 함수 입장에선 자신이 이 번에 처리할 인스턴스가 누군지 알 수 있는 방법이 필요합니다. 이건 개발자가 따로 정의할 수도 있습니다. 예를 들어볼까요.

var current = null;
var instance1 = {x:10, y:10};
var instance2 = {x:50, y:50};

function move( $x, $y ){
    current.x = $x;
    current.y = $y;
}

current = instance1;
move( 100, 100 ); //instance1을 100,100으로 이동

current = instance2;
move( 10, 10 ); //instance2를 10, 10으로 이동

하지만 매번 current를 설정하고 함수를 호출하게 되어 인자로 넘길 때보다 더 불편해졌습니다. 만약 current같은 변수가 언어 수준에서 내장되어 있다면 편리하겠죠. 하지만 여전히 current를 설정해야 하는 불편함은 어떻게 해야 할까요?
객체지향 언어에서는 이러한 불편함을 일반적으로 특수한 연산자를 통해 표시합니다.

//java
instance1.move( 100, 100 );

//c++
instance2->move( 10, 10 );

자바스크립트를 만들어질 때부터 자바랑 비슷한 모양으로 보이게 만들라는 어명(?)이 있었습니다. 따라서 자바스크립트도 자바처럼 점 구문을 사용하여 current가 누구인지 알려주는 구문을 사용합니다. 문제는 자바스크립트는 클래스를 갖지 않기 때문에 점 구문을 사용해도 그게 존재하지 않는다는 것입니다.
따라서 인스턴스의 키에 직접 그 함수를 지정해줘야 합니다.

var instance1 = {x:10, y:10, set:move};

그럼 이제 점 구문으로 move를 호출할 수 있습니다.

instance1.set( 200, 200 );

근데 여전히 move함수 내부에서는 current를 사용하고 있으므로 실제 instance1x, y에는 아무런 변화가 일어나지 않습니다. 자바스크립트가 언어 수준에서 current를 대신해 지원하는 변수는 바로 this입니다. 이제 변경된 move를 포함하여 전체 소스를 보죠.

function move( $x, $y ){
    this.x = $x;
    this.y = $y;
}

var instance1 = {x:0, y:0, set:move};
instance1.set( 200, 200 );

마지막 호스트 코드를 보고 있자면 자바와 매우 비슷해서 마치 클래스 언어 같은 느낌이 들지만 다음과 같은 절대 같을 수 없는 차이가 있습니다.

  1. 클래스가 없다.
  2. move는 내부에서 this라는 변수를 사용하지만 여전히 instance1과 아무런 관계가 없는 독립적인 함수다.
  3. 인스턴스는 메서드 형태로 어떤 함수를 사용하려면 반드시 본인의 속성에 해당 함수를 특정 키와 함께 저장해야 한다.

위의 세 가지는 연결되어있으면서도 각각 매우 중요합니다. 먼저 클래스가 없다는 딱히 설명할 필요가 없습니다. 실제로 코드 상에 어떤 인스턴스를 찍어내는 클래스 같은 건 없으니까요. instance1은 그 자신을 직접 정의했지 어떤 클래스로부터 만들어지지 않았습니다.
두 번째는 객체지향 개발자들이 엄청나게 헷갈려 하는 부분인데 위의 코드에서 move가 독립적이란 점은 아래의 코드로 표현할 수 있습니다.

function move( $x, $y ){
    this.x = $x;
    this.y = $y;
}

var instance1 = {x:0, y:0, set:move};
instance1.set( 200, 200 );

var rect = {x:0, y:0, width:100, height:100, position:move};
rect.position( 100, 100 );

위의 코드에서 동일한 move함수를 instance1도 사용하지만 rect도 사용하고 있습니다. 객체지향 언어에서 클래스 내에 정의한 메서드는 오직 특정 클래스의 인스턴스와 관계되지만 자바스크립트는 처음부터 인스턴스와 함수가 연결되는 것이라 클래스라는 구속이 없습니다. 따라서 아무 객체와도 연결 지을 수 있습니다.
이 부분을 더 자세히 파고 들어 봅시다. 클래스 지향 언어에서는 클래스 측이 함수의 소유자가 되므로 언어의 전반적인 문법이 무조건 인스턴스나 클래스로부터 함수가 불리어지는 형식을 취하고 있습니다( instance.method() )
하지만 자바스크립트는 딱히 인스턴스가 함수를 소유하는 것이 아닙니다. 따라서 문법적으로 함수 측에서 인스턴스를 고를 수 있는 방법도 제공하고 있습니다.

function move( $x, $y ){
    this.x = $x;
    this.y = $y;
}

var instance1 = {x:0, y:0};
move.apply( instance1, [200, 200] );

var rect = {x:0, y:0, width:100, height:100};
move.call( rect, 100, 100 );

이러한 문법으로 applycall을 제공하는데 함수가 특정 인스턴스를 선택할 수 있도록 첫 번째 인자에 인스턴스를 지정해줍니다. 그럼 함수 내부의 this는 바로 그 객체를 가리키게 됩니다. apply의 경우 나머지 인자를 배열로 묶어서 제공해주고 call의 경우는 직접 하나씩 지정한다는 점 외엔 같습니다. call은 인자의 갯수가 확정적인 경우에 사용하고 apply는 가변적인 경우에 사용하도록 디자인되어있습니다. 이렇듯 자바스크립트에서는 점 구문처럼 인스턴스 입장에서 메서드를 호출하는 구문 외에도 함수 입장에서 인스턴스를 지정할 수 있는 문법도 제공하고 있습니다.

세 번째인 인스턴스의 메서드가 되려면 인스턴스에 저장해야 한다는 내용은 방금 위에 설명한 것과 연관이 깊습니다. instance1.move( 200, 200 ); 을 사용하려면 반드시 instace1move:move 라는 식으로 키, 값을 저장해야 합니다. 즉 메서드 구문으로 호출하기 위해 키, 값을 낭비하게 됩니다. 자바스크립트는 함수 입장에서 인스턴스를 지정할 수 있기 때문에 구지 인스턴스를 기준으로 하는 메서드 호출에 연연에 할 필요가 없습니다. 개발자는 이 점을 구분하여 꼭 필요한 경우가 아니면 함수 측에서 인스턴스를 고르게 하는 편이 더 좋습니다.
또한 함수형 언어가 아닌 객체 기반의 언어에서는 함수는 객체가 아닙니다. 따라서 객체의 소유물일 뿐입니다.
하지만 자바스크립트는 함수를 완전한 객체로 취급하기 때문에 함수 자체에도 속성과 메서드가 존재할 수 있습니다. 심지어 함수 그 자체도 오브젝트로부터 파생된 인스턴스입니다. 따라서 마치 오브젝트처럼 아무 키, 값을 할당할 수 있습니다.

function move( $x, $y ){
    this.x = $x;
    this.y = $y;
}

 //그냥 키 값을 할당하면 된다!
move.test = 3;

요점은 자바스크립트에서는 클래스가 없기 때문에 전 프로그램 수준에서 공통된 함수를 정의하고 필요한 인스턴스를 만들어 함수와 연결 짓거나 아니면 함수 측에서 해당 인스턴스를 연결하도록 작성하게 된다는 점입니다.

 

그럼 new는 뭐야?

new는 오브젝트의 인스턴스를 만들어내는 키워드입니다. new외에도 다양한 리터럴을 통해 인스턴스를 만들어낼 수 있습니다.

var i0 = new Object();
var i1 = {};
var i2 = [];
var i3 = function(){};
var i4 = /a/;

위에 열거한 다양한 리터럴 구문을 이용하면 new가 아니라도 인스턴스를 생성할 수 있습니다. 하지만 자바스크립트의 new 구문은 매우 특수합니다. 일견 자바의 new처럼 보일지 몰라도 의미가 전혀 다릅니다. 자바에서 new 구문의 의미는 다음과 같습니다.

  1. new 뒤에 지정된 클래스의 인스턴스를 만들고,
  2. 해당 클래스에 생성자 함수가 있는 경우 그 함수를 호출한다.

하지만 자바스크립트에서 new는 완전히 다릅니다.

  1. 일단 오브젝트를 하나 생성한 뒤
  2. new 뒤에 지정된 함수의 프로토타입을 방금 생성한 오브젝트의 __proto__에 참조로 잡아준다.
  3. 그런 후 방금 생성한 오브젝트를 new 뒤의 함수에게 컨텍스트로 전달하여 호출한다.

좀 어렵기 때문에 1~3까지의 내용을 매우 자세히 코드와 함께 알아보겠습니다. 먼저 환경을 만들어보죠. 마침 위에서 사용하던 move함수가 있으니 그걸 new 구문으로 오브젝트를 만듭니다.

function move( $x, $y ){
    this.x = $x;
    this.y = $y;
}

var instance = new move( 10, 10 );

위의 1번에 따르면 new를 하는 순간 일어난 일은 {}를 만든 것으로 이해할 수 있습니다. 따라서 위의 코드는 아래와 같은 상황입니다.

//var instance = new move( 10, 10 );

//1번 상황
var instance = {};

이제 2번을 재현할 차례인데 지정된 함수는 move이므로 move의 프로토타입을 옮겨주면 됩니다.

//var instance = new move( 10, 10 );

//1번 상황
var instance = {};
//2번 상황
instance.__proto__ = move.prototype;

마지막으로 3번을 재현합니다. move함수의 call을 통해 간단히 instance를 넘겨주면 됩니다.

//var instance = new move( 10, 10 );

//1번 상황
var instance = {};
//2번 상황
instance.__proto__ = move.prototype;
//3번 상황
move.call( instance, 10, 10 );

new 구문을 사용하면 바로 위와 같은 3단계가 자동으로 일어납니다. 따라서 new 구문을 사용하는 것이나 수동으로 저 3단계를 진행하는 것이나 결과는 동일합니다만 2가지 차이점이 있습니다.

  • new구문은 c컴파일러 수준에서 한 번에 처리하므로 훨씬 고속이다.
  • __proto__속성은 브라우저에 따라 접근할 수 없는 경우(IE)도 있으므로 애초에 불가능할 수 있다.

여기까지의  설명은 좀 복잡하고 이해하기 어렵지만 요점은 간단합니다.

  • 자바스크립트의 new 구문 뒤에 오는 것은 클래스가 아니라 함수다.
  • new를 통해 생성된 것은 오브젝트{}
  • 함수에 컨텍스트만 전달될 뿐 생성자 함수라는 개념은 없다.

함수는 그저 함수입니다. 함수 내부에 this키워드는 그저 전달된 컨텍스트를 가리킬 뿐이죠.

 

결론

  1. 자바스크립트에는 클래스가 없다.
  2. 함수 내부에서 this의 의미는 클래스 언어에서 말하는 인스턴스 자신이 아니라 함수에게 전달된 컨텍스트다.
  3. 클래스 언어는 인스턴스를 기준으로 메서드를 호출하는 문법만 존재하나, 자바스크립트는 함수에게 인스턴스를 전달하는 방법도 존재한다.
  4. new는 근본적으로 오브젝트를 만들어낸다.
  5. new 뒤에 오는 것은 클래스가 아니라 그저 함수다.
  6. new로 생성한 오브젝트는 new에 전달된 함수의 prototype이라는 속성을 자신의 __proto__에 참조로 잡게 된다.
  7. new가 실행되면 3단계(오브젝트생성, prototype할당, 함수호출)의 순차적인 작업이 일어난다.