컴퓨터 공부/🕸️ Web

Javascript - OOP, prototype, ES6 Classes, Inheritance, super()

letzgorats 2023. 12. 15. 18:10

< OOP(Object-oriented programming) 란? >

객체 지향 프로그래밍(OOP)는 Java 및 C++ 를 비롯한 많은 프로그래밍 언어의 기본이 되는 프로그래밍 패러다임이다. 객체 지향 프로그래밍은 여러개의 독립된 단위 '객체'들의 모임으로 컴퓨터 프로그램을 파악한다.

=> 객체지향 프로그래밍은 객체들의 모임이다.

 

객체 지향 프로그래밍이 나오기 이전에는 명령어의 목록을 나열(절차 지향) 하는 기능 구현을 목적으로 작성했지만, 이렇게 코드를 길게 작성하다 보면, 매우 알아보기 힘든 복잡한 코드가 만들어진다.

 

그래서, 하나의 문제 해결을 위한 독립된 단위인 "객체"로 만들었으며, 이 객체로 인해 알아보기 쉽고 재사용성이 높아졌다.

[ OOP의 특징 ]

자료 추상화, 상속, 캡슐화, 다형성

1) 자료 추상화(Abstraction)

  • 불필요한 정보는 숨기고 중요한 정보만을 표현함으로써 프로그램을 간단히 만드는 것이다.
  • 이렇게 해서 그 객체 안에 자세한 내용을 몰라도 중요 정보를 이용해서 해당 객체를 사용할 수 있게 된다. (비유하자면, 커피를 마시기 위해서 커피머신을 이용할 줄 알면 커피 머신이 어떻게 작동하는지 몰라도 커피를 마실 수 있는 것과 비슷하다.)

추상화

2) 상속(Inheritance)

  • 새로운 클래스가 기존의 클래스의 자료와 연산을 이용할 수 있게 하는 기능이다.
  • 상속을 받는 새로운 클래스를 (부클래스, 파생 클래스, 하위 클래스, 자식 클래스) 라고 하며 새로운 클래스가 상속하는 기존의 클래스를 (기반 클래스, 상위 클래스, 부모 클래스) 라고 한다.
  • 상속을 통해서 기존의 클래스를 상속받은 하위 클래스를 이용해 프로그램의 요구에 맞춰 클래스를 수정할 수 있고 클래스 간의 종속관계를 형성함으로써 객체를 조직화 할 수 있다.

상속

3) 다형성(Polymorphism)

  • poly(많은) + morph(형태) => 다양한 형태를 가질 수 있다는 뜻이다.
  • 어떤 한 요소에 여러 개념을 넣어 놓는 것이다.
  • 예를 들어서, 도형이라는 부모 클래스에 삼각형, 사각형, 원 클래스가 있다고 가정해보자.
  • 도형이라는 클래스에 getArea(){ return this.width * this.height } 이 있다면 삼각형 클래스에서는 getArea 메소드에서는 다른 방식으로 Area 를 구할 것이며, 사각형 클래스에서도 getArea 메소드에서 다른 방식으로 Area를 구할 것이다.
  • 이렇게 같은 메소드라도 각 인스턴스에 따라 다양한 형태를 가질 수 있는 것을 다형성 이라고 한다.

다형성

4) 캡슐화(encapsulation)

  • 클래스 안에 관련 메소드, 변수 등을 하나로 묶어준다.
  • 이 매커니즘을 이용해서 바깥에서의 접근을 막아 보안이 강화되고 잘 관리되는 코드를 제공한다.

캡슐화


< 다형성 >

class Paypal extends PaymentGateway {
    pay(amount){
        // 페이팔 전용 로직 구현
    }
    refund(amount){
        // 페이팔 전용 로직 구현
    }
    connect(){
        // 페이팔 전용 로직을 구현
    }
}
class Visa extends PaymentGateway {
    pay(amount){
        // Visa 전용 로직 구현
    }
    refund(amount){
        // Visa 전용 로직 구현
    }
    connect(){
        // Visa 전용 로직을 구현
    }
}

 

위 코드에서처럼 Paypal 클래스와 Visa 클래스에서 쓰이는 메서드(pay, refund, connect) 는 같지만 그 안의 구현은 클래스별로 다르다.

class Customer {
    makePayment(gateway, amount){
        return gateway.pay(amount)
    }
    
    // 만약 다형성이 없다면 이런식으로 메소드들을 계속 생성해야 한다.
    // payByPaypal(amount){}
    // payByVisa(amount){}
    // payByKaKaoPay(amount){}
    // payByTossPay(amount){}
 	// ...
    
    getRefund(gateway, amount){
        return gateway.refund(amount)
    }
}

const Allu = new Customer();
const paypal = new Paypal();
const visaCard = new Visa();

Allu.makePayment(paypal,100);
Allu.makePayment(visaCard,100);

 

※ 다형성이 유용한 이유는?

- 조금 더 제너릭한, 더 일반적인 코드를 재사용하고 작성할 수 있다.

- 만약 위 코드 처럼 새로운 결제 게이트웨이 제공업체(paypal 등등...) 가 있는 경우, Client 클래스의 결제 로직을 조정하지 않고, 새 클래스를 생성하기만 하면 된다


< Javascript ProtoType >

아래와 같은 코드가 있다고 해보자.

let user = {
    name : "Allu",
    age : 10
}

console.log(user.name);    // Allu
console.log(user.hasOwnProperty("email"));    // false

 

hasOwnProperty라는 메소드는 따로 만들어주지 않았는데, 해당 코드를 실행해보면, false가 나온다.

hasOwnProperty 메소드가 먹힌다.

[  hasOwnProperty 는 어디서 왔을까? ]

현재 user 객체 변수에는 두 개의 속성(name, age) 만 있는데, hasOwnProperty라는 메소드는 어디서 나온 것일까?

hasOwnProperty

위에서 user 객체가 가지고 있는 프로토타입을 보면, hasOwnProperty 가 있는 것을 확인할 수 있다.

모든 객체는 global Object prototype을 가지는데, 이 프로토타입에 들어있는 것들이 위 사진에서 보이는 메소드들이다.

[  그러면 이 ProtoType은 무엇일까? ]

프로토타입은 자바스크립트 객체다른 객체로부터 메소드와 속성을 상속받는 매커니즘을 말한다. 이것을 프로토타입 체인(prototype chain) 이라고도 말한다.

 

위에서 보듯이 prototype object 안에 있는 hasOwnProperty를 상속받아서 사용하고 있다. 이렇게 하므로 인해서 더 적은 메모리를 사용할 수 있고 코드를 재사용 할 수 있다.

 

What is the "prototype chain"?

 

다른 예시로 살펴보자.

const testArray = [1,2,3];
testArray.push(4);
console.log(testArray); // [1,2,3,4]

 

해당 코드에서 push라는 메소드를 사용할 수 있는 이유는 무엇일까?

push : f push()

 

이 Prototype에 보면 여러가지 메서드들 중에서 push가 들어있는 것을 확인할 수 있다.

생성자 함수를 이용해서 객체를 생성할 때도 그 객체에서 생성자 함수의 프로토타입에 있는 메서드나 프로퍼티를 사용할 수 있었던 것처럼 testArray라는 배열을 생성할 때 Array() 생성자의 프로토타입에서 상속해주는 메서드나 프로퍼티를 사용할 수 있게 된다.

 

const testArray = [1,2,3]; (리터럴 표기법)

실제 내부에서는

const testArray = new Array(1,2,3); 

 

Array() 생성자는 새로운 Array 객체를 생성할 때 사용하는데, 아래 그림처럼 testArray를 만든다고 이해하면 된다.

array.prototype 객체 -> testArray

 

배열 testArray와 Array, prototype의 관계를 정리하면 아래 그림과 같다.

 

Array.prototypetestArray.__proto__ 객체를 비교해보면,

  • prototype
    • 부모prototype 객체를 실제로 가지고 있다.
  • __proto__ 
    • 자식이 __proto__ 를 이용해서 부모의 prototype 객체를 출력한다.
    • 자식이 실제 부모의 prototype 객체를 가지는 것이 아닌 prototype 객체를 향하는 링크만 가지고 있다.

그래서, prototype chain의 마지막은 null 이다.

__proto__ : null

 

[  예를 통해 ProtoType 알아보기 ]

// 생성자(constructor) 함수 - 다른 객체를 만드는 함수
function Pet(name, gender, birthday){
    this.name = name;
    this.gender = gender;
    this.birthday = new Date(birthday);
    this.calculateAge = function(){
        const diff = Date.now() = this.birthday.getTime();
        const ageDate = new Date(diff);
       	return Math.abs(ageDate.getUTCFullYear() - 1970);
    }
}
// return 을 안해도 자동으로 객체와 인스턴스를 리턴한다.

// 생성자 함수를 사용해서 객체를 만들 때는 new 키워드를 사용
const Allu = new Pet('Allu','male','06-02-96');
const Kim = new Pet('Kim','male','09-05-96');

console.log(Allu);
console.log(Kim);

 

위 코드를 실행해보면, 아래와 같은 결과가 나온다.

Allu객체, Kim 객체

여기서 Allu 든 Kim 이든 name, birthday, gender 값은 고유의 특성이 있겠지만, calculateAge 프로퍼티의 값은 같다.

아까 프로토타입은 더 적은 메모리를 사용할 수 있고 코드를 재사용 할 수 있다고 했다.

그러므로, 공통 부분인 이것을 prototype으로 옮겨줘보자.

// Pet Constructor
function Pet(name, gender, birthday){
    this.name = name;
    this.gender = gender;
    this.birthday = new Date(birthday);
}

Pet.prototype.calculateAge = function(){

    const diff = Date.now() = this.birthday.getTime();
    const ageDate = new Date(diff);
    return Math.abs(ageDate.getUTCFullYear() - 1970);

}

// 생성자 함수를 사용해서 객체를 만들 때는 new 키워드를 사용
const Allu = new Pet('Allu','male','06-02-96');
const Kim = new Pet('Kim','male','09-05-96');

console.log(Allu);
console.log(Kim);

 

prototype에 calculateAge 라는 이름으로 함수를 속성값으로 준 코드이다.

prototype에 calculateAge가 들어왔다.

 

이렇듯, 재사용할 수 있는 부분은 prototype 에 넣어서 더 효율적으로 사용해줄 수가 있다.

prototype에 넣어주는 방법은 위 방법 말고도 또 다른 방법이 있다.

 

바로, Object.create() 를 이용해서도 객체의 prototype을 지정해줄 수 있다.

[  Object.create() ]

Object.create() 메서드는 지정된 프로토타입 객체 및 속성(prototype)을 갖는 새 객체를 만든다.

// Pet Constructor
function Pet(name, gender, birthday){
    let pet = Object.create(petPrototype);
    pet.name = name;
    pet.gender = gender;
    pet.birthday = new Date(birthday);
    return pet;
}

const petPrototype = {
    calculateAge() {
        const diff = Date.now() = this.birthday.getTime();
        const ageDate = new Date(diff);
        return Math.abs(ageDate.getUTCFullYear() - 1970);
    }
}

// 생성자 함수를 사용해서 객체를 만들 때는 new 키워드를 사용
const Allu = new Pet('Allu','male','06-02-96');
const Kim = new Pet('Kim','male','09-05-96');

console.log(Allu);
console.log(Kim);

calculateAge()가 prototype에 잘 들어갔다.

 

이렇게 프로토타입을 먼저 만들어주고 Obejct.create() 를 통해서 그 지정된 prototpye을 갖는 새 객체를 생성할 수 있다.


< ES6 Classes >

ES6에서 나온 Class를 이용해서 더 쉽게 OOP를 구현할 수 있다.

이것은 문법을 OOP 방식을 이용하지만, 내부에서 prototype을 사용하여 작동된다.

class Pet{

    // constructor는 인스턴스의 생성과 동시에 클래스 필드의 생성과 초기화를 실행한다.
    // constructor은 생략할 수 있다.
    constructor(name,gender,birthday){
        this.name = name;
        this.gender = gender;
        this.birthday = new Date(birthday);
    } // this는 클래스가 생성할 인스턴스를 가리킨다.
    
    introduce() {
        return `Hello my name is $(this.name}`;
    }
}

// constructor()은 new() 에 의해 자동으로 호출된다.
const Allu = new Pet('Allu', 'male', '06-02-98');

 

클래스를 만들 때, constructor()은 생략을 해도 된다. new()를 통해 객체를 생성할 때 자동으로 constructor() 가 호출되기 때문이다.

 

객체 Allu 를 콘솔 로그 찍어보면

console.log(Allu)

해당 객체가 잘 나온것을 확인할 수 있다.

근데 이 때, introduce 메소드는 prototype 에 자동으로 들어간 것도 확인할 수 있다. 어떻게 해서, 자동으로 들어간 것일까?

 

아까처럼 특정 메소드를 prototype에 넣으려면, Pet.prototype.introduce 함수와 같이 만들어주거나 특정 프로토타입을 만들어주고Object.create()를 활용해야 했다.

 

하지만, 여기 예시처럼 ES6 클래스 문법을 이용했을 때, 이 클래스 안에서 메소드를 만들어주면 함수 자체가 자동으로 이 프로토타입에 들어가는 것을 확인했다. 결국, 이는 ES6 문법을 이용한 것인데, 클래스 내부에 메소드를 만들어주면 그 메소드는 자동으로 프로토타입에 들어가게 된다.(function calculateAge 처럼의 함수가 아니라 단순 메소드는 자동으로 프로토타입에 들어감.)

 

반면, 프로토타입이 아닌 클래스 함수 자체에 메서드를 설정할 수도 있는데, 그 방법은 static을 이용하는 것이 있다.

[  Static 사용 ]

"prototype"이 아닌 클래스 함수 자체에 메서드를 설정할 수도 있는데, 이런 메서드를 정적(static) 메서드라고 부른다.

 

아래 코드를 살펴보자.

class Pet{

    constructor(name,gender,birthday){
        this.name = name;
        this.gender = gender;
        this.birthday = new Date(birthday);
    } 
    
    introduce() {
        return `Hello my name is ${this.name}`;
    }
    
    static feedNumbers(yesterday,kg) {
        if (kg < 10){
            if (yesterday < 2){
            return yesterday * 2}
            else{
                return yesterday - 0.5
            }
        }
        else if(10 <= kg < 20){
            if (yesterday < 2){
                return yesterday * 2.5}
            else{
                return yesterday - 1
            }
        }
        else{
            if (yesterday < 2){
                return yesterday * 3
            }
            else{
                return yesterday - 1.5
            }
        }
    }
}

const Allu = new Pet('Allu','male','06-02-96');
console.log(Allu);

console.log(Allu.introduce()); // 'Hello my name is Allu'

console.log(Pet.feedNumbers(2.1,8)); // 1.6

 

객체 Allu를 찍어보면, static 으로 선언한 feedNumbers가 현재 프로퍼티에도 없고 프로토타입에도 없는 것을 확인할 수 있다.

Allu 객체

그럼 어디있을까?

위 결과사진을 보면 constructor: class Pet 이라는 부분이 보일 것이다. 저 안을 살펴보면 아래와 같다.

feedNumbers

함수 feedNumbers가 들어가 있는 것을 볼 수 있다. static 으로 설정된 함수는 constructor에 들어간다고 이해하면 된다.

그럼 이 static 으로 만드는 건 주로 언제 사용할까?

바로 this.name, this.gender, this.birthday 와 같은 것을 안 쓰는 독립적인 것을 정의할 때 static을 사용한다.

 

static 메서드를 사용할 때는 인스턴스가 아닌 클래스 이름을 이용해서 사용한다.

 

즉, introduce와 같은 메서드는 'Allu'와 같이 먼저 인스턴스 객체를 만든다음에, 인스턴스 객체를 통해서 메소드를 활성화시키는데,

(Allu.introduce();)

static 메서드를 사용할 때는 인스턴스가 아닌 부모 객체인 그냥 Pet 클래스를 사용해서 메소드를 호출한다

(Pet.feedNumber(2.1,8));


< Sub Class ( Inheritance) >

부모 클래스를 자식 클래스에 확장할 수 있다. 부모 클래스에 있던 기능을 토대로 자식 클래스를 만들 수 있는 것이다. 

상속을 실현시키기 위해서는 extends 키워드를 사용해주면 된다.

class Pet{
    constructor(name, age){
        this.name = name;
        this.age = age;
    }
    
    introduce() {
        return `Hello my name is ${this.name}`;
    }
}

// 부모 클래스에게 상속받아 자식 클래스를 만들고, 자식클래스에 부모 클래스를 불러올 때 super()를 사용한ㄷ.

class Dachshund extends Pet{
    constructor(name, age, color, address){
        
        super(name,age)
        
        this.color = color;
        this.address = address;
    }
}

const Allu = new Dachshund('Allu', 10, 'black', 'Seoul');

console.log(Allu.introduce());

Dachshund extends Pet

 

자식 클래스를 통해 인스턴스를 만들고, 'Allu. introduce' 를 하면 'Hello my name is Allu' 가 나온다.

자식 클래스에는 introduce() 메서드가 없는데도 사용할 수 있는 이유는 부모클래스에 introduce가 있기 때문에, 그대로 상속받아서 사용을 하기 때문이다.

 

- Allu.introduce 가 실행되는 순서 -

 

1. Dachshund 객체에 Allu.introduce 가 있는지 확인한다.

2. 없기 때문에, Dachshund.prototype 에 있는지도 확인하지만, introduce가 없다.

3. extends를 통해 관계가 만들어진 Dachshund.prototype의 프로토타입인 Pet.prototype에 메서드가 있는지 확인한다. 여기에 introduce가 있기 때문에, 이것을 사용한다.


< Super () 란? >

[  Constructor ]

Constructor(생성자)를 사용하면 인스턴스화된 객체에서 다른 메서드를 호출하기 전에 수행해야 하는 사용자 지정 초기화를 제공할 수 있다.

 

클래스를 new 를 붙여서 (new User("Allu")) 인스턴스 객체로 생성하면 넘겨받은 인수와 함께 constructor가 먼저 실행된다.

이 때 넘겨받은 인수인 Allu 가 this.name 에 할당된다.

[  자바스크립트에서의 super ]

- super 키워드는 자식 클래스 내에서 부모 클래스의 생성자를 호출할 때 사용된다.

- super 키워드는 자식 클래스 내에서 부모 클래스의 메소드를 호출할 때 사용된다.

class Car {
    constructor(brand){
        this.carname = brand;
    }
    present(){
        return 'I have a ' + this.carname;
    }
}

class Model extends Car {
    
    constructor(brand, mod){
        super(brand);   //  super 키워드는 자식 클래스 내에서 부모 클래스의 생성자를 호출할 때 사용된다.
        this.model = mod;
    }
    
    // super 키워드는 자식 클래스 내에서 부모 클래스의 메소드를 호출할 때 사용된다.
    show() {
        return super.present() + ', it is a ' + this.model;
    }
}

let myCar = new Model("Ford","Mustang");

console.log(myCar.show())

super()


참고 자료

따라하며 배우는 자바스크립트 A-Z

반응형