[JavaScript]_Private class features

hanseungjune·2023년 7월 6일
0

JavaScript

목록 보기
86/87
post-thumbnail

📃 원본 링크

🖨️ 번역본

Private class features

클래스 필드는 기본적으로 공개(public)이지만, 해시(#) 접두사를 사용하여 비공개(private) 클래스 멤버를 생성할 수 있습니다. 이러한 클래스 기능들의 개인 정보 보호는 JavaScript 자체에 의해 강제됩니다.

비공개 멤버는 이 문법이 존재하기 전에는 언어 자체에 내장되어 있지 않았습니다. 프로토타입 상속에서는 WeakMap 객체나 클로저를 사용하여 동작을 흉내 낼 수 있지만, # 문법의 편의성과는 비교할 수 없습니다.

문법

class ClassWithPrivate {
  #privateField;
  #privateFieldWithInitializer = 42;

  #privateMethod() {
    // ...
  }

  static #privateStaticField;
  static #privateStaticFieldWithInitializer = 42;

  static #privateStaticMethod() {
    // ...
  }
}

추가적인 문법 제약사항이 있습니다.

  • 클래스 내에서 선언된 모든 비공개 식별자는 고유해야 합니다. 네임스페이스는 정적(static) 및 인스턴스(instance) 속성 사이에서 공유됩니다. 유일한 예외는 두 선언이 getter-setter 쌍을 정의할 때입니다.
  • 비공개 식별자는 #constructor일 수 없습니다.

설명

대부분의 클래스 기능은 비공개 상황에서의 대응 기능이 있습니다:

  • 비공개 필드
  • 비공개 메서드
  • 비공개 정적 필드
  • 비공개 정적 메서드
  • 비공개 게터
  • 비공개 세터
  • 비공개 정적 게터
  • 비공개 정적 세터

이러한 기능들은 비공개 속성(private properties)이라고 일컫습니다. 그러나 생성자는 JavaScript에서 비공개로 만들 수 없습니다. 클래스 외부에서 클래스의 인스턴스를 생성하는 것을 방지하기 위해서는 비공개 플래그를 사용해야 합니다.

비공개 속성은 # 이름(해시 이름)으로 선언되며, # 접두사가 있는 식별자입니다. 해시 접두사는 속성 이름의 고유한 부분입니다. 이전의 언더스코어(_) 접두사 관례와 관련이 있지만, 일반적인 문자열 속성이 아니므로 대괄호 표기법으로 동적으로 액세스할 수는 없습니다.

클래스 외부에서 # 이름을 참조하는 것은 구문 오류입니다. 또한 클래스 본문에서 선언되지 않은 비공개 속성을 참조하거나 선언된 속성을 삭제하려고 하면 구문 오류가 발생합니다.

class ClassWithPrivateField {
  #privateField;

  constructor() {
    this.#privateField = 42;
    delete this.#privateField; // 구문 오류
    this.#undeclaredField = 444; // 구문 오류
  }
}

const instance = new ClassWithPrivateField();
instance.#privateField === 42; // 구문 오류

JavaScript는 동적인 언어이므로 이러한 컴파일 타임 확인을 수행할 수 있습니다. 이는 해시 식별자 구문을 통해 가능한데, 이로 인해 일반적인 속성과는 구문 수준에서 다른 특성을 가지게 됩니다.

참고: Chrome 콘솔에서 실행되는 코드는 클래스 외부에서 비공개 속성에 액세스할 수 있습니다. 이는 JavaScript 구문 제한의 개발자 도구에서만 허용됩니다.

객체에서 비공개 속성을 액세스할 때 해당 속성이 없는 경우, 일반적인 속성과는 달리 undefined를 반환하는 대신 TypeError가 발생합니다.

class C {
  #x;

  static getX(obj) {
    return obj.#x;
  }
}

console.log(C.getX(new C())); // undefined
console.log(C.getX({})); // TypeError: Cannot read private member #x from an object whose class did not declare it

in 연산자를 사용하여 가능성 있는 비공개 필드(또는 비공개 메서드)를 확인할 수 있습니다. 이렇게 하면 비공개 필드나 메서드가 존재하는 경우 true를 반환하고, 그렇지 않으면 false를 반환합니다.

비공개 필드가 누락될 가능성이 있는지 (또는 비공개 메서드) 확인하기 위해 in 연산자를 사용할 수 있습니다. 이는 비공개 필드나 메서드가 있는 경우 true를 반환하고, 그렇지 않으면 false를 반환합니다.

비공개 이름이 항상 미리 선언되고 삭제할 수 없다는 사실로부터의 직접적인 결과는 다음과 같습니다: 현재 클래스의 비공개 속성 중 하나를 가진 객체를 찾았다면 (try...catchin 확인 중 얻은 경우), 모든 다른 비공개 속성을 가지고 있을 것입니다. 클래스의 비공개 속성을 가진 객체는 일반적으로 해당 클래스에 의해 생성된 것을 의미합니다 (항상 그렇지는 않음에 유의).

비공개 속성은 프로토타입 상속 모델의 일부가 아니며, 현재 클래스의 본문 내에서만 액세스할 수 있으며 하위 클래스에 상속되지 않습니다. 서로 다른 클래스 내에서 동일한 이름의 비공개 속성은 완전히 다른 속성으로 간주되며 서로 연동되지 않습니다. 이들은 각 인스턴스에 부착된 외부 메타데이터로 생각하고 클래스가 관리하는 것으로 관리됩니다.

비공개 속성에는 프로토타입 상속 모델에 포함되지 않으므로 현재 클래스 본문 내에서만 액세스할 수 있으며 하위 클래스에서 상속되지 않습니다. 서로 다른 클래스 내에서 동일한 이름의 비공개 속성은 완전히 다르며 상호 작용하지 않습니다. 이들은 클래스에 의해 생성된 각 인스턴스에 부착된 외부 메타데이터로 간주되고 클래스에 의해 관리됩니다.

비공개 필드와 메서드는 클래스의 내부에서 데이터와 기능을 은닉하는 데 사용됩니다. 이는 클래스를 조직화하고 외부에 노출되는 인터페이스를 제한하는 데 도움이 됩니다. 비공개 필드와 메서드는 클래스의 내부 구현 세부 정보를 숨기고 클래스의 외부 사용자가 잘못된 방식으로 클래스를 변경하거나 접근하는 것을 방지하는 데 도움이 됩니다.

비공개 속성은 클래스의 일부로써 표현되며, 클래스의 인스턴스에 연결된 외부 메타데이터로서의 역할을 합니다. 이를 통해 클래스의 내부 상태와 동작을 관리하고, 클래스를 보다 견고하고 유지보수 가능한 코드로 만들 수 있습니다.

비공개 필드

비공개 필드에는 비공개 인스턴스 필드와 비공개 정적 필드가 있습니다. 비공개 필드는 클래스 선언 내부에서만 액세스할 수 있습니다.

비공개 인스턴스 필드

공개(public) 필드와 마찬가지로 비공개 인스턴스 필드는 다음과 같은 특징을 가집니다:

  • 기본 클래스의 생성자가 실행되기 전에 추가되거나, 서브클래스의 경우 super()가 호출된 직후에 추가됩니다.
  • 클래스의 인스턴스에서만 사용할 수 있습니다.
class ClassWithPrivateField {
  #privateField;

  constructor() {
    this.#privateField = 42;
  }
}

class Subclass extends ClassWithPrivateField {
  #subPrivateField;

  constructor() {
    super();
    this.#subPrivateField = 23;
  }
}

new Subclass(); // 일부 개발 도구에서는 Subclass {#privateField: 42, #subPrivateField: 23}와 같이 표시됩니다.

참고: ClassWithPrivateField 기본 클래스의 #privateField는 ClassWithPrivateField에만 비공개이며, Subclass에서는 접근할 수 없습니다.

반환된 객체 재정의

클래스의 생성자는 새로운 this로 사용할 다른 객체를 반환할 수 있습니다. 파생 클래스에서 private 필드를 해당 반환된 객체에 정의할 수 있습니다. 이는 관련 없는 객체에 private 필드를 "찍어낼" 수 있는 것을 의미합니다.

class Stamper extends class {
  // 생성자가 주어진 객체를 반환하는 기본 클래스
  constructor(obj) {
    return obj;
  }
} {
  // 이 선언은 기본 클래스 생성자가 반환한 객체에 private 필드를 "찍어냅니다."
  #stamp = 42;
  static getStamp(obj) {
    return obj.#stamp;
  }
}

const obj = {};
new Stamper(obj);
// `Stamper`는 `Base`를 호출하고 `obj`를 반환하므로 `obj`가
// 새로운 `this` 값이 됩니다. `Stamper`는 `obj`에 `#stamp`를 정의합니다.

console.log(obj); // 일부 개발 도구에서는 {#stamp: 42}와 같이 표시됩니다.
console.log(Stamper.getStamp(obj)); // 42
console.log(obj instanceof Stamper); // false

경고: 이는 혼란스러울 수 있는 작업입니다. 일반적으로 생성자에서 아무것도 반환하지 않는 것이 권장됩니다. 특히 this와 관련이 없는 것을 반환하는 경우에는 더욱 그렇습니다.

비공개 정적 필드

공개(public) 정적 필드와 마찬가지로 비공개 정적 필드는 다음과 같은 특징을 가집니다:

  • 클래스 평가 시 클래스 생성자에 추가됩니다.
  • 클래스 자체에서만 사용할 수 있습니다.
class ClassWithPrivateStaticField {
  static #privateStaticField = 42;

  static publicStaticMethod() {
    return ClassWithPrivateStaticField.#privateStaticField;
  }
}

console.log(ClassWithPrivateStaticField.publicStaticMethod()); // 42

비공개 정적 필드에는 제약이 있습니다: 비공개 정적 필드를 정의한 클래스만이 해당 필드에 액세스할 수 있습니다. 이는 this를 사용할 때 예상치 못한 동작을 일으킬 수 있습니다. 다음 예제에서는 Subclass.publicStaticMethod()을 호출할 때 thisSubclass 클래스(아닌 ClassWithPrivateStaticField 클래스)를 가리키므로 TypeError가 발생합니다.

class ClassWithPrivateStaticField {
  static #privateStaticField = 42;

  static publicStaticMethod() {
    return this.#privateStaticField;
  }
}

class Subclass extends ClassWithPrivateStaticField {}

Subclass.publicStaticMethod(); // TypeError: Cannot read private member #privateStaticField from an object whose class did not declare it

super를 사용하여 메서드를 호출할 때도 마찬가지입니다. super 메서드는 super 클래스가 this로 호출되지 않으므로 동일한 문제가 발생합니다.

class ClassWithPrivateStaticField {
  static #privateStaticField = 42;

  static publicStaticMethod() {
    // super를 통해 호출될 때 `this`는 여전히 Subclass를 가리킵니다.
    return this.#privateStaticField;
  }
}

class Subclass extends ClassWithPrivateStaticField {
  static callSuperMethod() {
    return super.publicStaticMethod();
  }
}

Subclass.callSuperMethod(); // TypeError: Cannot read private member #privateStaticField from an object whose class did not declare it

메서드를 호출할 때는 항상 this 대신 클래스 이름을 통해 비공개 정적 필드에 접근하는 것이 좋습니다. 이렇게 하면 상속이 메서드를 깨뜨리지 않습니다.

비공개 메서드

비공개 메서드에는 비공개 인스턴스 메서드와 비공개 정적 메서드가 있습니다. 비공개 메서드는 클래스 선언 내부에서만 액세스할 수 있습니다.

비공개 인스턴스 메서드

공개(public) 메서드와는 달리 비공개 인스턴스 메서드는 다음과 같은 특징을 가집니다:

  • 인스턴스 필드가 설치되기 바로 전에 설치됩니다.
  • 클래스의 .prototype 속성이 아닌 클래스의 인스턴스에서만 사용할 수 있습니다.
class ClassWithPrivateMethod {
  #privateMethod() {
    return 42;
  }

  publicMethod() {
    return this.#privateMethod();
  }
}

const instance = new ClassWithPrivateMethod();
console.log(instance.publicMethod()); // 42

비공개 인스턴스 메서드는 제너레이터(generator), 비동기(asyhnc), 비동기 제너레이터(async generator) 함수일 수 있습니다. 비공개 gettersetter도 가능하며, 공개 gettersetter와 동일한 구문 요구사항을 따릅니다.

class ClassWithPrivateAccessor {
  #message;

  get #decoratedMessage() {
    return `🎬${this.#message}🛑`;
  }
  set #decoratedMessage(msg) {
    this.#message = msg;
  }

  constructor() {
    this.#decoratedMessage = "hello world";
    console.log(this.#decoratedMessage);
  }
}

new ClassWithPrivateAccessor(); // 🎬hello world🛑

공개 메서드와 달리 비공개 메서드는 클래스의 .prototype 속성에서 액세스할 수 없습니다.

class C {
  #method() {}

  static getMethod(x) {
    return x.#method;
  }
}

console.log(C.getMethod(new C())); // [Function: #method]
console.log(C.getMethod(C.prototype)); // TypeError: Receiver must be an instance of class C

비공개 정적 메서드

공개 정적 메서드와 마찬가지로 비공개 정적 메서드는 다음과 같은 특징을 가집니다:

  • 클래스 평가 시 클래스 생성자에 추가됩니다.
  • 클래스 자체에서만 사용할 수 있습니다.
class ClassWithPrivateStaticMethod {
  static #privateStaticMethod() {
    return 42;
  }

  static publicStaticMethod() {
    return ClassWithPrivateStaticMethod.#privateStaticMethod();
  }
}

console.log(ClassWithPrivateStaticMethod.publicStaticMethod()); // 42

비공개 정적 메서드는 제너레이터(generator), 비동기(async), 비동기 제너레이터(async generator) 함수일 수 있습니다.

비공개 정적 필드에 대해 이전에 언급한 제한이 비공개 정적 메서드에도 적용되며, 마찬가지로 this 사용 시 예상치 못한 동작을 일으킬 수 있습니다. 다음 예제에서는 Subclass.publicStaticMethod()를 호출할 때 thisSubclass 클래스(ClassWithPrivateStaticMethod 클래스가 아닌)를 가리키므로 TypeError가 발생합니다.

class ClassWithPrivateStaticMethod {
  static #privateStaticMethod() {
    return 42;
  }

  static publicStaticMethod() {
    return this.#privateStaticMethod();
  }
}

class Subclass extends ClassWithPrivateStaticMethod {}

console.log(Subclass.publicStaticMethod()); // TypeError: Cannot read private member #privateStaticMethod from an object whose class did not declare it

비공개 생성자 모방

다른 많은 언어에서는 생성자를 비공개로 표시하여 클래스 외부에서 인스턴스를 생성하는 것을 방지할 수 있습니다. 즉, 인스턴스를 생성하는 정적 팩토리 메서드(static factory method)만 사용하거나 인스턴스를 생성할 수 없도록 할 수 있습니다. JavaScript에는 이를 위한 기본 기능이 없지만, 비공개 정적 플래그(private static flag)를 사용하여 구현할 수 있습니다.

class PrivateConstructor {
  static #isInternalConstructing = false;

  constructor() {
    if (!PrivateConstructor.#isInternalConstructing) {
      throw new TypeError("PrivateConstructor is not constructable");
    }
    PrivateConstructor.#isInternalConstructing = false;
    // 추가적인 초기화 로직
  }

  static create() {
    PrivateConstructor.#isInternalConstructing = true;
    const instance = new PrivateConstructor();
    return instance;
  }
}

new PrivateConstructor(); // TypeError: PrivateConstructor is not constructable
PrivateConstructor.create(); // PrivateConstructor {}

후기

사실 지금 이렇게 번역했지만... 내용을 전부 이해하지는 못했다. 그래도 나중에 다시 번역해서 이해하기 보다는 미리 번역해두면 더 편할 것 같아서 미리 번역해두는 거다. 일단 간단하게 예습 정도 해야할 일이 있어서 이렇게 글을 작성했고 다음에 더 깊게 공부하게 된다면, 해당 자료를 꼭 참고해야겠다.

profile
필요하다면 공부하는 개발자, 한승준

0개의 댓글