[JS] class로 생성한 Stack과 Queue에 공통 기능을 확장하시오.

준리·2022년 8월 18일
0
post-thumbnail

class로 생성한 Stack과 Queue에 공통 기능을 확장하시오.

// 공통: clear(), toArray(), print(), isEmtpy, peek, length

이전 글 : class로 stack과 queue 생성
https://velog.io/@ho2yahh/class와-Array-객체를-이용하여-Stack과-Queue를-구현하시오

문제정의😛

기존에 만들었던 stack과 queue에는 중복기능들이 들어있었다. 이를 해결하기 위해 superClass를 만들고 공통기능 메서드를 추가해야 한다. 상속과 다형성을 이용하고 메서드들은 overriding하여 만들어보겠다. 또한 계속 활용하게될 arr 멤버변수의 오염을 방지하기 위해 은닉성(private)을 추가하고 accessor property로 활용하고 현재 JS class에 없는 기능인 protected를 구현해보고자 한다.

은닉성 // 캡슐화, 내부(local) 변수 및 메소드 보호 (private / protected / public)
상속성 // 확장성 (extends, mixin, prototype), superclass & subclass
다형성 // polymorphism, interface / super class, 추상화

상속성 활용 : super class와 sub class


//super class
class Collection { 
    constructor(arr = []) {
        this.arr = arr;
    }
    push(value) {
        return this.arr.push(value);
    }

    pop() {
        return this.arr.pop();
    }
}
// sub class
class Stack extends Collection {
}

// sub class
class Queue extends Collection {
      constructor(arr) {
        super(arr);
    }
    enqueue(value) {
        return this.arr.push(value);
    }

    dequeue() {
        return this.arr.shift();
    }
}

super class(부모)와 sub class(자식)
Colection class를 새로 만들었다. stack과 queue class가 가지고 있던 constructor를 가지고 올라갔다. stack과 queue 클래스는 extends Collection을 추가로 적어줌으로서 Collection을 상속받을 수 있다. 왜냐고? 객체지향언어는 인간과 닮아있다. DNA를 공유한다고 생각하면 조금은 이해가 된다. 부모클래스가 자식클래스에서 consctructor 의 기능을 내려주는 것이다. 반대로 자식에서 부모로 올라갈 순 없냐고? 그럴 수 있을지... 현실에서 잘 생각해보라.

sub class에서 뭔가 다른 점이 발견되지 않는가? stack은 아예 constucutor가 없다. super class와 모든 것을 동일하게 하겠다는 다짐이다. stack에 존재했던 push와 pop도 같은 맥락이다.

근데 queue class 에는 constucutor가 여전히 존재하는 것이다.

    constructor(arr) {
        super(arr);
    }

하지만 내용이 약간 다르다. super keyword를 가지고 있다.

super 키워드는 부모 오브젝트의 함수를 호출할 때 사용됩니다.

지금 상황에선 사실 써도 되고 안써도 되는 상황이다. 다른 값을 추가하지 않았기 때문이다. super의 constucutor를 받고 추가적으로 초기화하는 과정에서this.name = name 같은 내용을 추가한다면 queue constucutor처럼 쓰는게 맞다. 지금 코드에선 삭제해보자.

근데 저게 왜 되는건데... prototype chain

prototype chain은 일종의 족보다. 근간을 찾아 떠난다. 혹시 함수의 scope를 따라가는 scope chain을 아는가? 그것을 안다면 그것과 비슷한 개념이라고 생각하면 된다. 제일 하위 class에서 prototype을 따라 거슬러 올라간다. 어디까지? Object 까지 올라간다. prototype chain을 따라가다보면 코드가 이해되기 시작한다.

class Stack extends Collection { } stack 클래스가 비어있어도 정상적으로 작동하는 건 이미 Collection class에 정의된 것들을 사용하고 있기 때문이다. 엄마아빠가 지어놓은 집에 자연스레 의심도 없이 살았던 나를 보면 정확히 이해 할 수 있다.

다형성 활용 // 공통기능 구현

peek 메서드 구현하기 // 마지막(다음에 나올) 원소

console.log(stack.peek, queue.peek); // 마지막(다음에 나올) 원소

peek 메서드는 stack과 queue에서 다 사용해야하기 때문에 Collection class에서 작성해서 상속해줘야할 것이다.

일단 stack class에서 사용할 것을 구현해보자
stack은 제일 마지막에 들어온 인덱스를 내보내야한다.
this.arr 배열에서 배열 길이 - 1을 해준 인덱스를 출력한다. 그럼 마지막게 나오겠지.

    peek() {
        // 마지막(다음에 나올) 원소 // stack은 마지막, queue는 index 0
        return this.arr[this.arr.length - 1];
    }

일단 찍어보기로 했다.

[Function: peek] [Function: peek] ??? 값이 안찍히고 type이 찍히네
왜그런가봤더니 console.log(stack.peek, queue.peek); peek는 함수로 호출한게 아니었다.

Accerssor Property

accerssor Property(접근자) : 속성 접근자는 점 또는 괄호 표기법으로 객체의 속성에 접근할 수 있도록 해줍니다. // 특정 인스턴스와 무관하고 Class에 존재하는 함수(method) // cf. Computed Property

프로퍼티 getter와 setter : 접근자 프로퍼티는 'getter(획득자)'와 ‘setter(설정자)’ 메서드로 표현됩니다. 객체 리터럴 안에서 getter와 setter 메서드는 get과 set으로 나타낼 수 있습니다.

함수를 호출한 것이 아니라 접근자 프로퍼티를 통해 접근했고, 이는 class 내에 getter가 필요한 것을 의미한다. getter 메서드는 obj.propName을 사용해 프로퍼티를 읽으려고 할 때 실행

읽기전용 함수를 구현했다고 생각해보자(진짜 읽기 전용인지는 뒤에서 해보자)

    get peek() {
        // 마지막(다음에 나올) 원소 // stack은 마지막, queue는 index 0
        return this.arr[this.arr.length - 1];
    }

console.log(stack.peek, queue.peek); // 2, 4

우리가 stack instance에서 기대한 값은 2, queue instance에서 기대한 값은 5이다. 하지만 아쉽게도 4가 나왔다. queue에게 뭔가 특별한 장치를 심어줘야 한다.

Overriding

상속받은 자식클래스는 메서드도 상속받을 수 있는데 이를 변형해서 사용할 수 있는 능력이있다.

그 능력은 바로 메서드 오버라이딩

이제 한발 더 나아가 메서드를 오버라이딩 해봅시다. 
특별한 사항이 없으면 class queueclass Collection에 있는 메서드를 ‘그대로’ 상속받습니다.

그런데 queue에서 get peek() 등의 메서드를 자체적으로 정의하면, 상속받은 메서드가 아닌 자체 메서드가 사용됩니다.

class Queue extends Collection {
  get peek() {
    // Queue.peek()을 호출할 때
    // Collection peek()이 아닌, 이 메서드가 사용됩니다.
  }
}
개발을 하다 보면 부모 메서드 전체를 교체하지 않고, 
부모 메서드를 토대로 일부 기능만 변경하고 싶을 때가 생깁니다. 
부모 메서드의 기능을 확장하고 싶을 때도 있죠

peek 메서드를 오버라이딩 했다.
아빠의 차를 그냥 타는건 그대로 상속받는 것이고
아빠의 차를 받아서 폭주뛸려고 내맘대로 도색하고 튜닝하는 건 메서드 오버라이딩이다.

class Collection {
    constructor(arr = []) {
        this.arr = arr;
    }
    push(value) {
        return this.arr.push(value);
    }

    pop() {
        return this.arr.pop();
    }

    get peek() {
        // 마지막(다음에 나올) 원소 // stack은 마지막, queue는 index 0
        return this.arr[this.arr.length - 1];
    }
}

class Stack extends Collection {}

class Queue extends Collection {
    constructor(arr) {
        super(arr);
    }
    enqueue(value) {
        return this.arr.push(value);
    }

    dequeue() {
        return this.arr.shift();
    }

    get peek() {
        return this.arr[0];
    }
}

Queue.peek는 return this.arr[0]; // 5 새로운 return을 내보내기때문에 peek 공통 메서드를 완성했다.

기본적으로 Collection class에 print 메서드를 선언 print() { return console.log(this.arr); }해주고 stack instance에 print 메서드를 오버라이딩했다.

class Stack extends Collection {
    print() {
        for (const k of this.arr) {
            console.log(k);
        }
    }
}
stack.print(); // 출력해보기
queue.print(); // 출력해보기

이외 공통 기능들

    toArray() { // 이터러블한 arr로 만들기
        return [...this.arr];
    }

    clear() { // arr.length를 0으로 만들어서 날려버리기
        return (this.arr.length = 0);
    }

    get isEmpty() { // 비었는지 확인하기
        return !this.arr.length;
    }

    get length() { // 길이 구하기
        return this.arr.length;
    }

const arr = queue.toArray().map((a) => console.log(a)); // map이 돌아가면 이터러블
if (!stack.isEmpty) stack.clear(); // arr가 비어있지 않으면 arr를 클리어해라
console.log("stack :>> ", stack); // []

console.log("queue :>> ", queue); // [4, 5]
if (queue.length) queue.clear(); // length가 존재하면 클리어해라
console.log("queue :>> ", queue); // []

은닉성 활용

class Collection { 
    constructor(arr = []) {
        this.arr = arr;
    }

코드를 짜다보니 arr 가 오염될 가능성이 보였다. 입력받은 arr은 private 멤버변수로 만들어야겠다고 생각했다.
현재는 arr이 public으로 되어있지만, js는 #private를 지원하고 있다.

class 의 속성(property)들은 기본적으로 public 하며 class 외부에서 읽히고 수정될 수 있다. 하지만, ES2019 에서는 해쉬 # prefix 를 추가해 private class 필드를 선언할 수 있게 되었다.

class Collection {
    #arr;
    constructor(arr = []) {
        this.#arr = arr;
    }
    push(value) {
        return this.#arr.push(value);
    }

#을 사용하여 private 멤버변수를 선언하고 초기화했다. 순조롭게 잘되나 싶었는데, 서브 클래스에서는 사용할 수 없다는 것을 알았다. protected 클래스 필드가 필요한데... js에는 없다. (!?)

자바스크립트가 문법적으로 protected 키워드를 지원하는 것은 아니지만 프로그래머들끼리 명시적인 암묵적 약속으로 정의할 수 있습니다. 다음과 같이 protected로 만들고 싶은 멤버앞에 언더 바( _ )를 붙여서 이용합니다.

방법 1) 명시적 암묵적 약속으로 protected를 적용한다.

class Collection {
    _arr;
    constructor(arr = []) {
        this._arr = arr;
    }
    push(value) {
        return this._arr.push(value);
    }

    pop() {
        return this._arr.pop();
    }
  ...

방법 2) #private 멤버를 사용하고 서브에 super를 사용한다. (실패)

class Stack extends Collection {
    print() {
        for (const k of super.arr) {
            console.log(k);
        }
    }
}

super의 메서드는 불러 올 수 있어도 #arr 자체에 접근할 수 없기 때문에 super로 모든 것을 처리할 수는 없다. 특히 오버라이딩 된 메서드들은 더 그렇다. getter 함수를 사용하거나 콜백함수를 사용해야할 것이다.

방법 3) #private 멤버를 사용하고 getter 함수를 사용한다.

class Collection {
    #arr;
    constructor(arr = []) {
        this.#arr = arr;
    }

    get __arr() {
        return this.#arr;
    }
...{중략}
}

class Stack extends Collection {
    print() {
        for (const k of this.__arr) {
            console.log(k);
        }
    }
}

class Queue extends Collection {
    enqueue(value) {
        return this.__arr.push(value);
    }

    dequeue() {
        return this.__arr.shift();
    }

    get peek() {
        return this.__arr[0];
    }
}

getter 함수를 활용해 부모가 가진 #arr를 읽기전용으로 만들어서 자식 클래스에서 this__arr형태로 접근한다. 이는 읽기 전용이기 때문에 외부의 접근도 막을 수 있다. 하지만 외부에서도 볼수 있기 때문에 은닉성이 절반의 기능만 유지 된다.

출제자의 의도가 내포된 코드

class Collection {
    #arr;
    constructor(...args) {
        // 뭐가 들어올지 모르니까 ...rest로
        // this.#arr = args[0];
        this.#arr = Array.isArray(args[0]) ? args[0] : [...args];
    }

    push(val) {
        this.#arr.push(val);
    }

    shift() {
        return this.#arr.shift();
    }

    print(cb) {
        if (cb) {
            cb([...this.#arr].reverse()); 
                // stack queue 전부  다 reverse가 되어야해서 같이 찍어줌
            return;
        }
        console.log("stack>>", this.#arr);
    }

    clear() {
        this.#arr.length = 0;
    }

    get peek() {
        if (this.constructor.name === "Stack") {
            return this.#arr[this.length - 1];
        }
        return this.#arr[0];
    }
}

class Stack extends Collection() {
    print() {
        super.print((arr) =>
            console.log("Stack>>\n", [...arr].reverse().join("\n "))
        ); // reverse는 순수함수기 때문에 arr를 스프레드 해서 가져옴
    }
}
class Queue extends Collection() {
    enqueue() {
        super.push(val);
    }

    dequeue() {
        return super.shift();
    }

    print() {
        super.print((arr) => console.log(arr.join(" -> "), "->"));
    }
}

const st2 = new Stack(1,2) // 배열이 아닌값을 받을 때.

자체 코드리뷰

Q. 부모가 메서드를 최대한 많이 가지고 있는 것이 좋은가?
A. 확장성을 위해 그렇다. 공통 메서드는 부모로 올리고 각자 다른 것만 남기는 것이 좋다.

    constructor(...args) {
        // 뭐가 들어올지 모르니까 ...rest로
        // this.#arr = args[0];
        this.#arr = Array.isArray(args[0]) ? args[0] : [...args];
    }

construcor의 매개변수는 뭐가 들어올지 모르니 일단 ...rest 프로퍼티로 챙겨준다.
const st2 = new Stack(1,2) 이런게 들어올것을 대비해
this.#arr = Array.isArray(args[0]) ? args[0] : [...args];
args의 0번째 인덱스가 Array 이면 그 배열을 가져오고, 아니면 스프레드 연산자로 묶어준다.
그렇게 인스턴스의 초기화를 끝낸다.


// super class
    print(cb) {
        if (cb) {
            cb([...this.#arr]); 
            return;
        }
        console.log("stack>>", this.#arr);
    }
      
// stack class (sub)
      print() {
        super.print((arr) =>
            console.log("Stack>>\n", [...arr].reverse().join("\n "))
        ); // reverse는 순수함수기 때문에 arr를 스프레드 해서 가져옴
    }
      
// queue class (sub)
      print() {
        super.print((arr) => console.log(arr.join(" -> "), "->"));
    }
      

print는 오버라이딩했는데 private 멤버 변수때문에 서브클래스에서 접근할 수 없어 콜백함수를 사용했다. stack과 queue의 특성을 생각해 오버라이딩했다.

    get peek() {
        if (this.constructor.name === "Stack") {
            return this.#arr[this.length - 1];
        }
        return this.#arr[0];
    }

peek 메서드 같은경우 각각 오버라이딩하지 않고 조건문을 걸어 constuctor.name 프로퍼티로 구분했다.


private 멤버변수를 활용해 은닉성을 유지했고, 클래스의 특성을 잘 이용한 class 예제다.
은닉성, 상속성, 다형성을 고루 잘 사용했다.

결론

막연했던 클래스를 이해하는데 큰 도움이 된 try.this였다. 클래스를 발전시키면서 새로운 클래스의 특성들을 적용시키고 활용해보았다. 특히 다형성이 중요하다는 것을 새삼 깨닫게 되었다. 중복을 최소화하고 아름다운 완성품(인스턴스)를 생산하고 싶을 때 클래스를 적용해봐야겠다.

출처

SSAC 영등포 교육기관에서 풀스택 실무 프로젝트 과정을 수강하고 있다. JS전반을 깊이 있게 배우고 실무에 사용되는 프로젝트를 다룬다. 앞으로 그 과정 중의 내용을 블로그에 다루고자 한다.

profile
트렌디 풀스택 개발자

0개의 댓글