LHJ

I'm a FE developer.

21.3 객체 보호 - 동결, 봉인, 확장 금지

05 Jun 2020 » js_lj

21.3 객체 보호: 동결, 봉인, 확장 금지

자바스크립트의 유연성은 매우 강력하기도 하지만, 그만큼 문제가 생길 소지도 많습니다.
어떤 코드든, 그 위치가 어디이든, 다른 객체를 쉽게 바꿀 수 있으므로 의도와 달리 위험한 코드를 만들 가능성이 항상 존재합니다.

자바스크립트에는 객체를 보호해서 의도하지 않은 수정을 막고, 의도적인 공격은 더 어렵게 만드는 세 가지 메커니즘이 있습니다.
동결(freezing), 봉인(sealing), 확장 금지(preventing extension)입니다.

동결된 객체는 수정할 방법이 없습니다.
일단 객체를 동결하면 다음과 같은 작업이 불가능해집니다.

  • 프로퍼티 값 수정 또는 할당
  • 프로퍼티 값을 수정하는 메서드 호출
  • setter 호출
  • 새 프로퍼티 추가
  • 새 메서드 추가
  • 기존 프로퍼티나 메서드의 설정 변경

동결

간단히 말해, 객체를 동결하면 그 객체는 문자열이나 숫자처럼 불변이 됩니다.
객체를 동결하면 상태를 바꾸는 메서드가 모두 쓸모없어지므로 데이터만 들어있는 객체에서 가장 유용합니다.

객체를 동결할 때는 Object.freeze를 사용하고, 객체가 동결됐는지 확인할 때는 Object.isFrozen을 사용합니다.
회사, 버전, 빌드ID, 저작권 정보 등 앞으로 바뀔 일이 없는 프로그램에 대한 정보를 객체에 보관한다고 생각해 봅시다.

const appInfo = {
    company: "White knight Software, Inc.",
    version: '1.3.5',
    buildId: '0a995448-ead4-4a8b-b050-9c9083279ea2',
    // 이 함수는 getter이므로 동결한 상태에서도 계속 동작합니다.
    copyright() {
        return `㈜ ${new Date().getFullYear()}, ${this.company}`;
    },
};
Object.freeze(appInfo);
Object.isFrozen(appInfo);   // true

appInfo.newProp = 'test';
// TypeError: Can't add property newProp, object is not extensible

delete appInfo.company;
// TypeError: Cannot delete property 'company' of [object Object]

appInfo.company = 'test';
// TypeError: Cannot assign to read-only property 'company' of [object Object]

Object.defineProperty(appInfo, 'company', { enumerable: false });
// TypeError: Cannot redefine property: company

봉인

객체를 봉인하면 새 프로퍼티를 추가하거나 기존 프로퍼티를 변경, 삭제할 수 없습니다.
클래스의 인스턴스를 사용하면서, 인스턴스의 프로퍼티를 수정하는 메서드는 동작하도록 할 때 봉인을 사용할 수 있습니다.
객체를 봉인할 때는 Object.seal를, 객체가 봉인됐는지 확인할 때는 Object.isSealed를 사용합니다.

class Logger {
    constructor(name) {
        this.name = name;
        this.log = [];
    }
    add(entry) {
        this.log.push({
            log: entry,
            timestamp: Date.now(),
        })
    }
}

const log = new Logger("Captain's Log");
Object.seal(log);
Object.isSealed(log);   // true

log.name = "Captain's Boring Log";          // OK
log.add("Another boring day at sea...");    // OK

log.newProp = 'test';
// TypeError: Can't add property newProp, object is not extensible

log.name = 'test';      // OK

delete log.name;
// TypeError: Cannot delete property 'name' of [object Object]

Object.defineProperty(log, 'log', { enumerable: false });
// TypeError: Cannot redefine property: log

마지막으로, 가장 약한 제약인 확장 금지입니다.
확장 금지를 사용하면 객체에 새 프로퍼티를 추가하는 것만 금지됩니다.
프로퍼티에 값을 할당하거나, 삭제하거나, 속성을 변경하는 작업은 모두 허용됩니다.
확장을 금지할 때는 Object.preventExtensions, 확장이 금지됐는지 확인할 때는 Object.isExtensible을 사용합니다.
이번에도 Logger 클래스를 다시 사용하겠습니다.

class Logger {
    constructor(name) {
        this.name = name;
        this.log = [];
    }
    add(entry) {
        this.log.push({
            log: entry,
            timestamp: Date.now(),
        })
    }
}

const log2 = new Logger("First Mate's Log");
Object.preventExtensions(log2);
Object.isExtensible(log2);  // false

log2.name = "First Mate's Boring Log";      // OK
log2.add("Another boring day at sea...");   // OK

log2.newProp = 'test';
// TypeError: Can't add property newProp, object is not extensible

log2.name = 'test';             // OK
delete log2.name;               // OK
Object.defineProperty(log2, 'log', {enumerable: false});    // OK

필자는 Object.preventExtensions를 자주 사용하지는 않습니다.
객체의 확장을 막아야 할 때는 보통 프로퍼티 삭제나 속성 변경도 금지해야 할 때가 대부분이므로 객체 봉인을 더 많이 쓰는 편입니다.
[표 21-1]에 객체 보호 옵션을 요약했습니다.

표 21-1 객체 보호 옵션

동작일반 객체동결 객체봉인 객체확장 금지 객체
프로퍼티 추가OXXX
프로퍼티 읽기OOOO
프로퍼티 값 설정OXOO
프로퍼티 속성 변경OXXO
프로퍼티 삭제OXXO