Notice
Recent Posts
Recent Comments
Link
«   2026/01   »
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 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

likeornament 님의 블로그

프로토타입의 메모리 효율성 vs 클로저의 데이터 은닉 본문

공부/javascript

프로토타입의 메모리 효율성 vs 클로저의 데이터 은닉

likeornament 2026. 1. 22. 19:43

1. 프로토타입

자바스크립트는 클래스 기반 언어가 아니라 프로토타입 기반 언어다.

 

어떤 객체를 새로 만들어낼 때,

완전히 새로운 구조를 복사하는 대신 이미 존재하는 객체를 원형(prototype)으로 삼아 이를 참조한다.

 

이 구조의 핵심은 단순하다.

인스턴스는 자신의 메서드를 직접 소유하지 않고,
생성자 함수의 prototype에 정의된 메서드를 참조한다.

 

덕분에 같은 생성자로 만들어진 수많은 인스턴스가 하나의 메서드를 공유할 수 있다.

 

인스턴스마다 메서드를 새로 생성하지 않아도 되니,

메모리 사용 측면에서 매우 효율적인 구조다.

 

프로토타입을 공부하며 내가 가장 인상 깊게 느낀 지점도 바로 이 부분이었다.

 

그런데 여기서 이런 의문이 들었다.

 

"그렇다면 모든 메서드는 무조건 prototype에 정의하는 것이 정답일까?"

 

메모리 효율만 놓고 보면 분명 그래 보였다.

 

하지만 곧 다른 문제와 충돌하게 됐다.


2. 프로토타입의 한계

그 문제는 바로 데이터 은닉이다.

 

프로토타입 메서드는 구조상
생성자 함수 내부의 지역 변수에 접근할 수 없다.

 

즉, 클로저로 보호된 데이터에는 닿을 수 없다.

 

결국 프로토타입 메서드에서 데이터를 사용하려면
아래처럼 인스턴스의 프로퍼티로 값을 노출할 수밖에 없다.

function Person(name) {
  this.name = name; // 데이터를 인스턴스의 프로퍼티로 저장
}

Person.prototype.getName = function() {
  return this.name; // this를 통해 데이터에 접근
};

const suzi = new Person('Suzi');

// 문제 발생: 외부에서 마음대로 수정이 가능함
suzi.name = 'Hacker';
console.log(suzi.getName()); // 'Hacker'

 

메서드는 공유되지만,
상태는 완전히 외부에 노출된 구조다.

 

그렇다면 데이터를 완전히 숨기는 방법은 없을까?


3. 클로저

이때 떠오른 방법은 클로저였다.

function PrivatePerson(name) {
  let _name = name; // this가 아닌 지역 변수로 선언 (외부 접근 불가)

  this.getName = function() {
    return _name; // 클로저를 통해 외부 변수에 접근
  };
}

const minji = new PrivatePerson('Minji');

console.log(minji._name); // undefined
minji._name = 'Hacker';   // 내부 변수에는 영향 없음
console.log(minji.getName()); // 'Minji'

 

클로저를 사용하자 상황이 정반대가 되었다.

데이터는 완벽하게 보호되었지만,

메서드는 인스턴스마다 새로 생성된다.

 

즉, 데이터 은닉은 완벽하지만
인스턴스 수가 많아질 수록 메모리 사용량이 증가하는 또 다른 문제가 드러났다.

 

여기서 선택지는 확실해졌다.

  • 프로토타입: 메모리는 효율적이지만 데이터는 노출
  • 클로저: 데이터는 보호되지만 메모리 비용 증가

하나를 얻으면, 다른 하나는 포기해야 하는 구조였다.

 

그렇다면 이 차이는 실제로 체감할 만큼 큰 비용 차이일까? 

 

이를 확인하기 위해,
직접 테스트를 통해 데이터로 비교해보기로 했다.


4. 두 패턴의 비용 비교 실험

앞에서 본 두 패턴은 장단점이 명확하다.

  • 프로토타입 패턴
    : 메서드 공유 O / 데이터 은닉 X
  • 클로저 패턴
    : 메서드 공유 X / 데이터 은닉 O

하지만 여기서 중요한 건 "그래서 이 차이가 실제로 얼마나 큰가?"다.

 

이 차이가 체감할 수 없는 수준이라면 논쟁 자체가 의미 없을 것이고,

수만 개의 인스턴스를 생성하는 상황이라면 선택 기준은 완전히 달라진다.

 

그래서 동일한 역할을 하는 객체를 두 패턴으로 각각 구현한 뒤,

실제로 생성 비용과 메모리 사용 양상을 비교해보기로 했다.


Case A - 프로토타입 패턴

function Person(name) {
  this.name = name;
}

Person.prototype.getName = function () {
  return this.name;
};
  • 데이터(name)는 인스턴스 프로퍼티로 노출됨
  • getNameprototype에 존재
  • 모든 인스턴스가 하나의 메서드(getName)를 공유

Case B - 클로저 패턴

function PrivatePerson(name) {
  let _name = name;

  this.getName = function getName() {
    return _name;
  };
}
  • _name은 생성자 함수의 지역 변수
  • getName은 클로저를 통해 _name을 참조
  • 인스턴스마다 getName 함수가 새로 생성됨

실험 방법

실험은 다음과 같이 진행했다.

  1. Chrome DevTools의 Memory 탭에서 Heap Snapshot을 준비한다.
  2. 콘솔에서 아래 테스트 코드를 실행한다.
  3. 각 테스트는 브라우저를 새로고침한 후 개별적으로 수행한다.
const COUNT = 100_000;
const arr = [];

console.time('create');

for (let i = 0; i < COUNT; i++) {
  arr.push(new Person('test')); // 또는 PrivatePerson
}

console.timeEnd('create');
  • 한 번의 테스트에서는 하나의 패턴만 사용
  • arr에 참조를 유지해 GC로 객체가 수거되지 않도록 함
  • COUNT를 1만 → 5만 → 10만으로 단계적으로 증가시키며 측정

프로토타입 패턴 실험 결과

 

 

실험 전 Heap Snapshot을 찍고 getName을 검색해보면,

Chrome 내부 코드나 DevTools 스크립트에서 사용 중인 getName만 존재하고
PersongetName아직 나타나지 않는다.

 

 

COUNT를 늘려가며 테스트한 결과는 다음과 같다.

  • 1만 개: 0.5229 ms
  • 5만 개: 0.9440 ms
  • 10만 개: 1.1650 ms

COUNT가 증가함에 따라 시간이 늘어나긴 하지만 증가 폭은 비교적 완만하다.

 

 

이후 다시 snapshot을 찍어 getName을 확인해보면,
우측 하단의 메서드 항목이 COUNT 수만큼 증가하지 않았음을 확인할 수 있다.

 

이는 프로토타입 패턴에서

인스턴스의 수가 늘어나도 메서드는 prototype에 단 하나만 존재하며,

모든 인스턴스가 동일한 참조를 공유한다는 사실을 보여준다.


클로저 패턴 실험 결과

이번에는 이전 snapshot을 모두 제거하고,
브라우저와 콘솔을 초기화한 뒤 클로저 패턴으로 동일한 실험을 진행했다.

 

 

실험 전 snapshot에서 getName을 검색하면
프로토타입 실험과 마찬가지로 아직 PrivatePersongetName보이지 않는다.

 

 

 

COUNT를 늘려가며 테스트한 결과는 다음과 같다.

  • 1만 개: 1.7128 ms
  • 5만 개: 31.5009 ms
  • 10만 개: 76.25 ms

프로토타입과 패턴과 비교하면,
COUNT 증가에 따라 소요 시간이 훨씬 가파르게 증가하는 것을 확인할 수 있다.

 

 

이를 그래프로 비교해보면 차이는 더욱 확실하다.

  • 프로토타입 패턴은 COUNT 증가에 따라 완만하게 증가
  • 클로저 패턴은 COUNT 증가에 따라 급격히 증가
    특히 5만  → 10만 구간에서 기울기가 크게 변화

왜 이런 차이가 발생했을까?

 

이제 클로저 패턴의 Heap Snapshot을 확인해보자.

 

우측 하단의 메서드 항목을 보면,

COUNT를 10만으로 설정한 snapshot에서는
getName 함수가 COUNT의 수만큼 존재함을 확인할 수 있다.

 

이는 곧 아래의 구조적 차이를 의미한다.

  • 프로토타입
    - 메서드가 prototype에 단 하나만 존재
    - 인스턴스 수가 늘어나도 함수 생성 비용은 거의 일정
  • 클로저
    - 인스턴스마다 새로운 함수와 새로운 LexicalEnvironment 생성
    - 인스턴스 수에 비례해 비용이 선형적으로 증가

이 실험을 통해

메모리 효율을 선택하면 데이터 은닉을 잃고,
데이터 은닉을 선택하면 메모리 효율을 잃는 구조적 한계를 데이터로 확인했다.

 

그렇다면 정말로 이 둘을 동시에 만족시키는 방법은 없을까?


4. 언어 차원의 해답 - ES2022의 #private

조사를 하던 중,
이 문제에 대한 언어 차원의 해답이 이미 도입되어 있다는 사실을 발견했다.

 

바로 ES2022의 클래스 전용 프라이빗 필드 문법이다.

class PrivateCounter {
  #count = 0;

  increment() {
    return ++this.#count;
  }
}

 

이 문법의 핵심은 
"접근하기 어렵다"가 아니라 "접근 자체가 불가능하다"는 점이다.

 

#count는 문법 레벨에서 접근이 차단된다.

 

인스턴스 외부에서 접근을 시도하는 순간 즉시 에러가 발생해,

어떤 트릭이나 우회도 허용되지 않는다.

 

const counter = new PrivateCounter();
counter.#count; // 에러 발생

 

이는 자바스크립트 엔진이 직접 강제하는 캡슐화다.


🔍  잠깐 짚고 가기 - 클래스는 프로토타입과 다른 개념일까?

여기서 한 가지 짚고 넘어갈 점이 있다.

 

자바스크립트는 여전히 프로토타입 기반 언어다.

전통적인 클래스 기반 언어처럼 별도의 클래스 상속 모델이 존재하지 않았다.

 

그럼에도 ES2015에서 class 문법이 도입된 이유는 명확하다.

기존 프로토타입 기반 구조를 더 읽기 쉽게 표현하기 위해서.

 

중요한 점은,

class새로운 객체 모델이 아니라는 사실이다.

 

아래 두 코드는 표현만 다를 뿐,

자바스크립트 엔진이 내부적으로 구성하는 구조는 완전히 동일하다.

// 프로토타입
function Person(name) {
  this.name = name;
}

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

// 클래스
class Person {
  constructor(name) {
    this.name = name;
  }

  getName() {
    return this.name;
  }
}

 

두 케이스 모두 getName은 프로토타입에 존재하며,
모든 인스턴스가 하나의 메서드를 공유한다.


4-1. 클래스의 #private 실험

그렇다면 클래스의 #private 필드는
정말로 프로토타입의 메모리 효율과 클로저의 데이터 은닉을 동시에 만족시킬 수 있을까?

 

이를 위해 다음과 같은 클래스를 준비했다.

class PrivateFieldPerson {
  #name;

  constructor(name) {
    this.#name = name;
  }

  getName() {
    return this.#name;
  }
}

 

이 코드를 앞서 실험했던 프로토타입 패턴, 클로저 패턴과
완전히 동일한 조건에서 테스트했다.

 

 

실험 시작 전 Heap Snapshot을 찍었다.

이 시점에는 getName을 사용한 PrivateFieldPerson 객체가 없는 것이 확인된다.

 

 

 

COUNT를 늘려가며 테스트한 결과는 다음과 같다.

  • 1만 개: 0.6381 ms
  • 5만 개: 0.7058 ms
  • 10만 개: 1.0859 ms

COUNT가 증가함에 따라 시간이 늘어나긴 했지만,

증가 폭이 프로토타입 패턴과 매우 유사하게 완만하다.

 

 

이후 Heap Snapshot을 다시 확인했다.

 

우측 하단을 보면 COUNT의 수만큼 getName이 늘어나지 않았다는 것을 확인할 수 있다.

 

이를 통해 데이터는 외부에서 은닉되었고,

getName 메서드는 복제되지 않았으며,

모든 인스턴스가 하나의 메서드를 공유하고 있다는 사실을 알 수 있다.

 

즉, 프로토타입의 메모리 효율과 클로저의 데이터 은닉이 동시에 만족된 구조다.

 

항목 프로토타입 클로저 #private
메모리 사용량 낮음 높음 낮음
메서드 공유
데이터 은닉
외부 접근 가능 불가 문법 오류

 

#private은 프로토타입의 메서드 공유라는 장점을 유지하면서,

클로저에서만 가능하다고 여겨졌던 데이터 은닉을 언어차원에서 제공한다.

 

ES2022의 #private"하나를 얻으면 하나를 포기해야 하는 구조"를 언어 차원에서 해결했다고 볼 수 있다.


4-2. #private의 동작 원리

앞선 실험에서 #private는 
프로토타입처럼 메서드는 공유되었고,
클로저처럼 데이터가 외부에서 완전히 차단되었다.

 

그렇다면 이런 동작은 어떤 구조이길래 가능한 걸까?

 

#private 필드는 인스턴스 단위로 분명히 존재한다.

 

하지만 이 상태는 this로도 접근할 수 없고,

프로퍼티 탐색 과정에서도 전혀 드러나지 않는다.

 

즉, 언어 문법 차원에서 접근 경로 자체가 설계되지 않았다는 것이다.

 

클래스 문법을 사용해도 자바스크립트 내부 구조는 여전히 프로토타입 기반이다.

 

따라서 메서드는 인스턴스가 아닌 prototype에 위치하고,

모든 인스턴스가 하나의 메서드를 공유한다.

 

이 구조 덕분에 클로저 방식처럼 인스턴스마다 함수가 복제되지 않고,

메모리 사용량도 낮게 유지된다.

 

구조를 한 번에 정리하면 다음과 같다.

  • 상태: 인스턴스 전용 프라이빗 슬롯
  • 행동: 프로토타입에서 공유
  • 접근 제어: 자바스크립트 엔진이 강제

이 세 역할이 명확히 분리 되었기 때문에,

기존의 "하나를 얻으면 하나를 포기해야 하는 구조"가 사라진 것이다.

그렇다면 왜 이건 클래스에서만 가능할까?

 

이런 강한 접근 제어는 구조가 정형화된 문법 안에서만 가능하다.

 

자유도가 높은 함수 + 프로토타입 조합에서는 엔진이 접근을 강제하기 어렵다.

 

그래서 #private은 클래스라는 문법적 틀 안에서만 제공된다.

 

즉, 클래스는 기존 프로토타입 문법 구조를 더 읽기 쉽게 표현하기 위해 등장했지만

그 구조 위에서 언어 차원의 캡슐화를 의도적으로 확장할 수 있는 기반이 되었다.


4-3. #private을 거의 보지 못 한 이유

그런데 왜 #private을 거의 본 적이 없을까?

 

본인이 개발을 할 때 다른 사람의 코드에서 #private을 보지 못 했던 이유를 생각해 봤다.

 

그 이유는 단순했다.

 

#private은 위에서 말했듯 클래스에서만 사용 가능하기 때문이다.

 

현대 프론트엔드 개발 환경에서는

클래스 컴포넌트에서 함수 컴포넌트와 Hook 기반으로 완전히 전환되었고,

데이터를 객체 내부에 숨기기보다는 불변성을 유지하며 외부로 드러내고 관리하는 흐름이 주가 되었으며,

많은 프론트엔드 개발자가 TypeScript를 사용하고 있다.

 

TypeScript에는 이미 private 키워드가 존재한다.

  • TS의 private: 컴파일 타임에만 체크되고 런타임에는 사라짐
  • JS의 #private: 런타임에서도 접근 자체가 불가능

대부분의 경우 컴파일 타임 보호만으로 충분했기 때문에,

굳이 #private을 섞어 사용할 필요가 없었던 것이다.

 

즉, 사람들이 #private을 사용하지 않았던 이유는

성능이나 완성도의 문제가 아니라 이미 익숙한 방식이 있었기 때문이다.

 

다만 최근에는 라이브러리 개발이나 복잡한 비즈니스 로직을 클래스로 설계할 때

#private을 사용하는 경우도 늘어나고 있다고 한다.


5. 이 글을 마치며

프로토타입은 메모리 면에서 유리하지만 데이터 은닉은 불가능했고,

클로저는 데이터 은닉은 완벽했지만 메모리 비용이 증가했다.

 

이 한계는 선택의 문제가 아니라 구조의 문제였다.

 

#private은 이 구조를 분리함으로써

자바스크립트가 오랫동안 안고 있던 문제에 

언어 차원의 해답을 제시했다.

 

그리고 이 과정을 직접 실험하고 검증하면서,

"이 문제는 구조적으로 어디까지 가능한가"를 바라보는 시선을 갖게 된 것 같다.