본문 바로가기

JavaScript

Implementing Private Fields for JavaScript (한글)

이 글은 아래 원문을 번역한 글로 의역이 있을 수 있습니다. 정확한 의미를 파악하고 싶으신 분은 원문을 참고해주시기 바랍니다.

 

원문: https://hacks.mozilla.org/2021/06/implementing-private-fields-for-javascript/

저작권 정보: https://creativecommons.org/licenses/by-sa/3.0/


이 게시물은 Matthew Gaudet의 블로그에서 교차 게시되었습니다.

 

JavaScript용 언어 기능을 구현할 때, 구현자는 사양의 언어가 구현에 매핑되는 방법에 대한 결정을 내려야 합니다. 때로는 사양과 구현이 동일한 용어와 알고리즘의 대부분을 공유할 수 있는 상당히 간단한 방법이기도 합니다. 또한 구현에 대한 압박으로 인해 구현 전략이 언어 사양에서 벗어나도록 요구하거나 압박하는 일이 더욱 어려워질 수 있습니다.


Private fields는 Firefox를 구동하는 JavaScript 엔진인 SpiderMonkey에서 사양 언어와 구현 현실이 어떻게 다른지 보여주는 예입니다. 좀 더 이해하기 위해  private fields가 무엇인지,  몇 가지 모델에 대해 설명하고, 당사의 구현이 사양 언어와 다른 이유에 대해 설명하겠습니다.

 

Private Fields

 

Private fields는 TC39 프로세스의 4단계에 있는 클래스 필드 제안의 일부로 TC39 제안 프로세스를 통해 JavaScript 언어에 추가되는 언어 기능입니다. Firefox 90에서 private fieldsprivate methods를 사용 가능하도록 할 것입니다.


 private fields 제안은 언어에  ‘private state’라는 엄격한 개념을 추가합니다. 다음 예에서 #x는 클래스 A의 인스턴스만 액세스 할 수 있습니다.

class A {
  #x = 10;
}


즉, 클래스 외부에서는 해당 필드에 액세스 할 수 없습니다. 예를 들어, 다음 예제와 같이 public fields와는 다릅니다.

class A {
  #x = 10; // Private field
  y = 12; // Public Field
}

var a = new A();
a.y; // Accessing public field y: OK
a.#x; // Syntax error: reference to undeclared private field

JavaScript에서 객체를 조회하기 위해 제공하는 다양한 다른 도구도 private fields에 액세스할 수 없습니다(예: Object.getOwnProperty{Symbols,Names}private fields를 나열하지 않습니다. Reflect.get을 사용하여 해당 필드에 액세스 할 수 있는 방법은 없습니다.)

 

A Feature Three Ways

JavaScript의 기능에 대해 이야기할 때 세 가지 다른 측면, 즉 the mental model, the specification, the implementation이 있습니다.

 

멘탈 모델은 프로그래머들이 주로 사용하길 기대하는 높은 수준의 사고를 제공합니다. 사양은 차례로 기능에 필요한 의미론의 세부사항을 제공합니다. 사양 의미론이 유지되는 한 구현은 사양 텍스트와 크게 다르게 보일 수 있습니다.

 

이 세 가지 측면은 사물을 통해 추론하는 사람들에 대해 다른 결과를 만들어내서는 안 된다. (그러나, 때때로 'mental model'은 약칭이며, 에지 케이스 시나리오에서 의미론을 정확하게 포착하지 못한다.)

 

다음 세 가지 측면을 사용하여 private fields를 살펴볼 수 있습니다.

Mental Model

private fields에 대해 가질 수 있는 가장 기본적인 정신적 모델은 주석에서 말하는  fields이지만 사적인 것이다. 이제 JS 필드는 객체의 속성이 되므로 멘탈 모델은 '클래스 외부에서 액세스 할 수 없는 속성'일 수 있습니다.

 

그러나 프록시를 접하면 이 멘탈 모델은 약간 고장이 납니다. '숨겨진 속성' 및 프록시에 대한 의미론을 지정하는 것은 어렵습니다(프록시가 속성에 대한 액세스 제어를 제공하려고 하면 프록시로 private fields를 볼 수 없는 경우 어떻게 됩니까? 하위 클래스가 private fields에 액세스할 수 있습니까? private fieldsprototype상속에 참여합니까?) 원하는 개인 정보 속성을 보존하기 위해 committee private fields에 대해 생각하는 방식이 대체 정신 모델이 되었습니다.

 

이 대체 모델을 'WeakMap' 모델이라고 합니다. 이 mental model에서는 각 클래스가 각각의 private field과 관련된 숨겨진  weak map를 가지고 있다고 상상합니다. 그래서 여러분은 가정할 때 'desugar'을 할 수 있습니다.

class A {
  #x = 15;
  g() {
    return this.#x;
  }
}

//into something like

class A_desugared {
  static InaccessibleWeakMap_x = new WeakMap();
  constructor() {
    A_desugared.InaccessibleWeakMap_x.set(this, 15);
  }

  g() {
    return A_desugared.InaccessibleWeakMap_x.get(this);
  }
}

놀랍게도 WeakMap 모델은 사양에 피쳐가 기록되는 방식이 아니라 설계 의도에서 중요한 부분을 차지합니다. 나중에 이 멘탈 모델이 어떤 모습으로 나타나는지 조금 후에 다루겠습니다.

 

Specification

실제 사양 변경은 클래스 필드 제안, 특히 사양 텍스트의 변경에 의해 제공됩니다. 이 사양 텍스트의 모든 부분을 다루지는 않겠지만, 사양 텍스트와 구현의 차이점을 설명하는 데 도움이 되도록 구체적인 측면을 설명하겠습니다.


먼저, 사양은 전역적으로 고유한 필드 식별자인 [[PrivateName]]개념을 추가합니다. 이러한 글로벌 고유성은 두 클래스가 동일한 이름만 가지고 서로의 필드에 액세스할 수 없도록 하는 것입니다.

function createClass() {
  return class {
    #x = 1;
    static getX(o) {
      return o.#x;
    }
  };
}

let [A, B] = [0, 1].map(createClass);
let a = new A();
let b = new B();

A.getX(a); // Allowed: Same class
A.getX(b); // Type Error, because different class.

사양은 또한 모든 객체에[[PrivateFieldValues]]라는 사양의 객체와 연결된 내부 상태의 사양 수준 조각인 새 '내부 슬롯'을 추가합니다. [[PrivateFieldValues]]는 다음 형식의 레코드 목록입니다.

{
  [[PrivateName]]: Private Name,
  [[PrivateFieldValue]]: ECMAScript value
}

이 목록을 조작하기 위해 사양에는 다음과 같은 네 가지 새로운 알고리즘이 추가됩니다.

 

1.PrivateFieldFind

2.PrivateFieldAdd

3.PrivateFieldGet

4.PrivateFieldSet

 

이러한 알고리즘은 일반적으로 예상대로 작동합니다. PrivateFieldAdd 는 목록에 항목을 추가합니다(단, 오류를 제공하기 위해 목록에 일치하는 PrivateName이 이미 있으면  TypeError가 발생합니다). 어떻게 그런 일이 일어날 수 있는지 나중에 보여주겠다.) PrivateFieldGet은 지정된 Private 이름으로 입력된 목록에 저장된 값을 검색합니다.


The Constructor Override Tric

처음 사양을 읽기 시작했을 때 PrivateFieldAdd 가 던질 수 있다는 사실에 놀랐습니다. 구성 중인 객체의 생성자로부터만 호출된다는 점을 감안할 때, 객체가 새로 생성될 것으로 충분히 예상했으므로 필드가 이미 존재하는 것에 대해 걱정할 필요가 없습니다.

 

이는 일부 사양이 생성자 반환 값을 처리하는 데 따른 부작용으로 확인되었습니다. 좀 더 구체적으로 말하자면, André Bargull이 제공한 예는 다음과 같습니다.

class Base {
  constructor(o) {
    return o; // Note: We are returning the argument!
  }
}

class Stamper extends Base {
  #x = "stamped";
  static getX(o) {
    return o.#x;
  }
}

Stamper private field를 임의의 객체에 '스탬프'할 수 있는 클래스입니다.

let obj = {};
new Stamper(obj); // obj now has private field #x
Stamper.getX(obj); // => "stamped"

즉, 객체에 private field를 추가할 때 이미 private field가 없다고 가정할 수 없습니다. 여기서 PrivateFieldAdd의 사전 존재 검사가 실행됩니다.

let obj2 = {};
new Stamper(obj2);
new Stamper(obj2); // Throws 'TypeError' due to pre-existence of private field

private field를 임의 객체에 스탬프 하는 기능은 여기서도 WeakMap 모델과 상호 작용합니다. 예를 들어, private field를 객체에 스탬프 할 수 있는 경우, private field봉인된 객체에 스탬프할 수도 있습니다.

var obj3 = {};
Object.seal(obj3);
new Stamper(obj3);
Stamper.getX(obj3); // => "stamped"

private field를 속성으로 생각하면 프로그래머에 의해 봉인된 객체를 향후 수정에 따라 수정한다는 의미이기 때문에 불편합니다. 그러나  weak map model에서는 봉인된 객체를  weak map의 키로만 사용하므로 완전히 허용됩니다.

 

PS: private field를 임의 객체에 스탬프 할 수 있다고 해서 반드시 해야 하는 것은 아닙니다. 제발 이러지 마십시오.

 

Implementing the Specification

사양의 구현에 직면할 때, 사양의 문자를 따르는 것과 일부 차원에서 구현을 개선하기 위해 다른 것을 하는 것 사이에 tension이 있습니다. 

 

사양의 단계를 직접 구현할 수 있는 경우, 사양 변경이 이루어짐에 따라 기능 유지관리가 쉬워지기 때문에 그렇게 하는 것이 좋습니다. SpiderMonkey는 많은 곳에서 이렇게 합니다. comments를 위한 스텝 번호가 있는 사양 알고리즘의 사본인  코드의 부분을 보게 될 것입니다. 사양의 정확한 문자를 따르는 것도 사양이 매우 복잡하고 작은 차이가 호환성 위험을 초래할 수 있는 경우에 도움이 될 수 있습니다.

 

그러나 때로는 사양 언어에서 벗어나야 할 충분한 이유가 있습니다. 자바스크립트 구현은 수년간 고성능을 위해 발전되어 왔으며 이를 위해 적용된 구현 트릭도 많다. 새로운 코드가 이미 작성된 코드의 성능 특성을 가질 수 있다는 것을 의미하기 때문에 때때로 이미 작성된 코드의 일부를 다시 작성하는 것이 올바른 작업입니다.


Implementing Private Names

Private Name의 사양 언어가 이미 SpiderMonkey에 이미 있는  Symbols 주변의 semantics와 거의 일치합니다. 따라서PrivateNames을  Symbol로 추가하는 것은 꽤 쉬운 선택입니다.


Implementing Private Fields

private fields의 사양을 살펴보면, 사양 구현은 SpiderMonkey의 모든 객체에 숨겨진 슬롯을 추가하기 위한 것으로, 여기에는 {PrivateName, Value} 리스트에 대한 참조가 포함되어 있다. 그러나 이를 직접 구현하는 데는 여러 가지 분명한 단점이 있다.

  • private fields 없는 객체에 메모리 사용량을 추가함
  • 새로운 바이트 코드의 추가나 성능에 민감한 프로퍼티 접근 경로의 복잡성을 요구.

대안 옵션은 사양 언어에서 벗어나 실제 사양 알고리즘이 아닌 의미론만 구현하는 것이다. 대다수의 경우, 당신은 정말로 private fields를 class 밖에서 숨겨진 object에 대한 특별한 속성으로 생각할 수 있다.

 

객체와 함께 유지되는 특별한 사이드리스트가 아닌 private fields를 속성으로 모델링하면, 이미 자바스크립트 엔진에서 속성 조작이 극도로 최적화되어 있다는 점을 활용할 수 있다. 그러나 속성은 반영의 대상이 된다. 따라서 만약 우리가 private fields를 객체 속성으로 모델링한다면, reflection API가 그것들을 드러내지 않고 Proxy를 통해 접근할 수 없도록 해야 한다.

 SpiderMonkey에서는 엔진의 속성에 대해 이미 존재하는 optimized machinery를 이용하기 위해 숨겨진 속성으로 private fields를 구현하는 것을 선택했다.  이 기능을 구현하기 시작했을 때, 수년 동안 SpiderMonkey를 만들어온 André Bargull은 일련의 패치를 건네주었고, 그 패치는 이미 많은 수의  private fields 구현이 이루어져 있었고 대단히 감사했다.

 

우리의 특별한 PrivateName Symbols를 사용하여 효과적으로 삭제

class A {
  #x = 10;
  x() {
    return this.#x;
  }
}

//to something that looks closer to

class A_desugared {
  constructor() {
    this[PrivateSymbol(#x)] = 10;
  }
  x() {
    return this[PrivateSymbol(#x)];
  }
}

그러나 Private fields는 속성과는 약간 다른 의미론을 가지고 있다. 이들은 프로그래밍 오류로 예상되는 패턴에 대해 묵시적으로 수용하기보다는 오류를 발생시키도록 설계됐다. 예를 들면 다음과 같다.

  1. 속성이 없는 객체의 속성에 액세스하면 undefined.반환됨. Private fieldsPrivateFieldGet algorithm 의 결과로 TypeError를 발생시킴.
  2. 속성이 없는 객체에 속성을 설정하면 속성이 추가됩니다. Private fieldsPrivateFieldSetTypeError 발생시킬 것이다. 
  3. 해당 필드가 이미 있는 객체에 Private fields를 추가할 때도 PrivateFieldSetTypeError 발생시킬 것이다. 이 작업을 수행하는 방법은 위의 “The Constructor Override Trick”을

다른 의미론을 다루기 위해, 우리는 private field 접근을 위해 바이트 코드량을 수정했습니다. 새로운 바이트 코드 op와 객체가 지정된 private field에 대해 올바른 상태인지 확인하는 CheckPrivateField 추가했다.  즉, 속성이 없거나 Get/Set 또는 Add에 적절한 경우 예외가 발생합니다. CheckPrivateField는 일반 'computed property name' 경로( A [someKey]에 사용되는 경로)를 사용하기 직전에 방출됩니다.

 

CheckPrivateField  CacheIR을 사용하여 인라인 캐시를 쉽게 구현할 수 있도록 설계되어 있습니다. private fields를boolean 값을 반환하기만 하면 된다. SpiderMonkey에 있는 객체의 모양은 객체의 어떤 속성을 가지고 있는지, 해당 객체의 저장소에 있는 위치를 결정한다. 모양이 같은 Object들은 동일한 특성을 가질 수 있도록 보장되며, CheckPrivateField의 IC로는 완벽한 검사입니다.

우리가 엔진을 수정하기 위해 속성 열거 프로토콜에서 private fields를 생략하고, private field를 추가할 경우 sealed object를 확장할 수 있습니다..

Proxies

프록시는 우리에게 새로운 도전을 주었다. 구체적으로는, private field를Stamper  클래스를 이용한다.

let obj3 = {};
let proxy = new Proxy(obj3, handler);
new Stamper(proxy)

Stamper.getX(proxy) // => "stamped"
Stamper.getX(obj3)  // TypeError, private field is stamped
                    // onto the Proxy Not the target!

나는 처음에 분명히 이것이 놀랍다는 것을 알았다. 이 놀라운 사실을 알게 된 이유는 나는 다른 operation들과 마찬가지로 private field의 추가가 proxy를 통해 타깃으로 갈 것으로 예상했기 때문이다. 그러나 내가 WeakMap mental modelinternalize 할 수 있게 되자, 나는 이 예시를 훨씬 더 잘 이해할 수 있었다. 트릭은 WeakMap model에서는 (그것이 바로 Proxy), target 객체가 아닌 #x WeakMap 안에서 키로 사용된다.

 

그러나 SpiderMonkey의 Proxies는 임의의 속성을 위한 공간이 없는 고도로 전문화된 객체이기 때문에 이러한 semantic들은 private fields를 숨겨진 속성으로 모델링하기 위한 우리의 구현 선택에 도전장을 던졌다. 이 경우를 지원하기 위해, 우리는 'expando' 객체를 위한 새로운 예약 슬롯을 추가했다. expando는 프록시에 동적으로 추가된 속성의 홀더 역할을 하는 lazily하게 할당된 객체다. 이 패턴은 이미 DOM 객체에 사용되고 있는데, 일반적으로 추가 특성을 위한 공간이 없는 C++ 객체로 구현된다. 그래서 만약 당신이 document.foo = "hi"라고 쓴다면, 이것은,이것은 document를 위한 expando객체를 할당하고, foo 프로퍼티와 값을 넣을 것이다. privateprivate fields로 돌아와,  #x 가 프록시에서 액세스 되면 프록시 코드는 expanso 객체에서 해당 속성을 찾아봐야 함을 알고 있다.

In Conclusion

Private Fields는 JavaScript 언어 기능을 구현한 사례로, 이미 최적화된 엔진 원시 요소 측면에서 사양을  re-castingre-casting 하는 것보다 사양을 직접 구현하는 것이 덜 수행될 것이다. 그러나 그러한 재작성 자체에는 사양에 없는 일부 문제 해결이 필요할 수 있다.

결국, 나는 우리가 Private Fields 실행을 위한 선택에 상당히 만족하며, 그것이 마침내 세상에 들어오는 것을 보게 되어 흥분된다!

Acknowledgements

다시 한번André Bargull에게 감사해야겠습니다, 첫 패치를 제공하고 내가 따라갈 수 있는 훌륭한 길을 마련해 주셨습니다. 그가 이미 많은 생각을 결정을 내리는 데 쏟았기 때문에 private fields를 끝내는 것을 훨씬 더 쉬웠다.

Jason Orendorff는 내가 private field bytecode의 두 개의 별도 구현체들과 proxy의 두 개의 별도 구현을 포함한 구현을 하는 동안  훌륭하고 인내심 있는 멘토였다.

이 글의 초안을 읽는 것을 도와준 Caroline CullenIain Ireland, 그리고 많은 오자를 고친 Steve Fink덕분이다.