본문으로 바로가기

JS 비공개(private) 멤버 및 특권(privileged) 메서드

자바 등 다른 언어와는 달리 자바스크립트에는 private, protected, public 프로퍼티와 메서드를 나타내는 별도의 문법이 없습니다.

객체의 모든 멤버는 public, 즉 공개되어 있습니다.





자바스크립 객체의 모든 멤버는 공개되어 있다

위에 언급했듯이 다른 언어와 달리 별도의 문법이 없기 때문에 일반적으로 객체의 모든 멤버는 공개되어 있습니다.


다음의 예제를 살펴봅니다.

javascript
var myObj = {
    myProp : 1,
    getProp: function () {
        return this.myProp;
    }
};
console.log(myObj.myProp); // myProp 에 공개적으로 접근할 수 있다.
console.log(myObj.getProp()); // getProp() 역시 공개되어 있다.


객체 리터럴 방식을 사용할 때와 마찬가지로 생성자 함수를 사용해 객체를 생성할 때도 마찬가지로 모든 멤버가 공개되어 있습니다.

javascript
function Gadget() {
    this.name = 'iPod';
    this.stretch = function () {
        return 'iPad';
    };
}
var toy = new Gadget();
console.log(toy.name); // name 은 공개되어 있다.
console.log(toy.stretch()); // stretch() 메서드도 공개되어 있다.




비공개(private) 멤버

위 예제에서는 모두 외부에서 객체의 멤버에 접근할 수 있도록 공개되어 있습니다.

그렇다면 객체의 멤버에 접근하지 못하도록 공개하지 않을 수는 없을까?

비공개 멤버에 대한 별도의 문법은 없지만 클로저를 사용해서 구현할 수 있습니다.

생성자 함수 안에서 클로저를 만들면, 클로저 유효범위 안의 변수는 생성자 함수 외부에 노출되지 않지만 객체의 공개 메서드 안에서는 쓸 수 있습니다.

즉, 생성자에서 객체을 반환할 때 객체의 메서드를 정의하면, 이 메서드 안에서는 비공개 변수에 접근할 수 있는 것입니다.


다음의 예제를 봅니다.

javascript
function Gadget() {
    // 비공개 멤버
    var name = 'iPod';
    this.getName = function () {
        return name;
    };
}

var toy = new Gadget();

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

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

보다시피 자바스크립트에서도 쉽게 비공개 멤버를 구현할 수가 있습니다.

비공개로 유지할 데이터를 함수로 감싸기만 하면 되는 것입니다. 이 데이터들을 함수의 지역 변수로 만들면, 함수 외부에서는 접근할 수 없습니다.



특권(privileged) 메서드

특권 메서드라는 개념은 특정한 문법과는 관련이 없습니다.

단지 비공개 멤버에 접근권한을 가진(즉 일종의 특권을 부여 받은)공개 메서드를 가리키는 이름일 뿐입니다.

앞선 예제에서 getName()은 비공개 프로퍼티인 name'특별한' 접근 권한을 가지고 있기 때문에 특권 메서드라고 할 수 있습니다.



비공개 멤버의 허점

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


다음에 나오는 Gadget 구현은 별 문제가 없어보일지 모릅니다.

javascript
function Gadget() {
   // 비공개 멤버
    var specs = {
        screen_width : 320,
        screen_height : 480,
        color : 'white'
    };

    // 공개 함수
    this.getSpecs = function () {
        return specs;
    };
}

여기서 getSpecs() 메서드가 specs 객체에 대한 참조를 반환하고 있다는 것이 문제입니다.

specs는 감춰진 비공개 멤버처럼 보이지만 Gadget 사용자에 의해 변경될 소지가 있습니다.

javascript
function Gadget() {
   // 비공개 멤버
    var specs = {
        screen_width : 320,
        screen_height : 480,
        color : 'white'
    };

    // 공개 함수
    this.getSpecc = function () {
        return specs;
    };
}

var toy = new Gadget(),
specs = toy.getSpecc();

specs.color = 'black';
specs.price = 'free';

console.dir(toy.getSpecc());
// 비공개 객체가 아래와 같이 수정된다.
/**
 * -------------------
 * color: "black"
 * price: "free"
 * screen_height: 480
 * screen_width: 320
 * --------------------
 */

이와 같은 예상치 않은 문제를 해결하기 위해서는 비공개로 유지해야 하는 객체나 배열에 대한 참조를 전달할 때 주의를 기울이는 수밖에 없습니다.

하나의 방법은 getSpecs()에서 아예 새로운 객체를 만들어 사용자에게 쓸모있을 만한 데이터 일부만 담아 반환하는 것입니다.

이것을 '최소 권한의 원칙(Principle of Least Authority, POLA)'이라고도 합니다. 필요 이상으로 권한을 주지 말아야 한다는 의미입니다.


위 예제에서는 Gadget 사용자가 Gadget이 어떤 상자에 들어맞을지를 알아보고 싶어하는 거라면 Gadget의 면적만 알려주면 됩니다.

그렇다면 모든 정보를 내주는 대신 getDimensions()라는 메서드를 만들어 width와 height만을 담은 객체를 반환하면 될 것입니다.

이렇게 구현한다면 getSpecs() 메서드는 아예 구현할 필요조차 없었을지도 모릅니다.



객체 리터럴과 비공개 멤버

지금까지는 비공개 멤버를 만드는데 생성자를 사용하는 방법들만 살펴보았습니다.

그렇다면 객체 리터럴로 객체를 생성한 경우에 어떻게 해야 할까? 이 경우에도 비공개 멤버를 구현할 수 있을까?

여태까지 보아왔듯이 비공개 데이터를 함수로 감싸기만 됩니다.

따라서 객체 리터럴에서는 익명 즉시 실행 함수를 추가하여 클로저를 만듭니다.


다음 예제를 살펴봅니다.

javascript
var myObj; // 이 변수에 객체를 할당할 것이다.

(function(){
    // 비공개 멤버
    var name = 'My name is jaehee';

    // 공개될 부분을 구현한다.
    // var를 사용하지 않았다는 데 주의하라.
    myObj = {
        // 특권(privileged) 메서드
        getName : function () {

        }
    }

}());

myObj.getName(); // 'My name is jaehee'


다음 예제는 기본 개념은 동일하지만 약간 다르게 구현해 본 것입니다.

javascript
var myObj = (function () {
    // 비공개 멤버
    var name = 'My name is jaehee';

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

myObj.getName(); // My name is jaehee



프로토타입과 비공개 멤버

생성자를 사용하여 비공개 멤버를 만들 경우, 생성자를 호출하여 새로운 객체를 만들 때마다 비공개 멤버가 매번 재생성된다는 단점이 있습니다.

사실 생성자 내부에서 this에 멤버를 추가하면 항상 이런 문제가 발생합니다.

이러한 중복을 없애고 메모리를 절약하려면 공통 프로퍼티와 메서드를 생성자의 prototype 프로퍼티에 추가해야 합니다.

이렇게 하면 동일한 생성자로 생성한 모든 인스턴스가 공통된 부분을 공유하게 됩니다.  감춰진 비공개 멤버들도 모든 인스턴스가 함께 쓸 수 있습니다.

이를 위해서는 두 가지 패턴, 즉 생성자 함수 내부에 비공개 멤버를 만드는 패턴과 객체 리터럴로 비공개 멤버를 만드는 패턴을 함께 사용해야 합니다.

왜냐하면 prototype 프로퍼티도 결국 객체라서, 객체 리터럴로 생성할 수 있기 때문입니다.


다음 예제를 통해 알아봅니다.

javascript
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)은 비공개 메서드를 구현하면서 동시에 공개 메서드로도 노출하는 것을 말합니다.

객체의 모든 기능이 객체가 수행하는 작업에 필수불가결한 것들이라서 최대한의 보호가 필요한데, 동시에 이 기능들의 유용성 때문에 공개적인 접근도 허용하고 싶은 경우가 있을 수 있습니다.

노출 패턴은 이런한 경우에 유용하게 사용할 수 있습니다. 그리고 메서드가 공개되어 있다는 것은 결국 이 메서드가 위험에 노출되어 있다는 말과도 같습니다.

공개 API 사용사자 어쩌면 본의 아니게 메서드를 수정할 수 있기 때문입니다.

ECMAScript 5에서는 객체를 고정(freeze)시킬 수 있는 선택지가 있지만, 하위 버전에서는 그렇지 않습니다.


이제 노출 패턴에 대해 알아봅니다.

이 용어는 크리스천 헤일먼(Christian Heimann)이 만들어냈으며 처음에는 '모듈 노출 패턴(revealing module pattern)'이라고 했습니다.

먼저 예제를 살펴 봅니다.

이 예제는 객체 리터럴 안에서 비공개 멤버를 만드는 패턴에 기반하고 있습니다.

javascript
var myArray;

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

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

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

    myArray = {
        isArray : isArray,
        indexOf : indexOf,
        inArray : indexOf
    }

}());

여기에는 비공개 변수 두 개와 비공개 함수 두 개, isArray()와 indexOf()가 존재합니다.

즉시 실행 함수의 마지막 부분을 보면, 공개적인 접근을 허용해도 괜찮겠다고 결정한 기능들이 myArray 객체에 채워집니다.

비공개 함수 indexOf()는 ECMAScript 5식의 이름인 indexOf와 PHP에서영향을 받은 이름인 inArray라는 두 개의 이름으로 노출되었습니다.

이제 새로운 myArray 객체를 테스트해 봅니다.

javascript
console.log(myArray.isArray([1, 2])); // true
console.log(myArray.isArray({0: 1})); // false
console.log(myArray.indexOf(['a', 'b', 'z'], 'z')); // 2
console.log(myArray.inArray(['a', 'b', 'z'], 'z')); // 2


이제 공개된 메서드인 indexOf()에 예기치 못한 일이 일어나더라도, 비공개 함수인 indexOf()는 안전하게 보호되기 때문에 inArray()는 계속해서 잘 동작할 것입니다.

javascript
myArray.indexOf = null;
console.log(myArray.inArray(['a', 'b', 'z'], 'z')); // 2



Jaehee's WebClub