클로저 vs 클래스 private: 메모리 효율 차이

    728x90
    반응형
    SMALL

     

    클로저 vs 클래스 private: 메모리 효율 차이

    생성자 함수에서 클로저로 private을 구현하면 메모리가 비효율적이라고 했는데, 정확히 무엇이 문제일까?

     

    핵심: 변수가 아니라 메서드가 문제!

    변수는 둘 다 똑같다

    // 클로저 방식
    function Person(name) {
      let _name = name;  // 인스턴스마다 새 변수
    }
    
    // 클래스 방식
    class Person {
      #name;  // 인스턴스마다 새 변수
      
      constructor(name) {
        this.#name = name;
      }
    }
    

    변수 할당:

    • 클로저든 클래스든 인스턴스마다 새 변수 생성
    • 각 인스턴스의 데이터니까 당연함
    • 메모리 사용량 동일 ✅

     

    진짜 차이: 메서드의 위치

    클로저 방식 - 메서드도 복사됨 ❌

    function Person(name) {
      let _name = name;
      
      // ❌ 메서드가 생성자 안에 있음
      // 인스턴스마다 새로 생성됨!
      this.getName = function() {
        return _name;
      };
      
      this.setName = function(newName) {
        _name = newName;
      };
    }
    
    const p1 = new Person('Kim');
    const p2 = new Person('Lee');
    const p3 = new Person('Park');
    
    // 메서드가 각각 다른 함수!
    console.log(p1.getName === p2.getName);  // false
    console.log(p2.getName === p3.getName);  // false
    
    // 3개 인스턴스 = getName 3개 + setName 3개 = 총 6개 함수
    

    메모리 구조:

    p1: { _name: 'Kim',  getName: fn1, setName: fn2 }
    p2: { _name: 'Lee',  getName: fn3, setName: fn4 }
    p3: { _name: 'Park', getName: fn5, setName: fn6 }
    
    → 6개 함수 생성 (비효율!)
    

     

     

    클래스 방식 - 메서드는 공유됨 ✅

    class Person {
      #name;
      
      constructor(name) {
        this.#name = name;
      }
      
      // ✅ 메서드가 prototype에 있음
      // 모든 인스턴스가 공유!
      getName() {
        return this.#name;
      }
      
      setName(newName) {
        this.#name = newName;
      }
    }
    
    const p1 = new Person('Kim');
    const p2 = new Person('Lee');
    const p3 = new Person('Park');
    
    // 메서드가 같은 함수!
    console.log(p1.getName === p2.getName);  // true
    console.log(p2.getName === p3.getName);  // true
    
    // 3개 인스턴스 = getName 1개 + setName 1개 = 총 2개 함수
    

    메모리 구조:

    Person.prototype: { getName: fn1, setName: fn2 }  ← 1개만!
                            ↑            ↑
                            │            │
    p1: { #name: 'Kim'  } ──┘            │
    p2: { #name: 'Lee'  } ───────────────┘
    p3: { #name: 'Park' } ───────────────┘
    
    → 2개 함수만 생성 (효율적!)
    

     

     

     

     

     

    왜 클로저는 메서드를 복사할까?

    클로저가 필요하기 때문

    function Person(name) {
      let _name = name;  // 생성자의 지역 변수
      
      // 이 함수는 _name에 접근해야 함
      // → 클로저 형성 필요
      // → 생성자 안에 있어야 함
      this.getName = function() {
        return _name;  // 클로저로 접근
      };
    }
    
    // prototype에 두면 안 됨
    Person.prototype.getName = function() {
      return _name;  // ❌ _name을 찾을 수 없음!
    };
    

    이유:

    • _name은 생성자 함수의 지역 변수
    • prototype 메서드는 생성자 밖에 있음
    • 클로저를 만들려면 메서드가 생성자 안에 있어야 함

     

    메모리 사용량 비교

    1000개 인스턴스 생성 시

    클로저 방식

    function Person(name) {
      let _name = name;
      this.getName = function() { return _name; };
      this.setName = function(newName) { _name = newName; };
      this.greet = function() { return `Hello, ${_name}`; };
    }
    
    const people = [];
    for (let i = 0; i < 1000; i++) {
      people.push(new Person(`Person${i}`));
    }
    

    메모리:

    변수: 1000개 (_name × 1000)
    메서드: 3000개 (getName, setName, greet × 1000)
    ────────────────────────────
    총: 4000개 항목
    

     

     

     

    클래스 방식

    class Person {
      #name;
      constructor(name) { this.#name = name; }
      getName() { return this.#name; }
      setName(newName) { this.#name = newName; }
      greet() { return `Hello, ${this.#name}`; }
    }
    
    const people = [];
    for (let i = 0; i < 1000; i++) {
      people.push(new Person(`Person${i}`));
    }
    

    메모리:

    변수: 1000개 (#name × 1000)
    메서드: 3개 (getName, setName, greet, prototype에서 공유)
    ────────────────────────────
    총: 1003개 항목
    

     

     

    시각화

    클로저 방식 (비효율)

    인스턴스 1:
    ┌─────────────────┐
    │ _name: 'Kim'    │
    │ getName: fn1    │ ← 함수 복사본
    │ setName: fn2    │ ← 함수 복사본
    └─────────────────┘
    
    인스턴스 2:
    ┌─────────────────┐
    │ _name: 'Lee'    │
    │ getName: fn3    │ ← 함수 복사본
    │ setName: fn4    │ ← 함수 복사본
    └─────────────────┘
    
    인스턴스 3:
    ┌─────────────────┐
    │ _name: 'Park'   │
    │ getName: fn5    │ ← 함수 복사본
    │ setName: fn6    │ ← 함수 복사본
    └─────────────────┘
    
    → 6개 함수 생성
    

     

     

    클래스 방식 (효율적)

    Person.prototype:
    ┌─────────────────┐
    │ getName: fn1    │ ← 1개만!
    │ setName: fn2    │ ← 1개만!
    └─────────────────┘
        ↑   ↑   ↑
        │   │   └────────┐
        │   └────────┐   │
        └────────┐   │   │
    인스턴스 1:  │   │   │
    ┌──────────┐│   │   │
    │#name:'Kim'││   │   │
    └──────────┘│   │   │
                │   │   │
    인스턴스 2:  │   │   │
    ┌──────────┐│   │   │
    │#name:'Lee'││   │   │
    └──────────┘│   │   │
                │   │   │
    인스턴스 3:  │   │   │
    ┌───────────┐   │   │
    │#name:'Park'│   │   │
    └───────────┘   │   │
    
    → 2개 함수만 생성
    

     

     

    클래스는 어떻게 둘 다 할까?

    private + prototype 동시 사용

    class Person {
      #name;  // private 필드
      
      getName() {  // prototype 메서드
        return this.#name;  // private 접근 가능!
      }
    }
    

    내부 메커니즘 (개념적):

    // private 필드는 내부 슬롯에 저장
    Person 인스턴스: {
      [[PrivateField_name]]: 'Kim'
    }
    
    // 메서드는 prototype에 저장
    Person.prototype: {
      getName: function() {
        // JavaScript 엔진이 특별한 메커니즘으로
        // 내부 슬롯에 접근
        return this.[[PrivateField_name]];
      }
    }
    

     

     

     

    정리

    메모리 비효율의 진짜 이유

    ❌ 변수 할당 때문이 아님
       클로저든 클래스든 인스턴스마다 변수 생성 (당연함)
    
    ✅ 메서드 복사 때문
       클로저: 메서드가 인스턴스마다 생성됨
       클래스: 메서드가 prototype에서 공유됨
    

     

     

    비교표

    항복 클로저 클래스 (#)
    private 변수 인스턴스마다 생성 인스턴스마다 생성
    메서드 위치 인스턴스 내부 prototype
    메서드 공유 ❌ 복사됨 ✅ 공유됨
    private 보호 ✅ 진짜 ✅ 진짜
    메모리 효율 ❌ 비효율 ✅ 효율적

     

    결론

    인스턴스 100개 생성 시:
    
    클로저:
    - 변수: 100개
    - 메서드: 300개 (메서드 3개 × 100)
    → 총 400개
    
    클래스:
    - 변수: 100개
    - 메서드: 3개 (prototype 공유)
    → 총 103개
    
    클래스가 약 4배 더 효율적!
    

    권장: 새 코드는 클래스 #private 사용 (ES2022+)

     

     

     

     

    그럼 생성자함수에서 클로저방식을 안쓰고 (프라이빗을 안하고) 기존 정적메서드(스태틱) 인스턴스메서드(그냥 메서드)를 쓰면 이런 비효율은 없는건가?

     

     

    클로저 안 쓰면 비효율 없어요!

    생성자 함수 + prototype (효율적 ✅)

    // 생성자 함수
    function Person(name, age) {
      // public 필드만
      this.name = name;
      this.age = age;
    }
    
    // 정적 메서드 - 함수에 직접 추가
    Person.isAdult = function(age) {
      return age >= 18;
    };
    
    // 인스턴스 메서드 - prototype에 추가
    Person.prototype.greet = function() {
      return `Hello, ${this.name}`;
    };
    
    Person.prototype.getAge = function() {
      return this.age;
    };
    
    // 사용
    const p1 = new Person('Kim', 25);
    const p2 = new Person('Lee', 30);
    const p3 = new Person('Park', 20);
    
    // ✅ 메서드 공유됨!
    console.log(p1.greet === p2.greet);  // true
    console.log(p2.greet === p3.greet);  // true
    ```
    
    **메모리:**
    ```
    Person.prototype: { greet: fn1, getAge: fn2 }  ← 1개만!
                            ↑            ↑
                            │            │
    p1: { name: 'Kim',  age: 25 } ───────┘
    p2: { name: 'Lee',  age: 30 } ───────┘
    p3: { name: 'Park', age: 20 } ───────┘
    
    → 메서드 2개만 (효율적!)

     

     

     

    클래스와 거의 동일!

    생성자 함수 + prototype

    function Person(name, age) {
      this.name = name;
      this.age = age;
    }
    
    Person.isAdult = function(age) {
      return age >= 18;
    };
    
    Person.prototype.greet = function() {
      return `Hello, ${this.name}`;
    };

    클래스 (위와 동일한 결과)

    class Person {
      constructor(name, age) {
        this.name = name;
        this.age = age;
      }
      
      static isAdult(age) {
        return age >= 18;
      }
      
      greet() {
        return `Hello, ${this.name}`;
      }
    }
    ```
    
    **결과:**
    ```
    typeof Person;              // "function" (둘 다)
    Person.prototype.greet;     // function (둘 다)
    p1.greet === p2.greet;     // true (둘 다 공유)
    ```
    
    ---
    
    ## 정리 비교표
    
    | 방식 | private | 메서드 위치 | 메서드 공유 | 메모리 효율 |
    |------|---------|-----------|-----------|-----------|
    | **클로저** | ✅ | 생성자 내부 | ❌ 복사됨 | ❌ 비효율 |
    | **prototype** | ❌ | prototype | ✅ 공유됨 | ✅ 효율적 |
    | **클래스 (public)** | ❌ | prototype | ✅ 공유됨 | ✅ 효율적 |
    | **클래스 (#private)** | ✅ | prototype | ✅ 공유됨 | ✅ 효율적 |
    
    ---
    
    ## 핵심 정리
    ```
    메모리 비효율의 원인:
    
    ❌ 생성자 함수 자체가 문제 아님
    ✅ 클로저 사용이 문제
       → 메서드가 생성자 안에 있어야 함
       → 인스턴스마다 복사됨
       → 클로저로 지정한 값을 메서드가 기억하고 있어야하니까, 프로토타입 메서드로 못만들기 때문
    
    해결책:
    
    1. private 필요 없으면:
       → prototype 사용 (효율적)
       
    2. private 필요하면:
       → 클래스 # 사용 (효율적 + private)
       
    3. ES5 환경에서 private 필요하면:
       → 클로저 사용 (비효율적이지만 어쩔 수 없음)

    최종 답변

    zzimzzim님 질문

    "생성자 함수에서 클로저 방식을 안 쓰고 (private을 안 하고) 기존 정적 메서드, 인스턴스 메서드를 쓰면 이런 비효율은 없는 건가?"

    답: 맞아요! ✅

    // ✅ 효율적 (클로저 없음)
    function Person(name) {
      this.name = name;  // public
    }
    
    Person.prototype.greet = function() {
      return `Hello, ${this.name}`;
    };
    
    // ❌ 비효율적 (클로저 사용)
    function Person(name) {
      let _name = name;  // private (클로저)
      
      this.greet = function() {
        return `Hello, ${_name}`;
      };
    }
    ```
    
    ---
    
    ### 핵심
    ```
    생성자 함수 자체는 문제 없음!
    
    비효율의 진짜 원인:
    → 클로저를 사용해서 메서드가 생성자 안에 있을 때
    
    해결:
    → private 필요 없으면 prototype 사용
    → private 필요하면 클래스 # 사용

     

    728x90
    반응형
    LIST

    댓글