← Back to NOTES 🌰 ← Effective JavaScript 🤿

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

핵심 요약

프로토타입과 인스턴스의 일대다 관계를 이해해야 한다. 인스턴스마다 달라야 하는 상태값을 실수로 프로토타입에 저장하면 모든 인스턴스가 같은 상태를 공유하게 되어 예상치 못한 버그가 발생한다. 가변 데이터는 반드시 인스턴스 객체에 저장하고, 프로토타입에는 메서드와 불변 데이터만 저장해야 한다.

자세한 내용

프로토타입과 인스턴스의 관계

프로토타입에 저장해도 안전한 것들

인스턴스에 저장해야 하는 것들

예제 코드

// 잘못된 예시: 가변 상태를 프로토타입에 저장
function BadTree(x) {
    this.value = x;
    // children 배열이 프로토타입에 있어서 문제 발생
}

BadTree.prototype = {
    children: [], // 모든 인스턴스가 공유하는 배열 (문제!)
    addChild: function(x) {
        this.children.push(x);
    }
};

console.log('=== 잘못된 구현의 문제점 ===');
var left = new BadTree(2);
left.addChild(1);
left.addChild(3);

var right = new BadTree(6);
right.addChild(5);
right.addChild(7);

var head = new BadTree(4);
head.addChild(left);
head.addChild(right);

console.log('head.children:', head.children); // [1, 3, 5, 7, BadTree, BadTree]
console.log('left.children === right.children:', left.children === right.children); // true (문제!)
console.log('모든 노드가 같은 배열을 공유함');

// 올바른 예시: 가변 상태를 인스턴스에 저장
function Tree(x) {
    this.value = x;
    this.children = []; // 각 인스턴스마다 독립적인 배열
}

Tree.prototype = {
    addChild: function(x) {
        this.children.push(x);
    },

    getChildren: function() {
        return this.children.slice(); // 방어적 복사
    },

    hasChildren: function() {
        return this.children.length > 0;
    },

    traverse: function(callback) {
        callback(this.value);
        for (var i = 0; i < this.children.length; i++) {
            if (this.children[i] instanceof Tree) {
                this.children[i].traverse(callback);
            } else {
                callback(this.children[i]);
            }
        }
    }
};

console.log('\\\\n=== 올바른 구현 ===');
var leftCorrect = new Tree(2);
leftCorrect.addChild(1);
leftCorrect.addChild(3);

var rightCorrect = new Tree(6);
rightCorrect.addChild(5);
rightCorrect.addChild(7);

var headCorrect = new Tree(4);
headCorrect.addChild(leftCorrect);
headCorrect.addChild(rightCorrect);

console.log('leftCorrect.children:', leftCorrect.children); // [1, 3]
console.log('rightCorrect.children:', rightCorrect.children); // [5, 7]
console.log('headCorrect.children:', headCorrect.children); // [Tree, Tree]
console.log('left와 right가 독립적인 배열을 가짐');

// 트리 순회 테스트
console.log('트리 순회 결과:');
headCorrect.traverse(function(value) {
    if (typeof value === 'number') {
        console.log('  값:', value);
    }
});

// 클래스 문법에서의 static 속성 예시
if (typeof class !== 'undefined') {
    class CounterClass {
        static instanceCount = 0; // 모든 인스턴스가 공유

        constructor(initialValue) {
            this.value = initialValue || 0; // 인스턴스별 독립 상태
            CounterClass.instanceCount++;
        }

        increment() {
            this.value++;
            return this.value;
        }

        static getInstanceCount() {
            return CounterClass.instanceCount;
        }
    }

    console.log('\\\\n=== 클래스의 static 속성 ===');
    var counter1 = new CounterClass(10);
    var counter2 = new CounterClass(20);

    console.log('총 인스턴스 수:', CounterClass.getInstanceCount()); // 2
    console.log('counter1.value:', counter1.value); // 10
    console.log('counter2.value:', counter2.value); // 20

    counter1.increment();
    console.log('증가 후 counter1.value:', counter1.value); // 11
    console.log('counter2.value는 영향받지 않음:', counter2.value); // 20
}

// 실제 사용 사례: 학생 관리 시스템
function Student(name, studentId) {
    this.name = name;
    this.studentId = studentId;
    this.courses = []; // 각 학생의 수강 과목 (인스턴스별 독립)
    this.grades = {}; // 각 학생의 성적 (인스턴스별 독립)
}

// 공유 가능한 메서드들
Student.prototype.addCourse = function(courseName) {
    if (this.courses.indexOf(courseName) === -1) {
        this.courses.push(courseName);
    }
};

Student.prototype.setGrade = function(courseName, grade) {
    if (this.courses.indexOf(courseName) !== -1) {
        this.grades[courseName] = grade;
    }
};

Student.prototype.getGPA = function() {
    var total = 0;
    var count = 0;

    for (var course in this.grades) {
        if (this.grades.hasOwnProperty(course)) {
            total += this.grades[course];
            count++;
        }
    }

    return count > 0 ? total / count : 0;
};

Student.prototype.toString = function() {
    return 'Student: ' + this.name + ' (ID: ' + this.studentId + ')';
};

// 학생 시스템 사용 예시
console.log('\\\\n=== 학생 관리 시스템 ===');
var alice = new Student('Alice', 'S001');
var bob = new Student('Bob', 'S002');

alice.addCourse('Math');
alice.addCourse('Physics');
alice.setGrade('Math', 85);
alice.setGrade('Physics', 92);

bob.addCourse('Chemistry');
bob.addCourse('Biology');
bob.setGrade('Chemistry', 78);
bob.setGrade('Biology', 88);

console.log('Alice 수강 과목:', alice.courses); // ['Math', 'Physics']
console.log('Bob 수강 과목:', bob.courses); // ['Chemistry', 'Biology']
console.log('Alice GPA:', alice.getGPA()); // 88.5
console.log('Bob GPA:', bob.getGPA()); // 83
console.log('각 학생이 독립적인 데이터를 가짐');

// 공유 데이터와 인스턴스 데이터의 올바른 구분
function University() {
    this.name = '';
    this.students = []; // 인스턴스별 학생 목록
    this.founded = null;
}

// 공통 설정값들 (불변이므로 프로토타입에 안전하게 저장)
University.prototype.GRADE_SCALE = {
    A: 90,
    B: 80,
    C: 70,
    D: 60,
    F: 0
};

University.prototype.MAX_STUDENTS = 10000;

University.prototype.addStudent = function(student) {
    if (this.students.length < this.MAX_STUDENTS) {
        this.students.push(student);
        return true;
    }
    return false;
};

University.prototype.getStudentCount = function() {
    return this.students.length;
};

University.prototype.getAverageGPA = function() {
    if (this.students.length === 0) return 0;

    var totalGPA = 0;
    for (var i = 0; i < this.students.length; i++) {
        totalGPA += this.students[i].getGPA();
    }

    return totalGPA / this.students.length;
};

// 대학 시스템 사용
var university1 = new University();
university1.name = 'Tech University';
university1.addStudent(alice);

var university2 = new University();
university2.name = 'Science University';
university2.addStudent(bob);

console.log('\\\\n=== 대학 시스템 ===');
console.log(university1.name + ' 학생 수:', university1.getStudentCount()); // 1
console.log(university2.name + ' 학생 수:', university2.getStudentCount()); // 1
console.log('각 대학이 독립적인 학생 목록을 가짐');

// 메모리 구조 분석
function analyzeMemoryStructure() {
    console.log('\\\\n=== 메모리 구조 분석 ===');

    var tree1 = new Tree(1);
    var tree2 = new Tree(2);

    console.log('프로토타입 공유 메서드:');
    console.log('tree1.addChild === tree2.addChild:', tree1.addChild === tree2.addChild); // true

    console.log('인스턴스별 독립 데이터:');
    console.log('tree1.children === tree2.children:', tree1.children === tree2.children); // false
    console.log('tree1.value === tree2.value:', tree1.value === tree2.value); // false

    console.log('\\\\n올바른 메모리 구조:');
    console.log('- 메서드: 프로토타입에서 공유 (메모리 효율성)');
    console.log('- 상태 데이터: 인스턴스별 독립 (데이터 무결성)');
}

analyzeMemoryStructure();