← Back to NOTES 🌰 ← Effective JavaScript 🤿

← 아이템 35 - 비공개 데이터를 저장하기 위해 클로저를 사용하라

핵심 요약

JavaScript의 객체 시스템은 정보 은닉을 강제하지 않으며, 모든 프로퍼티가 기본적으로 공개된다. 클로저를 사용하면 변수를 비공개로 만들 수 있으며, 명시적으로 제공된 메서드를 통해서만 접근할 수 있다. 클로저는 보안이 중요한 상황에서 정보 은닉을 보장하는 강력한 메커니즘이다.

자세한 내용

JavaScript의 정보 공개 특성

클로저의 정보 은닉 메커니즘

클로저 패턴의 장단점

예제 코드

// 전통적인 네이밍 컨벤션 방식 (완전하지 않은 은닉)
function User(name, age) {
    this._name = name;
    this._age = age;
}

var user = new User('Alice', 30);
console.log('=== 네이밍 컨벤션의 한계 ===');
console.log('user._name:', user._name); // "Alice" - 접근 가능
console.log('user._age:', user._age);   // 30 - 접근 가능

// 프로퍼티 열거로 내부 데이터 노출
console.log('모든 프로퍼티:', Object.keys(user)); // ["_name", "_age"]

// 클로저를 사용한 완전한 정보 은닉
function SecureUser(name, passwordHash) {
    // 이 변수들은 외부에서 접근 불가능
    var privateName = name;
    var privatePasswordHash = passwordHash;

    // 공개 메서드만 외부에서 접근 가능
    this.toString = function() {
        return "[User " + privateName + "]";
    };

    this.checkPassword = function(password) {
        return hash(password) === privatePasswordHash;
    };

    this.getName = function() {
        return privateName;
    };

    this.setName = function(newName) {
        if (typeof newName === 'string' && newName.length > 0) {
            privateName = newName;
            return true;
        }
        return false;
    };
}

function hash(password) {
    return password + "_hashed"; // 실제로는 복잡한 해싱 사용
}

var secureUser = new SecureUser('Bob', '0ef33ae...');

console.log('\\\\n=== 클로저를 통한 정보 은닉 ===');
console.log('secureUser.name:', secureUser.name);           // undefined
console.log('secureUser.passwordHash:', secureUser.passwordHash); // undefined
console.log('secureUser.toString():', secureUser.toString()); // "[User Bob]"
console.log('getName():', secureUser.getName());             // "Bob"
console.log('모든 프로퍼티:', Object.keys(secureUser));      // 메서드만 표시

// 아이템11의 box 예제 - 완전한 캡슐화
function createBox() {
    var value = undefined;

    return {
        set: function(newVal) {
            value = newVal;
        },
        get: function() {
            return value;
        },
        type: function() {
            return typeof value;
        }
    };
}

var box = createBox();
console.log('\\\\n=== Box 예제 ===');
console.log('box.value:', box.value); // undefined - 직접 접근 불가
box.set('Hello');
console.log('box.get():', box.get()); // "Hello"
console.log('box.type():', box.type()); // "string"

// 실제 사용 사례: 은행 계좌 시뮬레이션
function BankAccount(initialBalance, accountNumber) {
    var balance = initialBalance || 0;
    var accountNum = accountNumber;
    var transactionHistory = [];

    // 비공개 헬퍼 함수
    function addTransaction(type, amount, description) {
        transactionHistory.push({
            type: type,
            amount: amount,
            description: description,
            timestamp: new Date(),
            balanceAfter: balance
        });
    }

    // 공개 메서드들
    this.deposit = function(amount, description) {
        if (amount > 0) {
            balance += amount;
            addTransaction('deposit', amount, description || 'Deposit');
            return balance;
        }
        throw new Error('Deposit amount must be positive');
    };

    this.withdraw = function(amount, description) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            addTransaction('withdrawal', amount, description || 'Withdrawal');
            return balance;
        }
        throw new Error('Invalid withdrawal amount');
    };

    this.getBalance = function() {
        return balance;
    };

    this.getAccountNumber = function() {
        return accountNum;
    };

    this.getTransactionHistory = function() {
        // 방어적 복사 - 원본 배열 보호
        return transactionHistory.map(function(transaction) {
            return {
                type: transaction.type,
                amount: transaction.amount,
                description: transaction.description,
                timestamp: new Date(transaction.timestamp),
                balanceAfter: transaction.balanceAfter
            };
        });
    };
}

// 은행 계좌 사용 예시
var account = new BankAccount(1000, 'ACC-001');

console.log('\\\\n=== 은행 계좌 시뮬레이션 ===');
console.log('초기 잔액:', account.getBalance()); // 1000

account.deposit(500, '급여');
console.log('입금 후 잔액:', account.getBalance()); // 1500

account.withdraw(200, '생활비');
console.log('출금 후 잔액:', account.getBalance()); // 1300

// 직접 접근 시도 - 실패
console.log('account.balance:', account.balance); // undefined
console.log('account.transactionHistory:', account.transactionHistory); // undefined

console.log('거래 내역:', account.getTransactionHistory());

// 상속과 클로저를 함께 사용하는 패턴
function createCounter(initialValue) {
    var count = initialValue || 0;

    function Counter() {
        // 빈 생성자 - 상태는 클로저에서 관리
    }

    Counter.prototype.increment = function() {
        count++;
        return count;
    };

    Counter.prototype.decrement = function() {
        count--;
        return count;
    };

    Counter.prototype.getValue = function() {
        return count;
    };

    Counter.prototype.reset = function() {
        count = 0;
        return count;
    };

    return new Counter();
}

var counter1 = createCounter(10);
var counter2 = createCounter(5);

console.log('\\\\n=== 독립적인 카운터들 ===');
console.log('counter1 초기값:', counter1.getValue()); // 10
console.log('counter2 초기값:', counter2.getValue()); // 5

counter1.increment();
counter2.increment();

console.log('증가 후 counter1:', counter1.getValue()); // 11
console.log('증가 후 counter2:', counter2.getValue()); // 6

// 메모리 사용량 비교
function compareMemoryUsage() {
    console.log('\\\\n=== 메모리 사용량 비교 ===');

    // 일반 프로토타입 방식
    function PublicUser(name) {
        this.name = name;
    }

    PublicUser.prototype.getName = function() {
        return this.name;
    };

    // 클로저 방식
    function PrivateUser(name) {
        var privateName = name;

        this.getName = function() {
            return privateName;
        };
    }

    // 여러 인스턴스 생성
    var publicUsers = [];
    var privateUsers = [];

    for (var i = 0; i < 3; i++) {
        publicUsers.push(new PublicUser('User' + i));
        privateUsers.push(new PrivateUser('User' + i));
    }

    console.log('프로토타입 방식 - 메서드 공유:');
    console.log('user1.getName === user2.getName:',
               publicUsers[0].getName === publicUsers[1].getName); // true

    console.log('클로저 방식 - 메서드 독립:');
    console.log('user1.getName === user2.getName:',
               privateUsers[0].getName === privateUsers[1].getName); // false

    console.log('\\\\n결론:');
    console.log('- 프로토타입: 메서드 1개 공유, 메모리 효율적');
    console.log('- 클로저: 인스턴스당 메서드 복사, 메모리 사용량 증가');
    console.log('- 보안이 중요한 경우 클로저의 메모리 비용은 감수할 만함');
}

compareMemoryUsage();

메모 / 코멘트