back

for in 과 객체의 enumerable 속성

5년 전 작성

막상 프로그래밍 블로그를 만들어 놓고 종종 글을 쓰려니 생각보다 귀차니즘이 크지만, 이대로두면 영영 적지 않고 잊어버릴 소재인 듯 싶어 생각난 김에 적어둡니다. 얼마나 많은 분들이 저와 같은 문제를 겪을지 모르겠지만, 일단 문제가 터지고나면 원인을 찾는데 상당한 시간을 허비할 수 있는 문제인만큼 node.js 카테고리의 첫 글을 이 녀석으로 장식하려고 합니다.

자바스크립트(JavaScript)의 가장 큰 장점(혹은 관점에 따라서는 단점?) 중 하나는 바로 프로토타입(prototype - 다른 언어에서는 보통 "원형"이라고 번역하는데 의미는 동일해도 다른 개념을 지칭하므로 "프로토타입"이라고 옮기겠습니다) 기반의 객체지향언어라는 점입니다. 이는 프로그래머가 필요에 따라 손쉽게 타입 확장을 통해 언어를 확장해 가는 것이 가능함을 의미합니다.

예를 들어, 이 바닥에서는 유명한 Douglas Crockford의 책 JavaScript: The Good Parts에 아래와 같은 예가 소개되어 있습니다.

Function.prototype.method = function (name, func) {
    this.prototype[name] = func;
    return this;
};

이 코드는 자바스크립트에서 생성자(constructor)로 쓰이는 함수의 프로토타입 객체(prototype object)에 method라는 이름의 메소드(method)를 추가하여 이후 다른 프로토타입 객체에 메소드 추가를 용이하게 해주는 코드입니다.

제가 작성하는 코드에서는 특정 객체(object)의 프로퍼티(property)를 순차로 방문해야 하는 경우가 많았습니다. 단, 이때 프로토타입 체인(prototype chain)을 통해 보이는 프로퍼티는 배제할 필요가 있었기 때문에 처음에는 책에서 추천한대로 아래와 같은 구조를 사용하기 시작했습니다.

for (key in object)
    if (object.hasOwnProperty(key))
        // object[key] 로 작업

하지만 이렇게 사용하는 구조가 슬슬 많아지기 시작하니 외관상으로도 뽀대나고 키보드 치는 횟수도 좀 줄일 수 있는 방법을 찾게 되었습니다. 그래서 과감하게 Object.prototypeforeach라는 이름의 메소드를 추가하게 됩니다.

Object.method('foreach', function (closure) {
    for (key in this)
        if (this.hasOwnProperty(key))
            closure.call(this, key);
});

그리고 사용은 뽀대나게 이렇게...

objet.foreach(function (key) {
    // this[key] 로 작업
});

그리고 단순히 객체의 프로퍼티 값을 읽거나 수정하는 것보다 복잡한 작업을 위해서 초기치를 받아 각 단계마다 필요에 따라 가공할 수 있는 구조로 개선했습니다.

Object.method('foreach', function (closure, memo) {
    for (key in this)
        if (this.hasOwnProperty(key))
            memo = closure.call(this, key, memo);
    return memo;
});

예를 들어, 각 속성의 값을 모아서 배열을 만들고 싶으면 이렇게...

result = object.foreach(function (key, memo) {
    memo.push(this[key]);
    return memo;
}, []);

자바스크립트에서는 매개변수와 인자 개수가 맞지 않아도 그만이기에 memo에 줄 인자를 생략하는 것은 지극히 정상적인 코드이고, 초기치나 단계별 결과가 필요 없는 경우에는 이전처럼 사용해도 정상 동작하기에 나름 괜찮은 구조였습니다.

그러다 어딘가에서 for in 구조에 대한 잘못된 성능 이야기를 듣고 와서는 hasOwnProperty() 대신 Object.keys()를 사용하는 구조로 변경했습니다.

Object.method('foreach', function (closure, memo) {
    var i,
        keys = Object.keys(this);

    for (i = 0; i < keys.length; i++)
        memo = closure.call(this, keys[i], memo);

    return memo;
});

물론 node.js 프로그래밍을 할 때는 underscore.js에서 _.each()라는 메소드를 지원하기는 합니다. 하지만, underscore.js는 클라이언트(client)인 브라우저(browser) 상에서 사용하고자 할 때는 덩치가 좀 되는 편이기에 필요한 부분만 간단히 작성해 Object.prototype같은 프로토타입 객체에 추가함으로써 언어를 확장해 사용하고자 하는 것이 의도였습니다. 그리고 그 패턴이 그대로 서버 쪽의 node.js 프로그래밍까지 왔습니다 - 다시 말해, 코드 재사용(code reuse)라는 이름 아래 다시 만들기 귀찮다는 이유로 공통적인 부분은 그대로 복사 & 붙여넣기 했다는 뜻입니다.

그러다 객체의 얕은 복사본(shallow copy)을 만들거나 두 객체를 합하는 등의 필요가 있어 underscore.js_.clone()이나 _.extend() 등을 사용하니 어이없게도 제가 Object.prototype에 넣었던 메소드가 프로토타입 체인이 아닌 원래 객체의 속성인 것처럼 복사되는 일이 발생했습니다 - 제가 구현한 foreach와는 다르게 프로토타입 체인을 배제하지 않은 것입니다. 그래서 그렇게 필요한 애들을 foreach를 사용해 하나하나 새로 구현해 사용하게 됩니다(만 underscore.js에 대한 불만은 충분히 생겼지요...).

그렇게 큰 문제 없이 잘 진행되는 듯 싶다가 아래와 같은 문제가 생기게 됩니다.

  • passport.js에서 OAuth 인증이 오동작합니다.
  • node-mysql에서 SQL 쿼리문에서 ? 문자를 객체 내용으로 바꿔주는 부분이 오동작합니다.

passport.js와 관련된 문제는 오류 메시지로 검색할 경우 나오는 모든 경우를 다 살펴보았지만 해결되지 않아 코드의 모든 부분을 주석 처리해가며 찾은 원인이 Object.prototype에 메소드를 추가하면 오동작함을 확인하였고, node-mysql 관련 문제 역시 비슷한 방법으로 Array.prototype에 메소드를 추가하면 오동작함을 확인하였습니다 - 무려 반나절 이상을 투자한... 눈물이...

그래서 "node.js에서는 언어의 기본 생성자의 프로토타입 객체에 메소드를 추가하는 것이 추천되는 방법이 아닌가?"라는 의문으로 Google+의 node.js 커뮤니티에 올렸고 - 사실, 올릴 때만 해도 별다른 방법은 없지 않을까 하는 생각이었습니다 - 의외의 방법을 하나 건져서 공유합니다.

for in을 통해 객체의 속성이 보이는지 여부를 enumerable 속성이라고 하는데, ECMAScript5에 새로 추가된 Object.defineProperty()를 통해 이를 제어하는 것이 가능합니다.

Function.prototype.method = function (name, func) {
    this.prototype[name] = func;
    Object.defineProperty(this.prototype, name, { enumerable: false });
    return this;
};

이렇게 프로토타입 객체에 메소드를 추가하는 함수를 수정하고 이 method 자체의 속성도 변경하여

Object.defineProperty(Function.prototype, 'method', { enumerable: false });

method는 물론 이를 통해 추가된 모든 메소드가 원치 않게 for in으로 보이고 다른 부분에 영향을 주는 것을 막을 수 있습니다. 이를 통해 앞서 열거한 문제(underscrore.js, passport.js, node-mysql)가 해결되었음은 당연하지요.

첫 글부터 너무 달렸네요. 다음 글은 좀 흥미로운 주제가 되길 바라며 마칩니다.