5.3 비공개 프로퍼티와 메서드

자바 등 다른 언어와는 달리 자바스크립트에는 private, protected, public 프로퍼티와 메서드를 나타내는 별도의 문법이 없다. 객체의 모든 멤버는 public, 즉 공개되어 있다.

1
2
3
4
5
6
7
8
var myobj = {
myprop: 1,
getProp: function() {
return this.myprop;
}
}
console.log(myobj.myprop); // 'myprop'에 공개적으로 접근할 수 있다.
console.log(myobj.getProp()); // getProp() 역시 공개되어 있다.

비공개(private) 멤버

비공개 멤버에 대한 별도의 문법은 없지만 클로저를 사용해서 구현할 수 있다. 생성자 함수 안에서 클로저를 만들면, 클로저 유효범위 안의 변수는 생성자 함수 외부에 노출되지 않지만 객체의 공개 메서드 안에서는 쓸 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Gadget(){
// 비공개 멤버
var name = 'iPod';
// 공개된 함수
this.getName = function() {
return name;
};
}
var toy = new Gadget();

// 'name'은 비공개이므로 undefined가 출력된다.
console.log(toy.name); // undefined

// 공개 메서드에서는 'name'에 접근할 수 있다.
console.log(toy.getName());

특권 메서드

특권 메서드는 비공개 멤버를 외부에서 접근 가능하게 하는 메서드를 말한다. 위에서는 getName()에 해당한다.

비공개 멤버의 허점

비공개 멤버를 유지하는게 관건이라면, 다음과 같은 경우에 대해서 신경을 써야 한다.

  • 파이어폭스의 초기 버전 중 일부는 eval() 함수에 두 번째 매개변수를 전달 할 수 있게 되어있다. 이 매개변수는 함수의 비공개 유효범위를 들여다볼 수 있게 해주는 컨텍스트 객체다. 모질라 라이노(Rhino)의 parent 프로퍼티도 이와 비슷한 방식으로 비공개 유효범위에 접근할 수 있게 해준다. 현재 널리 사용되는 브라우저에는 적용되지 않는 사례들이다.

  • 특권 메서드에서 비공개 변수의 값을 바로 반환할 경우 이 변수가 객체나 배열이라면 값이 아닌 참조가 반환되기 때문에, 외부 코드에서 비공개 변수값을 수정할 수 있다.

비공개 멤버에 대한 접근

1
2
3
4
5
6
7
8
9
10
11
12
function Gadget(){
// 비공개 멤버
var specs = {
screen_width: 320,
screen_height: 480,
color: "white"
};
// 공개 함수
this.getSpecs = function(){
return specs;
}
}

여기서 getSpec() 메서드가 specs 객체에 대한 참조를 반환하는게 문제다. 의도적으로 감춘 specs를 참조를 통해 외부에서 변경이 가능해지게 된 것이다. 의도대로라면 setSpec()메서드를 통해서만 값을 변경 가능하게 작동해야 한다.

이를 해결하기 위해서는 getSpec() 에 객체를 복사해서 반환하는 방식으로 구현을 하면 된다. 이를 ‘최소 권한의 원칙(Principle of Least Authority, POLA)’ 이라고도 한다.

객체 리터럴과 비공개 멤버

지금까지는 비공개 멤버를 만드는데 생성자를 사용했다. 그렇다면 객체 리터럴로 만들어진 객체는 비공개 멤버를 구현할 수 있을까? 객체 리터럴에서는 익명 즉시 실행함수 를 추가하여 클로저를 만든다. 다음 예제를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var myobj;
(function(){
// 비공개 멤버
var name = "my, oh my";

// 공개될 부분을 구현한다.
// var를 사용하지 않았다는데 주의하라.
myobj = {
// 특권 메서드
getName: function(){
return name;
}
};
}());
myobj.getName(); // "my, oh my"

다음 예제는 기본 개념은 동일하지만 약간 다르게 구현되어 있다. 앞으로 나올 ‘모듈 패턴’의 기초가 되는 예제이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
var myobj = (function(){
// 비공개 멤버
var name = "my, oh my";

// 공개될 부분을 구현한다.
return {
getName: function(){
return name;
}
};
}());

myobj.getName(); // "my, oh my"

프로토타입과 비공개 멤버

생성자를 사용하여 비공개 멤버를 만들 경우, 생성자를 호출하여 새로운 객체를 만들 때마다 비공개 멤버가 매번 재생성된다는 단점이 있다. 이러한 중복을 없애고 메모리를 절약하려면 공통 프로퍼티와 메서드를 생성자의 prototype 프로퍼티에 추가해야 한다. 이렇게 하면 동일한 생성자로 생성한 모든 인스턴스가 공통된 부분을 공유하게 된다.

감춰진 비공개 멤버들도 모든 인스턴스가 함께 쓸 수 있다. 이를 위해서는 두 가지 패턴, 즉 생성자 함수 내부에 비공개 멤버를 만드는 패턴과 객체 리터럴로 비공개 멤버를 만드는 패턴을 함께 써야한다. 왜냐하면 prototype 프로퍼티도 결국 객체라서, 객체 리터럴로 생성할 수 있기 때문이다.

예제를 보자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Gadget(){
// 비공개 멤버
var name = 'iPod';
// 공개 함수
this.getName = function(){
return name;
};
}

Gadget.prototype = (function(){
// 비공개 멤버
var browser = "Mobile Webkit";
// 공개된 프로토타입 멤버
return {
getBrowser: function(){
return browser;
}
};
}());

var toy = new Gadget();
console.log(toy.getName());
console.log(toy.getBrowser());

비공개 함수를 공개 메서드로 노출시키는 방법

노출 패턴(revelation pattern)은 비공개 메서드를 구현하면서 동시에 공개 메서드로도 노출하는 것을 말한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var myarray;

(function(){
var astr = "[object Array]",
var toString = Object.prototype.toString;

function isArray(a){
return toString.call(a) === astr;
}

function indexOf(haystack, needle){
var i = 0,
var max = haystack.length;
for( ; i < max; i += 1){
if(haystack[i] === needle){
return i;
}
}
return -1;
}

myarray = {
isArray: isArray,
indexOf: indexOf,
inArray: indexOf
}
}());

위의 예제는 객체 리터럴 안에 비공개 멤버를 만드는 패턴에 기반하고 있다. 여기에는 비공개 변수 두 개와 비공개 함수 두개가 존재한다. 즉시 실행함수의 마지막 부분을 보면, 공개적인 접근을 허용해도 괜찮다고 결정한 기능들이 myarray 객체에 채워진다.

새로운 myarray 객체를 테스트 해보자

1
2
3
4
5
6
7
myarray.isArray([1,2]); // true
myarray.isArray({0 : 1}); // false
myarray.indexOf(["a", "b", "z"], "z"); // 2
myarray.inArray(["a", "b", "z"], "z"); // 2

myarray.indexOf = null;
myarray.inArray(["a", "b", "z"], "z"); // 2