본문 바로가기

Backend/Javascript

Javascript 내장 클래스 확장

- 문제 상황

 

배치 프로그램 작성 중 Javascript 의 내장 클래스인 Map 을 사용하면 딱 적절할 부분이 있었는데, 해당 클래스에 몇 가지의 메서드를 추가하거나 기존 메서드를 수정하고 싶은 케이스가 있었습니다. 예를 들면 생성한 Map 에 set 을 하는데 조건을 만족하는 key 에 대해서만 얼마나 set 이 호출되었는지 카운팅을 하기 위해서 말이죠.

 

const map = new Map();

map.set(...);
map.set(...);
...

console.info(map.getSpecificCount()); // ?

 

원하는 바를 약간 수도코드처럼 작성하면 위와 같을 것 입니다. 하지만 내장 클래스인 Map 에 set 메서드는 있지만 getSpecificCount 와 같은 메서드는 없고, 기존 set 메서드도 원래의 동작일 뿐 별도의 카운팅을 하는 기능은 없습니다.

원하는 기능을 가진 아예 새로운 클래스를 만들 수도 있겠지만 내장 클래스가 원래 갖고 있던 메서드도 일일이 구현하기엔 꽤나 귀찮습니다. 이 때 기존 내장 클래스를 확장해서 새로운 클래스를 만들 수 있습니다.

 

- Map 클래스 확장

 

간단한 예시를 보겠습니다.

 

class A extends Map {
  isEmpty() {
    return this.size === 0;
  }
}

const a = new A();

console.info(a.isEmpty()); // true

a.set('1', 'A');
console.info(a.get('1')); // 'A'
console.info(a.isEmpty()); // false

 

Syntax 는 간단합니다. 일반적으로 특정 클래스를 상속 받기 위해 사용하는 extends 키워드를 이용해 내장 클래스 Map 을 상속받도록 작성하면 됩니다. 기존 Map 클래스는 isEmpty 라는 메서드는 갖고 있지 않습니다. 따라서 Map 에 아이템이 얼마나 있는지 혹은 없는지를 체크하려면 size 라는 property 에 접근해야만 했는데, 이 size 를 0 과 비교한 결과값 boolean 을 반환하는 isEmpty 라는 메서드를 추가했습니다.

 

위 코드에서 사용한 것 처럼 기존 Map 클래스의 메서드인 get, set 사용에도 아무런 문제가 없고 isEmpty 메서드도 정상 동작 합니다.

물론 기존의 상속이 그렇듯이 원래 있던 메서드도 덮어쓸 수 있습니다.

 

class A extends Map {
  constructor() {
    super();
    this.specificCount = 0;
  }

  set(key, value) {
    super.set(key, value);
    if (key.includes('test')) {
      this.specificCount += 1;
    }
  }

  getSpecificCount() {
    return this.specificCount;
  }
}

const a = new A();

a.set('1', 'A');
a.set('test-1', 'B');
a.set('test-2', 'C');

console.info(a.getSpecificCount()); // 2

 

제일 처음에 잠깐 얘기했던 문제 상황에 대한 심플한 코드입니다. set 메서드를 덮어씌웠는데, super.set 을 호출함으로써 기존 set 메서드와 동작은 동일하게 가져가고 set 할 때 받은 key 의 값에 'test' 문자열이 포함되어있으면 specificCount 를 증가시키도록 작성했습니다.

 

이런식으로 아예 새로운 클래스를 작성할 필요 없이 Javascript 에서 기본적으로 제공하는 내장 클래스를 활용해 좀 더 다채로운 코드를 작성할 수 있습니다.

 

- Symbol.species

 

조만간 Javascript 의 Symbol 에 대해서도 따로 포스팅하겠지만 잠깐 살펴보자면 Symbol.species 는 특수 정적 getter 입니다.

그리고 이 getter 는 클래스에 추가할 수 있는데, Symbol.species 를 정의함으로써 map, filter 등의 메서드를 통해 만들어지는 개체의 생성자를 지정할 수 있습니다.

 

class A extends Array {
  isEmpty() {
    return this.length === 0;
  }
}

const a = new A(3, 4, 5);

console.info(a.isEmpty()); // false

const c = a.filter(item => item > 4);

console.info(c); // [5]
console.info(c.isEmpty()); // false

 

쉬운 이해를 위해 내장 클래스 Array 를 확장시킨 A 클래스를 만들고 isEmpty 메서드를 추가했습니다.

a 는 당연히 클래스 A 를 통해 만들어진 객체니 isEmpty 메서드 호출이 가능한데 a.filter 메서드를 통해 만들어진 c 도 isEmpty 메서드 호출이 가능합니다. 이는 내장 메서드 filter 가 반환하는 인스턴스는 Array 가 아닌 A 라는 의미입니다.

 

console.info(a.constructor); // [class A]
console.info(c.constructor); // [class A]

const b = new Array();
console.info(b.constructor); // [Function: Array]

 

간단히 constructor 를 찍어보면 기존의 Array 와 달라졌음을 알 수 있습니다. 내장 클래스 Array 를 확장한 클래스에서 사용하는 내장 메서드 filter, map 등은 Array 가 아닌 a.constructor 를 기반으로 새로운 배열이 만들어지고 결과가 담겨 반환되는 것 입니다.

 

class A extends Array {
  isEmpty() {
    return this.length === 0;
  }

  static get [Symbol.species]() {
    return Array;
  }
}

const a = new A(3, 4, 5);

console.info(a.isEmpty()); // false

const c = a.filter(item => item > 4);

console.info(c); // [5]
console.info(c.isEmpty()); // TypeError: c.isEmpty is not a function

 

위에서 말한 것 처럼 Symbol.species 를 A 클래스에 추가해 map, filter 등의 내장 메서드가 반환하는 개체의 생성자를 Array 로 지정했습니다. 이제 a.filter 가 반환하는 개체의 constructor 는 A 가 아니라 Array 이며 Array 에는 isEmpty 메서드가 없으므로 c.isEmpty() 호출 시 TypeError 가 발생하게 됩니다.

 

새로운 배열을 반환하는 map, filter 와 같은 내장 메서드를 가진 내장 클래스 Array 를 예시로 들었지만 Symbol.species 는 다른 컬렉션에서도 사용되는 getter 이며 Map, Set 등의 컬렉션에서도 동일하게 동작합니다.

'Backend > Javascript' 카테고리의 다른 글

TC39 Process  (0) 2022.02.07
ES2022  (0) 2022.01.30
Javascript - Closure (클로저) 에 대해  (1) 2021.07.17
Javascript - 실행 컨텍스트란  (0) 2021.07.10
JavaScript - this 에 대해  (0) 2021.07.03