자바스크립트에서의 객체지향 프로그래밍

dahyeon·2022년 8월 25일
0

자바스크립트

목록 보기
6/7

Intro

객체지향 프로그래밍(Object-Oriented Programming)은 좀 더 나은 프로그램을 만들기 위한 프로그래밍 패러다임으로, 객체들을 조립해서 하나의 프로그램을 만드는 것이다.

객체지향 프로그래밍에서는 어떤 이름과 값 쌍들을 하나의 집합(객체)에 묶을 것인지, 즉 연관된 메소드와 그 메소드가 사용하는 변수들을 분류하고 그룹핑하는 것이 핵심이 된다.

객체지향 프로그래밍의 4대 원칙

  • 캡슐화(Encapsulation)

    사용자가 특정 프로퍼티/메소드에만 접근할 수 있게 하고, 중요한 정보들은 은닉함으로써 외부에 의해 수정되지 못하게 막는다.

    → 외부에 노출할 정보들은 public 키워드를 사용하고, 은닉할 정보들은 private 키워드를 사용하여 숨긴다. (프로그래밍 언어마다 방법이 다르며, 자바스크립트의 경우는 뒤에서 다룰 것이다.)

  • 추상화(Abstraction)

    캡슐화의 연장선상에 있는 개념으로, 연관된 정보들만 노출시키고 복잡한 내부 동작이나 다른 변수들은 숨기는 것.

    자동차를 예로 들어보면, 자동차의 내부 동작은 아주 복잡하지만 운전자는 엑셀, 브레이크, 핸들과 같은 일부 요소들만 사용해서 이를 제어한다. 운전자는 자동차가 어떻게 동작하는지에 대한 세세한 디테일들을 알 필요가 없으며, 이러한 디테일들이 은닉되어 있기에 편리하고 간단하게 운전을 할 수 있다.

    추상화를 구현하는 과정에서는

    • 목적과 관련이 없는 부분을 제외하고 필요한 부분을 포착하는 것과,
    • 객체들이 어떤 속성들을 공통적으로 갖고 있는지 포착하여 이를 또다른 객체로 만드는 것이 중요하다.
  • 상속(Inheritance)

    자식 객체는 부모 객체로부터 상속을 받아 부모 객체의 메서드와 프로퍼티를 사용할 수 있다. 상속을 하면 이미 정의된 프로퍼티와 코드를 재사용할 수 있고, 새로운 기능을 추가해서 확장된 객체를 만들 수 있다는 장점이 있다.

  • 다형성(Polymorphism)

    부모 객체로부터 메서드를 상속(Inheritance) 받아 자식 객체끼리 공유하되, 메서드가 자식 객체에서 각기 다른 기능을 하는 것. 다음과 같은 두 가지 방식으로 구현 가능하다:

    • Static polymorphism: Method Overloading. 같은 이름의 메소드를 중복하여 정의하는 것. 매개변수의 개수나 타입을 다르게 하면, 하나의 이름으로 메소드를 작성할 수 있다.

      ❗ 자바스크립트에서는 같은 이름을 가진 함수가 여러 개 있다면 가장 마지막에 정의된 함수가 실행된다. 따라서 overloading을 구현하기 위해서는 if/else 문이나, switch 문 등을 사용할 수 있다. (참고 링크)

    • Dynamic polymorphism: Method Overriding. 부모 클래스에서 이미 정의된 메소드를 자식 클래스에서 같은 시그니쳐를 갖는 메소드로 다시 정의하는 것.




자바스크립트에서의 객체지향 프로그래밍

자바스크립트의 객체는 이름과 값을 한 쌍으로 묶은 집합이다. 자바스크립트에서는 원시 타입(숫자, 문자열, 논리값, 특수값, 심벌)을 제외한 배열, 함수, 정규 표현식과 같은 다양한 요소가 객체이다.

자바스크립트에서 객체를 생성하는 방법

자바스크립트에서는 다양한 방식으로 객체를 생성할 수 있다.

1) 객체 리터럴

var card = { suit: "하트", rank: "A" };

2) 함수 선언문

function Card(suit, rank){
	this.suit = suit;
	this.rank = rank;
}

Card.prototype.getInfo = function(){
	return this.suit + this.rank;
}

var card = new Card("하트", "A");

3) Object.create

var card = Object.create(Object.prototype, {
	suit: { value: "하트" },
	rank: { value: "A" }
})

4) 클래스 선언문(ES6~)

class Card{
	constructor(suit, rank){
		this.suit = suit;
		this.rank = rank;
	}
	show(){
		console.log(this.suit + this.rank);	
	}
}

var card = new Card("하트", "A");

본 포스팅에서는 4) 클래스 구문을 사용할 때의 자바스크립트의 객체지향 프로그래밍 문법들을 살펴보도록 하겠다.


프로토타입이란

프로토타입이 무엇인지 이해하기 위해서 다음의 예를 살펴보자.

다음과 같은 코드에서,

function Person(name){
	this.name = name;
	this.describe = function(){
		return "I am " + this.name;
	}
}

var jane = new Person("Jane");
var andrew = new Person("Andrew");

각각의 인스턴스(jane, andrew…)가 생성될 때마다 describe라는 메서드가 인스턴스의 개수만큼 생성된다. 이는 그만큼의 메모리를 소비하게 된다는 것을 의미한다. 어차피 동일한 기능을 하는 메소드라면 이를 공유하게끔 할 수는 없을까? 즉, 공유할 메서드들이 담긴 객체(=프로토타입)가 따로 존재하고, 인스턴스가 생성되면 이 객체를 상속받아서 객체가 갖고 있는 메소드를 사용할 수 있게끔 하면 어떨까?

자바스크립트의 모든 객체는 프로토타입(prototype)이라는 객체를 가지고 있으며, 모든 객체는 그들의 프로토타입으로부터 프로퍼티와 메소드를 상속받는다. 즉, 상속되는 정보를 제공하는 객체를 프로토타입이라고 한다.

따라서 위의 경우에서 describe라는 메서드를 갖고 있는 객체를 각 인스턴스들의 프로토타입으로 설정해주면 된다.

다음과 같이 코드를 바꿔보자.

function Person(name){
	this.name = name;
}

Person.prototype.describe = function(){
		return "I am " + this.name;
}

var jane = new Person("Jane");
var andrew = new Person("Andrew");

이를 다이어그램으로 나타내면 다음과 같다.

  • 생성자의 prototype 프로퍼티가 가리키는 객체가 그 함수의 프로토타입 객체이다.

  • 인스턴스의 __proto__ 프로퍼티가 가리키는 객체가 인스턴스의 프로토타입이다.

  • 생성자로 객체를 생성할 때 인스턴스의 __proto__ 프로퍼티에 생성자의 prototype이 설정된다.

    위 예시에서 Person의 prototypePerson.prototype이 jane의 __proto__ 프로퍼티가 가리키는 값으로 설정되었다.

  • 프로토타입에는 constructor 프로퍼티가 있는데 이는 생성자를 가리킨다.

    위 예시에서 Person.prototype의 프로퍼티인 constructor은 Person을 가리킨다. 인스턴스인 jane과 생성자 Person은 직접적인 연결 고리가 없고 prototype을 통해 연결된다.


❗ 클래스 선언문의 경우, (constructor 다음에 작성된) class의 메서드는 생성자 함수의 prototype에 메서드로 추가된다.

다음의 코드를 보자.

class Animal {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }
  run(speed) {
    this.speed = speed;
    alert(`${this.name} runs with speed ${this.speed}.`);
  }
  stop() {
    this.speed = 0;
    alert(`${this.name} stands still.`);
  }
}
let animal = new Animal("My animal");

이를 다이어그램으로 나타내면 다음과 같다. class 생성자 내에서 정의한 run, stop 메서드가 prototype의 메소드로 설정되어있음을 알 수 있다.

그 외에 함수 선언문으로 작성한 생성자와 클래스 선언문으로 정의한 생성자의 주요 차이점:

  • 클래스 선언문은 자바스크립트 엔진이 끌어올리지 않으므로, 생성자를 사용하기 전에 작성되어야 한다.

  • 같은 이름을 가진 클래스가 여러 개 정의될 수 없다.

  • 클래스 선언문에 정의한 생성자만 따로 호출할 수 없다.



상속으로 클래스 확장하기

  • extends 키워드를 클래스 선언문에 붙여주면 다른 생성자를 상속받을 수 있다.

    다음의 코드를 보자.

    class Animal {
      constructor(name) {
        this.speed = 0;
        this.name = name;
      }
      run(speed) {
        this.speed = speed;
        alert(`${this.name} runs with speed ${this.speed}.`);
      }
      stop() {
        this.speed = 0;
        alert(`${this.name} stands still.`);
      }
    }
    
    class Rabbit extends Animal {
      hide() {
        alert(`${this.name} hides!`);
      }
    }
    
    let rabbit = new Rabbit("White Rabbit");
    
    rabbit.run(5); // White Rabbit runs with speed 5.
    rabbit.hide(); // White Rabbit hides!

    위 코드를 다이어그램으로 나타내면 다음과 같다.

    하나씩 살펴보자면,

    • class Rabbit에 작성한 메서드는 Rabbit.prototype에 추가된다.
    • class Animal에 작성한 메서드는 Animal.prototype에 추가된다.
    • class Rabbit extends Animal 와 같이 작성하면 Rabbit.prototype의 프로토타입Animal.prototype으로 설정된다.
    • Rabbit의 인스턴스는 class Animal과 class Rabbit의 메서드를 모두 사용할 수 있다.

    ❗ECMAScript6 부터는 __proto__ 프로퍼티에 [[Prototype]]의 값이 저장된다.


super 키워드

class Rabbit 내에서 Animal의 프로퍼티나 메서드에 접근하고 싶다면?

super 키워드를 사용하면 된다.

  • super.method(…)를 사용하면 부모 class의 메서드를 호출할 수 있다.
  • super(…)를 사용하면 부모 class의 constructor가 호출된다.

추후 설명할 개념이 좀 헷갈리기 때문에,

먼저 “new” 키워드를 사용했을 때 어떤 일이 발생하는지 알아보자.

1) class Animal과 같이 extends 키워드가 사용되지 않은 경우 = 부모 객체가 Javascript Object인 경우

  • new 연산자로 Animal 생성자를 사용하면 우선 빈 객체를 생성한다.
  • Animal.prototype을 생성된 객체(인스턴스)의 프로토타입으로 설정한다.
  • Animal 생성자를 실행하고(Animal에 등록된 constructor가 실행됨) 생성된 객체를 초기화한다. 이 때 this는 생성된 객체로 설정한다.
  • 완성된 객체를 결괏값으로 반환한다.

2) 앞선 예시와 같이 class Rabbit 처럼 상속 클래스이고 + 상속 클래스에 constructor가 정의되어 있지 않은 경우에는 인스턴스가 생성될 때 부모 클래스(Animal)의 constructor가 실행된다.

앞선 예시에 다음과 같은 코드를 추가하고 실행하면

class Animal {
  constructor(name) {
    this.speed = 0;
    this.name = name;
		console.log("Constructor called!!");
    console.log(this);
  }
...
}
...

let rabbit = new Rabbit("White Rabbit");

결과는 다음과 같다.

Constructor called!!
Rabbit {speed: 0, name: 'White Rabbit'}

따라서 앞선 예시 코드에서 super 키워드를 사용하지 않고 부모 class의 constructor에 정의된 프로퍼티(this.name)에 접근할 수 있었다.

  ...
class Rabbit extends Animal {
  hide() {
    alert(`${this.name} hides!`);
  }
}
...

3) 자식 클래스(extends 키워드가 사용된 경우, 상속 클래스)에 constructor를 정의할 경우에는 어떻게 될까?

다음의 코드에서,

super 키워드를 사용하기 전에 자식 객체에서 this 키워드를 사용하려고 하면 에러가 발생한다.

class Animal {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }
  // ...
}

class Rabbit extends Animal {

  constructor(name, earLength) {
    this.speed = 0;
    this.name = name;
    this.earLength = earLength;
  }

  // ...
}

// Doesn't work!
let rabbit = new Rabbit("White Rabbit", 10); // Error: this is not defined.

자식 class의 constructor 내에서 에서 this 키워드를 사용하기 전에 반드시 super를 통해 먼저 부모 class의 constructor를 호출해야 한다.


🤔 왜 에러가 발생할까?

3) 자식 클래스(extends 키워드가 사용된 경우, 상속 클래스)에 constructor를 정의할 경우

  • 위와 같은 일반 클래스가 new와 함께 실행되면, 빈 객체가 만들어지고 this에 이 객체를 할당하지만, Rabbit과 같은 상속 클래스의 생성자 함수가 실행되면, 일반 클래스에서 일어난 일이 일어나지 않는다. 상속 클래스의 생성자 함수는 빈 객체를 만들고 this에 이 객체를 할당하는 일을 부모 클래스의 생성자가 처리해주길 기대한다.
  • 따라서 부모 클래스의 constructor를 호출해줘야 한다!


프로토타입 체인

  • 특정 객체의 프로퍼티나 메소드에 접근하려고 할 때 해당 객체에 접근하려는 프로퍼티 또는 메소드가 없다면 [[Prototype]]이 가리키는 링크를 따라 자신의 부모 역할을 하는 프로토타입 객체의 프로퍼티나 메소드를 차례대로 검색한다. 이것을 프로토타입 체인이라 한다.

  • 즉, 객체는 자신이 가지고 있지 않은 특성을 프로토타입 객체에 위임한다.

  • 아래 그림에서 알 수 있듯, Object.prototype은 인스턴스에서 프로토타입 체인을 따라 거슬러 올라갈 수 있는 마지막 단계의 객체이다.


캡슐화

  • private 클래스 안에서만 접근할 수 있는 프로퍼티/메소드를 정의할 때에는 앞에 #를 붙인다. 다음의 예시를 보자.
    class CoffeeMachine {
      #waterLimit = 200;
    
      #checkWater(value) {
        if (value < 0) throw new Error("물의 양은 음수가 될 수 없습니다.");
        if (value > this.#waterLimit) throw new Error("물이 용량을 초과합니다.");
      }
    
    }
    
    let coffeeMachine = new CoffeeMachine();
    
    // 클래스 외부에서 private에 접근할 수 없음
    coffeeMachine.#checkWater(); // Error
    coffeeMachine.#waterLimit = 1000; // Error
    #waterLimit 프로퍼티는 class 내부에서는 읽고 쓸 수 있지만, 외부에서 접근하려고 하면 에러가 발생한다. 자식 클래스에서도 접근 불가능하다.

  • getters/setters 접근자는 객체가 가진 프로퍼티 값을 외부에서 읽거나 쓸 수 있도록 제공하는 메서드를 말한다. 접근자 프로퍼티 하나에 그 프로퍼티를 읽을 때의 처리를 담당하는 getter 함수와 쓸 때의 처리를 담당하는 setter 함수를 정의할 수 있다. 둘 중에 하나만 정의해도 된다. 다음의 예시를 보자.
    class User {
    
      constructor(name) {
        // invokes the setter
        this.name = name;
      }
    
      get name() {
        return this._name;
      }
    
      set name(value) {
        if (value.length < 4) {
          alert("Name is too short.");
          return;
        }
        this._name = value;
      }
    
    }
    
    let user = new User("John");
    alert(user.name); // John
    • name이라는 프로퍼티에 값을 할당하려고 하면 setter 함수인 set name(value)가 호출된다.

      위 예시에서는 constructor 내에서 this.name에 name을 할당할 때 호출된다.

    • name이라는 프로퍼티를 읽으려고 하면 getter 함수인 get name()이 호출된다.

      🤔 this._name과 같이 왜 프로퍼티 앞에 밑줄을 붙이는가? (참고 링크)

      자바스크립트에서 강제한 사항은 아니지만, 밑줄(_)은 프로그래머들 사이에서 외부 접근이 불가능한 프로퍼티나 메서드를 나타낼 때 쓴다고 한다.


정적 메서드

  • 메서드 앞에 static 키워드를 붙여주면 프로토타입이 아닌 클래스 함수 자체에 메서드를 설정할 수 있다.

  • 정적 프로퍼티도 만들 수 있다.

  • 정적 프로퍼티와 메소드는 상속된다!

    아래 코드에서, 정적 메소드인 compare과 정적 프로퍼티인 planet은 모두 상속되어서 Rabbit.compareRabbit.planet으로 접근 가능하다.

    class Animal {
      static planet = "지구";
    
      constructor(name, speed) {
        this.speed = speed;
        this.name = name;
      }
    
      run(speed = 0) {
        this.speed += speed;
        alert(`${this.name}가 속도 ${this.speed}로 달립니다.`);
      }
    
      static compare(animalA, animalB) {
        return animalA.speed - animalB.speed;
      }
    
    }
    
    // Animal을 상속받음
    class Rabbit extends Animal {
      hide() {
        alert(`${this.name}가 숨었습니다!`);
      }
    }
    
    rabbits.sort(Rabbit.compare); // 가능

    위 코드를 다이어그램으로 나타내면 다음과 같다.

    compare 함수가 Animal.prototype이 아닌 생성자 Animal 안에 정의되어 있다.

그렇다면 어떻게 Rabbit에서 접근이 가능한 것일까?

Rabbit extends Animal은 두 개의 [[Prototype]] 참조를 만들어 내기 때문이다.

  1. 함수 Rabbit은 프로토타입을 통해 함수 Animal을 상속받는다.
  2. Rabbit.prototype은 프로토타입을 통해 Animal.prototype을 상속받는다.
// 정적 메서드
alert(Rabbit.__proto__ === Animal); // true

// 일반 메서드
alert(Rabbit.prototype.__proto__ === Animal.prototype); // true

참고자료

Intro, 객체지향 프로그래밍의 4대 원칙 관련

객체지향 프로그래밍 - 생활코딩

What is object-oriented programming? OOP explained in depth

객체 지향 프로그래밍( OOP ) 개념

The Four Pillars of Object Oriented Programming

코딩교육 티씨피스쿨

자바스크립트에서의 객체지향 프로그래밍

다음 링크의 내용을 가장 많이 참고해서 작성하였습니다:
Class inheritance,
책 <모던 자바스크립트 입문>

그 밖에 내용 보충을 위해 참고한 자료들
prototype inheritance in javascript
What is super() in JavaScript? | CSS-Tricks
Prototype chains and classes * JavaScript for impatient programmers (ES2021 edition)

profile
https://github.com/dahyeon405

0개의 댓글