히든 클래스

자바스크립트는 동적 타이핑 언어로, 변수 타입을 실행 시점에 값이 할당 될 때 결정함.
이는 사전형 객체(Dictionary Object)를 관리할 때, 속성을 찾을 때마다 메모리를 검색해야 하기 때문에 속도가 느려지는 문제를 유발함.

따라서 v8엔진은 히든 클래스(Hidden Class)를 사용하여 해당 성능 문제를 해결하고 자바스크립트 실행을 최적화함.

히든 클래스의 정의

객체의 프로퍼티 구조(이름·순서·오프셋)를 내부적으로 표현한 메타데이터로, 자바스크립트 객체를 정적 언어의 구조체처럼 빠르게 접근하기 위해 V8이 사용하는 최적화 기법임.

동적 언어인 JS는 프로퍼티의 속성을 자유롭게 변경할 수 있다는 장점이 있지만, 이로 인해 프로퍼티의 메모리 오프셋을 컴파일 시점에 결정하는 것이 불가능함.

사전형 탐색 (Dictionary Lookup)

프로퍼티 이름을 키(Key)로 삼아 매번 탐색해서 값을 찾는 방식

자바스크립트 객체는 “키(Key): 값(Value)”의 쌍으로 이루어진 **해시 테이블(Hash Table)**이고, 예를 들어 user.name에 접근하려고 하면 엔진은

  1. 속성 이름 name을 해시 함수에 통과시켜 해시값을 얻고,
  2. 해당 해시값에 대응하는 버킷(메모리 공간)을 찾고,
  3. 버킷 안에 실제 내가 찾는 name이 있는지 확인하고,
  4. 메모리 주소에 접근해 값을 읽어옴.

이 과정은 속성에 접근할 때마다 반복되어야 함.

이는 정적 언어(Java 혹은 C++ 등)에서 클래스 선언 시 이미 속성의 위치(Memory Offset)이 고정되어 있기 때문에, 시작점에서 8byte 뒤가 name임을 알고 주소값으로 즉시 점프하는 것에 비해

동적 언어에서는 더 많은 CPU 연산을 필요로함을 알 수 있음.

또한, 객체의 모양이 언제든 변할 수 있다는 전제 하에 움직이기 때문에 캐싱이 불가함.

따라서 V8엔진은 이와 같은 원리에 착안하여 동적 언어의 유연함을 유지하고, 사전형 탐색을 대체하는 수단으로 히든 클래스를 채택함.

히든 클래스의 동작 방식

히든 클래스는

  • 객체 생성 시점에 결정되어,
  • 객체 내 프로퍼티의 메모리 오프셋(상대적 위치)을 저장하고,
  • 객체에 프로퍼티가 추가될 때마다 엔진이 기존 히든 클래스를 수정하는 대신, 새로운 히든 클래스를 생성하고, 기존 클래스와 연결(Transition)하는 방식으로 동작함.

예를 들어, 아래와 같이 빈 객체를 생성하면,

const user = {};

V8엔진은 오프셋 없이 해당하는 히든 클래스(C01)을 생성함.

js-hiddenclass-01.png

해당 빈 객체에 프로퍼티를 추가하면,

user.name = "Hailey";

V8엔진은 이전 히든 클래스(C01)의 모든 프로퍼티를 상속하여 새 히든 클래스(C02)를 생성하고,

이전 히든 클래스(C01)dpsms name 프로퍼티를 추가하는 경우, 새 히든 클래스(C02)로 전환된다는 전환정보(Transition Information)를 남김.

js-hiddenclass-02.png

따라서 V8엔진은 이를 통해 사전형 탐색(Dictionary Lookup)을 우회할 수 있음.

같은 맥락으로 user객체에 또 다른 프로퍼티(age)를 추가하면,

user.age = 30;

이전 히든 클래스(C02)의 모든 프로퍼티를 상속하여, 새 히든 클래스(C03)를 생성하고,

이전 히든 클래스(C02)에는 age 프로퍼티가 추가되면 새 히든 클래스(C03)으로 전환된다는 정보를 담음.

js-hiddenclass-03.png

따라서 최종 히든 클래스는 C03이 되며, 빈 객체에 같은 순서(name → age)로 프로퍼티를 추가하는 경우에 언제나 C03을 참조함.

이는 이후에 같은 동작(빈 객체를 생성 → name 프로퍼티 추가 → age 프로퍼티 추가)을 수행할 때의 사전형 탐색을 대체함.

생성자 함수와 히든 클래스

const user = {};
user.name = "Hailey";
user.age = 30;

위에서 언급한 히든 클래스의 생성 방식은 생성자 함수로 객체를 생성할 때에만 해당하는데, 엔진은 전환경로(Transition Path, C01C02C03)를 기억하여 같은 순서로 생성되는 모든 객체에 C03을 사용함.

따라서 생성자 함수로 객체를 선언하는 방식은 같은 형식의 데이터를 대량으로 처리해야 할 때 최적임.

객체 리터럴과 히든 클래스

const user = {
	name: "Hailey",
	age: 30,
};

객체 리터럴은 선언하는 순간에 모든 속성값이 정해져 있으므로,

엔진은 해당 객체의 모양을 보고 해당 모양의 히든 클래스를 생성하고, 같은 형태({name: (string), age: (number)})의 객체를 생성하게 되는 경우에 해당 클래스를 참조함.

따라서 리터럴로 객체를 선언하는 방식은 같은 모양의 단순한 데이터 묶음을 처리하는 것에 최적임.

히든 클래스를 고려한 성능 최적화

  1. 속성 추가 순서를 고정하기

    위에서 언급한 바와 같이 엔진은 프로퍼티 순서가 다르면 다른 클래스로 인식함. 따라서 담고 있는 데이터가 같더라도 속성 순서가 다르면 별개의 클래스로 인식하기 때문에, 메모리 낭비와 속도 저하를 초래함.

  2. 속성을 미리 정의하기

    • ❌ 나쁜 예 - 동적 추가

      function User(name) {
        this.name = name;
      }
      
      const user1 = new User('Alice');
      user1.age = 25; // 생성자 밖에서 추가 (Transition 발생)
      
    • ✅ 좋은 예 - 초기화 시, 모든 속성 정의

      function User(name, age) {
        this.name = name;
        this.age = age || null; // 처음부터 틀을 잡아둠
      }
      
  3. delete 대신 null 사용하기

    delete로 프로퍼티를 삭제하는 경우, 엔진은 해당 객체가 기존 형태와 달라졌다고 판단하기 때문에 Dictionary Mode로 강등시킴. 해당 모드로 전환 시, 오프셋을 이용한 빠른 접근이 불가능해지기 때문에 속성 자체를 삭제하기보다undefined나 null로 할당하는 것이 좋음.