프로토타입 기반 OOP

EUNSUN KIM·2025년 1월 16일

JS

목록 보기
5/6

<OOP(Object Oriented Programming) 개념 간단 정리>

객체 지향 프로그래밍, OOP(Object Oriented Programming)는 프로그램을 명령어 또는 함수의 목록으로 보는 전통적인 명령형 프로그래밍의 관점에서 벗어나, 여러 개의 독립적 단위, 즉 객체(Object)의 집합으로 프로그램을 표현하려는 프로그래밍 패러다임을 말한다.

객체는 실세계의 사물이나 개념을 상태 데이터와 상태 데이터를 조작할 수 있는 동작으로 모델링하는 복합적인 자료구조다. 이때 객체의 상태 데이터를 프로퍼티(property), 동작은 메서드(method)라고 한다.

각 객체는 고유의 기능을 가지는 독립적인 부품이면서도, 다른 객체와 관계성을 가질 수 있다. 즉, 다른 객체와 메시지를 주고 받거나 데이터를 처리할 수도 있다. 또는 다른 객체의 상태 데이터나 동작을 상속받아 사용하기도 한다.

객체지향 프로그래밍의 4가지 원칙을 간단하게 표현하면 다음과 같다

  1. 추상화(Abstraction)
    프로그램의 모든 코드를 나열하는 대신, 프로그램의 기능 자체만 명확하게 드러나도록 코드의 디테일을 숨긴다

  1. 캡슐화(Encapsulation)
    외부에서 객체의 프로퍼티와 메서드에 직접 접근하지 못하게 하고, 대신 객체의 public interface(API)를 통해 외부와 상호작용하도록 한다

  1. 상속(Inheritance)
    한 객체의 프로퍼티와 메서드를 상속하는 자녀 객체를 생성할 수 있고, 이들은 위계 관계로 연결된다

  1. 다형성(Polymorphism)
    자녀 객체는 부모로부터 상속받은 메서드를 overwrite할 수 있다. 즉, 하나의 객체를 상속받으면서도 상속 받은 기능을 각각 다른 방식으로 구현하는 여러 객체들을 생성할 수 있다

자바와 같은 전통적인 객체 지향 프로그래밍 언어에서는 특정 기능을 객체로 구현할 때 위 4가지 특징을 가진 class를 구현하고, 필요한 곳에 class의 인스턴스 객체를 생성해 사용한다.

전통적인 OOP와 달리 자바스크립트는 class기반이 아니라 프로토타입(Prototype) 기반 상속을 구현한다.

프로토타입이란 객체의 상위(부모) 객체의 역할을 하는 객체로서, 자신과 연결된 하위 객체가 자유롭게 접근해 사용할 수 있는 공유 프로퍼티와 메서드를 제공한다.

자바스크립트의 프로토타입 기반 상속은 다음에 설명할 프로토타입 체인(Prototype chain)을 통한 ‘위임’ 이나 ‘연결’에 가깝다.


프로토타입 체인(Prototype chain)


자바스크립트의 모든 객체는 자신의 프로토타입 객체와 연결되어 있고, 각 프로토타입 객체도 마찬가지로 자신의 프로토타입 객체와 연결되어있다. 그리고 모든 객체의 최상위에 위치한 프로토타입은 Object.prototype 다.

이렇게 하위 객체가 자신의 프로토타입 객체와 연결되는 구조를 프로토타입 체인(prototype chain)이라고 한다.

함수 내에 선언되지 않은 변수를 스코프체인을 통해 찾는 것처럼, 객체도 정의되지 않은 프로퍼티나 메서드를 사용하면 프로토타입 체인을 통해 상위 객체에서 해당 메서드/프로퍼티를 찾아 첫 번째로 발견되는 것을 사용한다.

예를 들어 배열 객체마다 정의하지 않은 map, forEach 등의 메서드를 사용할 수 있는 이유가 바로 이 프로토타입 체인 때문이다.

모든 배열 객체의 프로토타입인 Array.prototype 객체에 이 메서드들이 정의되어 있고, 프로토타입 체인을 통해 배열 객체가 이에 접근할 수 있기 때문이다.

각 객체에 존재하는 [[Prototype]] 내부 슬롯값이 객체의 프로토타입에 대한 참조인데, 다음과 같이 .__proto__ 접근자를 통해 확인할 수 있다.

배열 객체의 프로토타입인 Array.prototype 객체

Array.prototype 객체의 프로토타입인 Object.prototype (최상위 객체)



프로토타입 기반의 인스턴스 생성하기


1. 생성자 함수

생성자 함수란 이름 그대로 객체(인스턴스)를 생성하는 함수다. 자바의 class를 생각할 수 있겠지만 전혀 다르다.

일단 자바스크립트의 함수는 일급객체로, 함수 객체만을 위한 내부 메서드와 내부 슬롯을 가지고 있다.
다음과 같이 일반함수로 정의된 함수는 내부에 [[constructor]] 라는 메서드를 가지고 있는데, [[constructor]] 를 가진 함수는 new 연산자와 함께 호출해서 객체를 생성하는 생성자 함수로 사용할 수 있다.

// 일반함수의 정의: 함수 선언문, 함수 표현식
function foo(){}
const bar = function(){}

new foo(); // -> 내부의 [[constructor]]가 실행, 객체를 생성한다
foo(); // -> 내부의 [[Call]]이 호출, 일반 함수처럼 실행되고 객체를 생성하지 않는다
new bar(); // -> bar {}

<인스턴스 생성 과정>

function Person(firstName, birthYear) {
  this.firstName = firstName;
  this.birthYear = birthYear;
}

Person.prototype.calcAge = function() {
  console.log(2037 - this.birthYear);
}

const sunny = new Person('Sunny', 1994);

위 코드에서 일반함수 Personnew 연산자와 함께 호출하면 다음 과정이 발생한다.

  1. 빈 객체{}가 생성되고 함수 실행컨텍스트의 this가 생성된 객체에 바인딩 된다

    • 이 과정은 함수 코드 실행 직전, 실행 컨텍스트 생성 단계에서 이뤄진다
  2. 생성된 객체의 .__proto__ 프로퍼티에 생성자 함수의 .prototype 객체가 바인딩 된다

    • .prototypenew 연산자와 함께 호출할 수 있는 함수에만 존재하는 프로퍼티로, 함수 호출을 통해 생성할 객체의 프로토타입을 가지고 있다.
      인스턴스 공통으로 사용할 프로퍼티나 메서드는 반드시 아래처럼 생성자함수의 .prototype 에 프로퍼티로 추가해야 한다.
        Person.prototype.calcAge = function() {
          console.log(2037 - this.birthYear);
        } // 생성자 함수 내부에서 this, 즉 생성될 인스턴스의 프로퍼티로 추가한 값들은 인스턴스마다 새롭게 생성된다.
  1. 생성자 함수 내부 코드 실행이 완료되면 빈 객체를 바인딩한 this가 암묵적으로 반환된다.

    • 만약 생성자 함수에 return 값을 명시하면 생성한 객체 대신 명시한 return 값이 반환된다.

<생성된 인스턴스 확인>


// 객체의 .__proto__는 객체를 생성한 함수의 .prototype 프로퍼티다
console.log(sunny.__proto__ === Person.prototype); // true

// Person.prototype.calcAge 
sunny.calcAge(); // 43

// 함수의 .prototype는 함수의 프로토타입이 아니라, 
// 함수에 의해 생성되는 모든 객체들의 prototype이다
console.log(Person.prototype.isPrototypeOf(Person)); // false
console.log(Person.prototype.isPrototypeOf(sunny)); // true

Person.prototype.species = 'Homo Sapiens';
console.log(sunny.species); // 'Homo Sapiens'

// Object.prototype.hasOwnProperty 
//: 프로토타입을 제외한 객체 고유의 프로퍼티중에 특정 프로퍼티가 있는지 확인한다
console.log(sunny.hasOwnProperty('species')); // false 

2. Class

자바스크립트의 class는 생성자 함수를 좀 더 전통 OOP의 class 처럼 사용할 수 있게끔 하는 syntatic sugar다. 즉, 내부적으로는 생성자 함수와 동일하게 프로토타입 기반 상속을 사용하지만 겉으로는 전통적인 OOP의 class에 가깝다.


💡 class vs 생성자 함수
class와 생성자 함수는 내부적으로 동일하게 동작하기 때문에, 둘 중 무엇을 사용할지는 전적으로 취향의 문제다. class가 프로토타입기반의 상속을 코드상에서 숨기기 때문에 싫어하는 사람도 있고, 생성자 함수보다 더 코드를 깔끔하게 작성하게 해주기 때문에 좋아하는 사람도 있다고 한다.


자바스크립트의 class는 사실 함수로, 다음과 같은 특징이 있다.

  • 호이스팅(hoisting) 되지 않는다
  • 일급 객체다
    • 함수의 파라미터가 될 수도 있고, 반환 값이 될 수도 있다
  • 내부 코드에 strict mode가 자동으로 적용된다

따라서 class는 표현식으로도 선언할 수 있다. (보통은 전통 OOP에서 하듯 선언한다)

// 일반적인 OOP식 선언
class PersonCl {
	... 
}

// 함수 표현식으로 선언 
const PersonCl = class {}

<인스턴스 생성 과정>

class Person{
  constructor(fullName, birthYear){
    // new 연산자와 함께 호출시 이 부분이 호출된다 
    this.fullName = fullName;
    this.birthYear = birthYear;
  }

  // constructor 함수 밖의 메서드는 모두 Person.prototype에 추가된다 
  calcAge(){
    console.log(2037 - this.birthYear)
  }

  greet(){
    console.log(`hey ${this.fullName}`);
  }

}

const jessica = new Person('Jessica', 1996)

위 코드에서 Person 클래스를 new 연산자와 함께 호출하면 생성자 함수 호출때와 마찬가지로 빈 객체가 생성되고, this가 바인딩 된다.

생성자 함수와 다른 점은, class는 호출시 constructor 함수 밖에 선언된 메서드를 자동으로 생성자의 .prototype에 추가한다. 즉, 다음 코드를 내부적으로 실행한다.

Person.prototype.calcAge = function(){
 console.log(2037 - this.birthYear)
}

Person.prototype.greet = function(){
  console.log(`hey ${this.fullName}`);
}

<생성된 인스턴스 확인>

jessica.calcAge(); // 41
jessica.greet(); // 'hey Jessica'

console.log(jessica.__proto__ === Person.prototype); // true

3. Object.create()

앞서 생성자 함수와 class는 호출시 자동으로 함수의.prototype 객체를 생성된 객체의 프로토타입으로 추가했다.

Object.create 은 생성자함수, class보다 덜 선호되는 방식이지만, 생성하는 객체의 프로토타입을 특정 객체로 직접 지정할 수 있다.

// 프로토타입으로 사용할 객체 선언 
const PersonProto = {
  calcAge() {
    console.log(2037 - this.birthYear);
  },

	// 객체의 초기값세팅 메서드
  init(firstName, birthYear){
    this.firstName = firstName;
    this.birthYear = birthYear;
  }
}

// PersonProto를 프로토타입으로 갖는 빈 객체를 생성한다 
const steven = Object.create(PersonProto); // PersonProto라는 프로토타입 객체에 연결된 새 객체 생성

// 생성한 객체의 초기값을 직접 세팅한다 
steven.init("Steven", 2002");

위 코드에서 눈에 띄는 부분은 아마 PersonProtoinit 메서드일 것이다. 이는 생성된 객체의 프로퍼티를 세팅하기 위해 구현한 메서드다.

init 메서드는 steven 객체의 프로토타입인 PersonProto의 메서드로, 프로토타입 체인을 통해 steven 객체가 사용할 때 thissteven 객체를 가리키게 되므로 steven 객체의 프로퍼티를 세팅하게 된다.

Object.create는 빈 객체를 생성하고 프로토타입을 지정하는 것 까지만 하기 때문에, 객체 프로퍼티를 세팅하는 로직을 따로 작성해야 한다는 번거로움이 있다.

<생성된 인스턴스 확인>


steven.calcAge(); // 35

console.log(steven.__proto__ === PersonProto); // true 
profile
현재가 미래를 만든다

0개의 댓글