JavaScript의 privileged 메서드가 끔찍한 이유

예제 코드가 약간의 오해의 소지를 가지고 있어 조금 수정했습니다.
– 2014년 6월 29일 –

요즘 가장 뜨거운 관심을 받고있는 프로그래밍 언어인 JavaScript는 정보 보호(또는 은닉, information hiding)를 언어 스펙 차원에서 지원하지 않습니다. 이는 JavaScript는 진정한 OOPL(object-oriented programming language)이 아니라고 주장하는 사람들의 근거가 되기도 합니다. 여기에 대해 Douglas Crockford는 다음과 같이 변호합니다.

Some argue that JavaScript is not truly object oriented because it does not provide information hiding. That is, objects cannot have private variables and private methods: All members are public. But it turns out that JavaScript objects can have private variables and private methods. Of course, few understand this because JavaScript is the world’s most misunderstood programming language.

JavaScript가 개체의 멤버 접근을 제한할 수 있다며 이것이 잘 알려지지 않은 것을 안타까워합니다.

최근에 JavaScript가 OOPL인가의 질문에 대한 새로운 정보를 접하게 되었습니다. 사실 이 포스트를 작성하게 된 계기도 JavaScript와 OOP에 대한 얘기들을 정리하기 전에 privileged 메서드에 대한 의견을 먼저 포스팅 하는 것이 좋겠다고 생각했기 때문입니다.

Privileged 메서드

소개된 JavaScript에서 보호된 멤버를 구현하는 방법은 클로저(closure)를 이용하는데 privileged 메서드를 통해 개체 외부로부터 데이터 접근을 제한합니다. 예를 들어 양의(positive) 수와 음의(negative) 수를 저장하는 속성과 관련된 privileged 메서드 집합을 제공하는 개체를 정의하면 이렇습니다.

function MyObject() {
  var mustBePositive = 2;
  var mustBeNegative = -2;

  this.getMustBePositive = function () {
    return mustBePositive;
  };
  this.setMustBePositive = function (value) {
    if (value > 0 !== true) {
      throw 'Out of range';
    }
    mustBePositive = value;
  };

  this.getMustBeNegative = function () {
    return mustBeNegative;
  };
  this.setMustBePositive = function (value) {
    if (value < 0 !== true) {
      throw 'Out of range';
    }
    mustBeNegative = value;
  };
}

mustBePositive라는 private 멤버를 정의하고 클로저 함수가 이 멤버에 접근하는데 생성자가 종료되면 오직 이 privileged 메서드만 mustBePositive 변수에 접근할 수 있습니다. 다른 방법은 전혀 없습니다. mustBeNegative 변수에 대해서도 마찬가지죠.

여기까지만 보면 JavaScript의 강력함에 경의를 표할지도 모르겠습니다. 저도 몇 년 전 이 방법에 대해 알게되었을 때 너무 재밌어서 이를 응용해 protected 멤버를 구현한 JavaScript 프레임워크를 만들었습니다. 하지만 몇 가지 테스트 후 과도한 메모리 사용과 성능 부하를 확인하고 코드 한 줄 남기지 않고 다 삭제했죠. 사실 privileged 메서드를 사용한 정보 보호를 인프라 수준에서 지원하려는 생각을 했던 것은 당시 제가 JavaScript에 대한 이해도가 너무 낮았기 때문입니다. 정확히 말하면 프로토타입 기반 상속의 원리에 대해 깊이 고민하지 않았던 것이죠.

전통적 방식

끔찍한 벤치 마크 결과를 직접 확인하고 나서야 많은 프레임워크들이 privileged 메서드를 사용하지 않고 언더스코어 접두사 등을 이용해 컨벤션(convention) 수준에서 정보 보호를 구현하는 이유를 알게되었고 지금은 이 방법을 선호합니다.

마음만 먹으면 누구나 접근할 수 있는 정보 보호 시스템이라니! 놀라시는 분들이 계실지도 모르겠습니다. 하지만 C++를 떠올려 보세요. 우리는 포인터(pointer)라는 강력한 무기가 있기 때문에 개발자가 원하면 얼마든지 개체의 보호된 상태에 접근하고 조작할 수 있습니다. Java나 C# 역시 코드가 충분한 권한을 가진다면 가능합니다. 여러 프로그래밍 언어에서 구현되는 정보 보호는 개발자의 실수로 부터 구조적으로 정보를 보호하는 것이지 개발자의 의도로부터 데이터를 지켜주지는 못합니다.

위 코드를 언더스코어 컨벤션과 프로토타입을 기반으로 구현한다면 다음과 같을 것입니다.

function MyObject() {
  this._mustBePositive = 2;
  this._mustBeNegative = -2;
}

MyObject.prototype.getMustBePositive = function () {
  return this._mustBePositive;
};
MyObject.prototype.setMustBePositive = function (value) {
  if (value > 0 !== true) {
    throw 'Out of range';
  }
  this._mustBePositive = value;
};

MyObject.prototype.getMustBeNegative = function () {
  return this._mustBeNegative;
};
MyObject.prototype.getMustBeNegative = function (value) {
  if (value < 0 !== true) {
    throw 'Out of range';
  }
  this._mustBeNegative = value;
};

메모리 사용과 성능

Node.js를 사용해 두 가지 구현의 메모리 사용과 메서드 호출 성능을 테스트해 보겠습니다. 정확도가 높진 않지만 차이가 워낙에 크니 결론 도출에는 별 문제 없을 것 같습니다. 테스트 코드와 결과는 다음과 같습니다.

var n = 1000000, arr = new Array(n), before = process.memoryUsage();
for (var i = 0; i < n; i++) {
  arr[i] = new MyObject();
}
var after = process.memoryUsage();
console.log(((after.heapUsed - before.heapUsed) / n) + ' bytes/obj');

var begin = new Date(), sum = 0.0;
for (var i = 0; i < n; i++) {
  sum += arr[i].get();
}
var elapsed = new Date() - begin;
console.log('sum: ' + sum);
console.log(elapsed + ' ms elapsed.');

 

구분 개체당 메모리 사용(byte) 총합 계산 소요시간(ms)
Privileged 메서드 407.844712 67
Underscore 컨벤션 47.799872 4

오, 이런! Privileged 메서드를 이용한 방법은 전통적 구현에 비해 8배가 넘는 메모리를 사용하고(미리 언급했듯이 정확한 수치라고 보기는 어렵습니다. 하지만 8배입니다, 자그마치 8배! 그리고 private 변수의 수가 늘어날 수록, privileged 메서드 코드가 커질 수록 10배, 20배 이상 될 수 있습니다.) 합계를 계산하는 데에 걸린시간은 16배가 넘습니다. 성과에 비해 비용이 너무 높다고 생각되네요. 많은 양의 엔터티를 처리하는 SPA(single-page application)나 네트워크 서버에서 구동되는 코드를 떠올려보세요. 사용자나 클라이언트 프로그램에 대한 반응 시간은 길어질 것이고 GC(garbage collector)는 바빠지겠죠.

Privileged 메서드는 클로저 개체이며 각각의 MyObject 인스턴스에 대해 만들어집니다. 반면 프로토타입에 정의된 메서드는 각 인스턴스 별로 만들어지는 것이 아니라 위임(delegation)을 통해 공유되는 자원이며 인스턴스 데이터는 실행 문맥(this)을 통해 전달됩니다. 그리고 JavaScript 엔진에 따라 차이가 있겠지만 프로토타입에 정의된 함수는 최적화를 통해 내부적으로 형(type) 기반 바인딩이 이뤄질 가능성이 높아집니다. 테스트 결과가 이해가 되죠.

결론

정말로 엄격한 데이터 접근 제한을 구현해야 하는 개체가 있다면 privileged 메서드를 고려할 수 있습니다. 하지만 개발 플랫폼 수준에서 응용 프로그램의 모든 개체 혹은 데이터 엔터티를 대상으로 사용하려 한다면 신중하게 판단하는 것이 좋겠습니다. JavaScript는 프로토타입 기반 프로그래밍 언어(prototype-based programming language)입니다. 프로토타입을 이해하고 잘 활용하는 것은 큰 힘이 될 것입니다.

Advertisements

JavaScript의 privileged 메서드가 끔찍한 이유”에 대한 1개의 생각

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Google+ photo

Google+의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

%s에 연결하는 중