10/18/2023

Object-Oriented Programming

OOP(Object-Oriented Programming)

객체 지향 프로그래밍(OOP) 객체 개념을 기반으로 하는 프로그래밍 패러다임으로 패러다임은 코드의 스타일, 즉 코드를 작성하고 구성하는 방식을 의미한다. 객체를 사용하여 사용자나 할 일 목록 항목과 같은 실제 세계의 측면을 모델링하고 HTML 구성 요소나 자료구조와 같이 추상적인 기능을 설명한다. 객체는 속성이라고 부르는 데이터와 메서드이라고 부르는 코드를 포함한다. 따라서 객체를 사용함으로써 모든 데이터와 해당 동작을 하나의 블록 안에 담을 수 있다. OOP에서 객체는 코드의 자체 포함된(self-contained) 조각 또는 코드 블록이다. 객체를 애플리케이션의 구성 요소이며 서로 상호 작용한다. 상호 작용은 공개 인터페이스 또는 API를 통해 발생한다. API는 객체 외부의 코드가 접근할 수 있는 메서드의 묶음이며 이를 통해 객체와 통신한다. OOP는 코드를 잘 구성해서 유연하고 유지 관리하기 쉽게 만들기 위한 목표로 개발되었다. OOP 이전에는 여러 함수를 통합하거나 구조 없이 전역 범위에서 코드를 작성했다. 이러한 스파게티 코드는 큰 코드 베이스를 유지 관리하기 매우 어렵게 만들며 새로운 기능을 추가하기는 더욱 어렵다. 따라서, OOP의 아이디어는 이 문제의 해결책으로 만들어졌다. 참고로, OOP가 코드를 정리하고 유지 관리하는 유일한 방법은 아니다. 다른 패러다임이 많이 있으며 그 중 하나는 함수형 프로그래밍이다.


Principles of OOP

Abstraction

추상화는 중요하지 않은 세부 사항을 무시하거나 숨김으로써 구현하려는 대상의 전체적인 관점을 파악하게 해주며 구현과 관련이 없는 세부 사항을 다루지 않고 쉽게 이해하게 돕는 원칙이다. 예를 들어, 휴대폰를 구현한다고 가정하며 추상화 없이 휴대폰에 관한 모든 것 내부적인 것을 포함하여 모든 것을 설계할 수 있다. 예를 들면, 휴대폰의 온도와 전압을 확인하고 진동 모터나 스피커를 켜는 것과 같은 저수준의 세부사항도 포함한다. 하지만 사용자가 휴대폰을 사용할 때 모든 세부사항이 필요하지 않다. 이러한 세부사항은 추상화된다. 사용자는 홈 버튼, 볼륨 버튼 및 화면을 사용하여 휴대폰과 상호 작용한다. 내부적으로 휴대폰은 진동하고 전압을 측정하고 스피커를 켜야하지만 이러한 세부사항을 사용자로부터 숨긴다. 이것이 바로 추상화가 의미하는 것입니다. 또 다른 예로 addEventListener 함수를 생각해 보면 함수가 내부적으로 어떻게 작동하는지 정확히 알 필요가 없다.

Encapsulation

캡슐화는 클래스 내에서 속성과 메서드를 비공개로 유지하여 외부에서 접근할 수 없게 하고 일부 메서드를 공개 인터페이스(API)로 노출하는 원칙이다. 캡슐화를 통해 외부 코드가 실수로 내부 속성 및 상태를 실수로 조작하는 것을 방지할 수 있다.또한, 외부 코드를 망가뜨리지 않고 내부 코드를 변경할 수 있어서 버그 및 스파게티 코드를 방지하는 데 도움이 된다.

Inheritance

상속은 특정 클래스의 모든 속성과 메서드를 하위 클래스에서 사용 가능하게 만들어 클래스 간 계층적인 관계를 형성하고 이를 통해 공통 로직을 재사용하고 현실 세계의 관계를 모델링하는 원칙이다. 하나는 부모 클래스이고 다른 하나는 자식 클래스가 되며, 자식 클래스는 부모 클래스를 확장(extends)한다. 자식 클래스는 부모 클래스에서 모든 속성과 메서드를 상속받는다. 공식적인 용어로는 상속은 특정 클래스의 모든 속성과 메서드를 자식 클래스에서 사용 가능하게 만들며 이는 두 클래스 간의 계층 구조를 형성한다. 이것의 목표는 두 클래스 모두에게 공통인 로직을 재사용하는 것이다.

Polymorphism

다형성은 자식 클래스는 부모 클래스에서 상속한 메서드를 덮어쓸 수 있는 원칙이다. 그리스어에서 다형성은 말 그대로 "다양한 모양"이라는 뜻이다.


Class

전통적인 OOP에서 새로운 객체를 생성하려면 클래스라고 불리는 것을 사용한다. 클래스는 청사진으로 생각할 수 있으며 클래스에 설명된 규칙을 기반으로 새로운 객체를 만들 수 있다. 하지만, 청사진은 추상적인 계획 혹은 규칙 집합이다. 클래스를 통해 생성된 모든 객체를 해당 클래스의 인스턴스라고 부른다. 다시 말해, 인스턴스는 클래스에서 생성된 실제 객체이며 클래스 자체는 객체가 아니다. 인스턴스는 포함하는 데이터가 다를 수 있지만 모두 동일한 기능을 공유한다. 인스턴스를 생성하는 이 과정을 인스턴스화라고 한다.


OOP in JavaScript

JavaScript에서 OOP는 앞서 설명한 방식과 다르게 작동한다. JavaScript에서 프로토타입이라고 하는 것이 있다. JavaScript의 모든 객체는 특정 프로토타입 객체에 연결되기에 각 객체는 프로토타입을 가진다고 말한다. 프로토타입 객체는 해당 프로토타입에 연결된 모든 객체가 접근하고 사용할 수 있는 메서드와 속성을 포함한다. 이 동작을 프로토타입 상속이라고 한다. 다시 말해, 프로토타입에 연결된 모든 객체는 해당 프로토타입에 정의된 메서드와 속성을 사용할 수 있다. 그래서 기본적으로 객체는 프로토타입으로부터 메서드와 속성을 상속받는다. 이 상속은 앞서 언급한 상속과는 다르다. 앞의 상속은 한 클래스가 다른 클래스에서 상속하는 것이지만 이 상속은 기본적으로 객체가 객체로 상속을 하는 것이다. 객체가 연결된 프로토타입 객체로 행동을 위임한다고 말할 수도 있다. 행동은 여기서 메서드의 다른 용어이다. 프로토타입 상속뿐만 아니라 이 메커니즘을 위임이라고 부를 수 있다. 이와 달리, 클래스 기반의 전통적인 OOP에서 행동, 즉 메서드는 실제로 클래스에서 인스턴스로 복사된다.

Implementing OOP

JavaScript에서 OOP를 구현하는 세 가지 다른 방법이 있다. 바로 생성자 함수 기법, ES6 클래스 및 Object.create 메서드이다.
  • 생성자 함수: 객체를 프로그래밍 방식으로 생성하는 방법으로 새로운 객체의 프로토타입을 설정하는 함수를 사용한다. 실제로 Array, Map 또는 Set과 같은 내장 객체가 구현된 방법이다. JavaScript에서 OOP가 거의 처음부터 이 방식으로 이루어져 왔다.
  • ES6 클래스: ES6 클래스는 JavaScript에서 OOP를 구현하는 더 현대적인 방법이다. 그러나 이 클래스는 전통적인 클래스가 아니며 생성자 함수 위에 구문 설탕이라고 불린다. 즉, ES6 클래스는 사실상 생성자 함수 위에 있는 추상화 계층에 불과하다. JavaScript에서 OOP를 구현하는 더 나은 구문이며 뒤에서 ES6 클래스는 생성자 함수로 구현되며 프로토타입 상속도 사용한다.
  • Object.create() 메서드: 객체를 프로토타입 객체에 연결하는 가장 간단하고 직접적인 방법이지만 다른 두 방법만큼 널리 사용되지는 않는다.


Constructor Function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 엄밀히 말하면 클래스가 아닌데 JavaScript는 전통적인 객체 지향 프로그래밍의 의미에서는 클래스를 가지고 있지 않다.
// 그러나 생성자 함수를 통해 객체를 생성한다.
// 생성자 함수는 JavaScript의 시작부터 클래스를 시뮬레이션하기 위해 사용되어 왔기에 sunshim과 kildong을 Person의 인스턴스라고 말할 수 있다.
const Person = function (name, birthday) {
  console.log(this); // 빈 객체, Person {}
 
  // 인스턴스 속성
  this.name = name;
  this.birthday = birthday;
 
  // 절대로 생성자 함수에서 메서드 생성하지 않기!
  // this.calcAge = function () {
  //  console.log(2023 - this.birthday);
  // };
};
 
const sunshim = new Person("박선심"1966);
const kildong = new Person("홍길동"1111);
console.log(sunshim); // Person { name: '박선심', birthday: 1966 }
 
const sungil = "성일";
 
console.log(sunshim instanceof Person); // true
console.log(sungil instanceof Person); // fals
cs
생성자 함수는 실제로 일반 함수이다. 일반 함수와 생성자 함수의 유일한 차이점은 생성자 함수를 new() 연산자와 함께 호출한다는 것이다. 함수 표현식과 함수 선언식은 생성자 함수로 작동하지만 화살표 함수는 작동하지 않는데 화살표 함수가 this 예약어를 갖지 않기 때문이다. new() 연산자는 특수한 연산자로 함수를 호출한다. new() 연산자는 뒤에서 다음 4단계를 거친다.
  1. 새로운 빈 객체가 생성된다.
  2. 생성자 함수가 호출되며 this 예약어는 단계1에서 생성된 객체로 설정된다. 즉, 실행 컨텍스트 내에서 함수의 this 예약어는 단계1에서 생성된 새 객체를 가리킨다.
  3. 단계1에서 생성된 객체가 생성자 함수의 prototype 속성(__proto__ 속성)과 연결된다.
  4. 단계1에서 생성된 객체는 생성자 함수에서 자동으로 반환된다. 함수가 처음에 생성한 빈 객체를 자동으로 반환하지만 이 시점에서 객체는 더 이상 비어 있을 필요가 없다.

함수의 끝에서 this 예약어가 반환될 것이기 때문에 빈 객체에 추가하는 모든 것이 함수에서 반환된다. 속성과 달리 메서드를 생성자 함수 내에서 절대로 만들면 안되는데 생성자 함수를 사용하여 수백 개, 수천 개 또는 수만 개의 객체를 만들경우 각 객체가 메서드를 가진다. 즉, 1000개의 객체가 있다면 사실상 메서드의 1000개의 복사본을 만드는 것이다. 이는 코드의 성능에 대한 치명적인 영향을 미친다. 문제를 해결하기 위해 프로토타입과 프로토타입 상속을 사용한다.


Prototype

JavaScript의 생성자 함수를 포함한 모든 함수는 prototype이라는 속성을 자동으로 갖고 있다. 생성자 함수에 의해 생성된 모든 객체는 해당 생성자 함수의 prototype 속성에 정의된 모든 메서드와 속성에 접근할 수 있다. 객체는 __proto__라는 특별한 속성을 갖고 있다. 이 __proto__ 속성은 어디에서 올까? 바로 생성된 빈 객체를 프로토타입에 연결하는 new() 연산자의 단계 3에서 발생한다. 여기서 __proto__ 속성을 생성하고 그 값을 호출된 함수의 prototype 속성으로 설정한다. 참고로, 생성자 함수.prototype은 생성자 함수의 프로토타입이 아니다. prototype 속성에 속성을 설정할 수 있는데 이 속성은 객체 자체에 직접적으로 존재하지 않으므로 객체의 자체 속성이 아니다. 따라서, 자체 속성은 객체 자체에 직접 선언된 속성만 포함하며 상속된 속성은 포함하지 않는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 프로토타입 메서드를 설정한다.
Person.prototype.calcAge = function () {
  console.log(2023 - this.birthday);
};
 
// Person 생성자 함수의 모든 객체는 calcAge 메서드에 접근 가능하다.
// 메서드의 복사본을 생성하여 각 객체에 첨부하지 않고 메서드를 상속한다(즉, 프로토타입 상속).
// 생성자 함수를 사용하여 생성된 모든 객체는 함수를 재사용한다.
// 각각의 객체에 대한 this 예약어는 메서드를 호출하는 객체에 항상 설정한다.
sunshim.calcAge(); // 57
kildong.calcAge(); // 912
 
// sunshim 객체의 프로토타입은 생성자 함수(Person)의 프로토타입 속성(prototype)이다.
console.log(sunshim.__proto__);
console.log(sunshim.__proto__ === Person.prototype); // true
 
// 객체의 프로토타입이 Person인지 확인하는 메서드
console.log(Person.prototype.isPrototypeOf(sunshim)); // true
console.log(Person.prototype.isPrototypeOf(Person)); // false
 
// 프로토타입 속성을 설정한다.
Person.prototype.nationality = "ROK";
 
console.log(sunshim.nationality, kildong.nationality); // ROK ROK
console.log(sunshim.hasOwnProperty("name")); // true, 자체 속성. 
console.log(sunshim.hasOwnProperty("nationality")); // false, 상속 속성.
 
// 프로토타입 체인.
console.log(sunshim.__proto__); // Person.prototype
console.log(sunshim.__proto__.__proto__); // Object.prototype
console.log(sunshim.__proto__.__proto__.__proto__); // null, Object.prototype은 일반적으로 프로토타입 체인의 맨 위에 있다.
 
console.dir(Person.prototype.constructor); // Person 생성자 함수를 가리키는 constructor 속성.
cs

sunshim 객체가 프로토타입에 연결되어 있으며 프로토타입에서 메서드와 속성을 조회할 수 있는 능력을 프로토타입 체인(prototype chain)이라고 부른다. 즉, sunshim 객체와 프로토타입은 프로토타입 체인을 형성하며 프로토타입 체인은 여기에서 끝나지 않는다. Person.prototype 자체도 객체이며 JavaScript의 모든 객체는 프로토타입을 가진다. 따라서, Person.prototype도 프로토타입을 가지는데 바로 Object.prototype(즉, Person.prototype의 __proto__ 속성)이다. Person.prototype은 내장된 Object 생성자 함수에 의해 생성되며 이 생성자 함수는 객체 리터럴을 만들 때 내부에서 호출되는 함수이다. 즉, {} === new Object(...)이다. 생성자 함수의 prototype 속성은 __proto__ 속성의 반대인 constructor 속성을 가지는데 이 속성은 다시 생성자 함수를 가리킨다(즉, Person.prototype.constructor === Person).

정리하면, 프로토타입 체인은 객체들을 프로토타입을 통해 연결하는 일련의 링크이다. Object.prototype은 일반적으로 프로토타입 체인의 맨 위에 있으므로 그 자체의 프로토타입은 null이다. 그래서, __proto__ 속성은 단순히 null(즉, Object.prototype의 __proto__ 속성)을 가리킨다. 프로토타입 체인이 스코프 체인과 매우 유사하다. 스코프 체인에서 JavaScript는 특정 스코프에서 특정 변수를 찾을 수 없으면 다음 스코프 및 스코프 체인으로 올라가 변수를 찾는다. 프로토타입 체인에서 JavaScript는 특정 객체에서 특정 속성 또는 메서드를 찾을 수 없으면 다음 프로토타입 및 프로토타입 체인으로 올라가 해당 속성 또는 메서드를 찾는다. 위의 코드에서 hasOwnProperty() 메서드가 대표적인 예시이다.


Prototypal Inheritance on Built-in Objects

내장 객체인 배열과 같은 객체에서 프로토타입 상속과 프로토타입 체인을 확인할 수 있다. 모든 배열의 프로토타입은 Array 생성자 함수의 prototype 속성으로 각 배열은 이 프로토타입에서 메서드를 상속받는다. 객체를 객체 리터럴 {}로 만들면 Object 생성자 함수가 뒤에서 호출되는 것처럼 배열을 배열 리터럴 []로 생성하면 Array 생성자 함수가 호출된다(즉, [] === new Array(..,)). 참고로, Array 생성자 함수의 프로토타입은 프로토타입 체인을 통해 Object 생성자 함수의 prototype 속성이다. 모든 배열이 프로토타입에서 모든 메서드를 상속하기에 이를 사용하여 배열의 기능을 더 확장할 수 있다. 다시 말해, Array.prototype에 새로운 메서드를 추가하면 모든 배열이 해당 메서드를 상속한다. 내장 객체의 프로토타입을 확장하는 것은 일반적으로 좋지 않은데 1번째는 JavaScript의 다음 버전에서 같은 이름의 메서드를 추가할 수 있지만 다르게 작동할 수 있고 그러면 코드가 제대로 작동하지 않는다. 2번째로 다른 개발자들과 함께 작업할 여러 개발자가 다른 이름으로 같은 메서드를 구현하면 많은 버그를 생성할 수 있다. 함수 자체도 객체이므로 프로토타입을 가진다. 함수의 프로토타입에는 apply() 메서드, bind() 메서드 및 call() 메서드가 존재한다. 따라서 모든 함수에서 해당 메서드를 호출할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const arr = [1233345677]; // [] === new Array(...)
console.log(arr.__proto__); // 배열은 프로토타입(Array.prototype)에서 메서드를 상속받는다.
console.log(arr.__proto__ === Array.prototype);
console.log(arr.__proto__.__proto__ === Object.prototype); // Array의 프로토타입은 Object.prototype
 
// Array.prototype에 메서드를 생성해서 배열의 기능을 확장한다.
// 추천하지 않는 방법
Array.prototype.unique = function () {
  return [...new Set(this)];
};
 
console.log(arr.unique());
 
// call(), bind(), apply() 메서드가 존재한다.
console.dir((x) => {
  x * 3;
});
cs


ES6 Class

JavaScript의 클래스는 Java 또는 C++과 같은 언어의 전통적인 클래스처럼 작동하지 않는다. JavaScript의 클래스는 내부에서 여전히 프로토타입 상속을 구현한다. 즉, ES6 클래스는 단순히 생성자 함수 위의 구문 설탕이다. 클래스를 정의할 때 함수처럼 클래스 표현식과 클래스 선언식을 사용할 수 있다. 인자 없이 함수처럼 작동하는데 클래스가 함수의 특수 유형이기 때문이다. 클래스는 constructor() 메서드를 가지는데 생성자 함수처럼 유사한 방식으로 작동한다. 생성자 함수처럼 원하는 속성을 가진 객체에 대한 인자를 전달한다. 객체를 만드는 작업도 생성자 함수 정확히 동일한 방식으로 작동한다. 새로운 객체를 만들 때마다 new() 연산자를 사용하면 constructor() 메서드가 자동으로 호출되고 constructor() 메서드 내부에서 this 예약어는 새로 만들어진 빈 객체로 설정된다. 클래스에서 작성하는 constructor() 메서드 외부의 모든 메서드는 .prototype 속성에 추가되며 객체의 프로토타입에 있으며 객체 자체에는 없다. 즉, 프로토타입 상속이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 클래스 표현식
// const Person = class {};
 
// 클래스 선언식
class Person {
  constructor(name, birthday) {
    this.name = name;
    this.birthday = birthday;
  }
 
  // 메서드는 .prototype 속성에 추가된다. 즉, 프로토타입 상속
  calcAge() {
    console.log(2023 - this.birthday);
  }
 
  sayHi() {
    console.log(`안녕하세요! ${this.name}입니다~`);
  }
}
 
const sunshim = new Person("박선심"1966);
console.log(sunshim);
sunshim.calcAge();
 
console.log(sunshim.__proto__ === Person.prototype); // true
 
// 똑같이 작동! 클래스는 프로토타입 상속을 숨긴다.
// Person.prototype.sayHi = function () {
//   console.log(`안녕하세요! ${this.name}입니다~`);
// };
 
sunshim.sayHi();
cs

클래스에 대해 중요한 3가지는 다음과 같다. 먼저 클래스는 호이스팅 되지 않는다. 호이스팅 되는 함수 선언식과 달리 클래스 선언식은 호이스팅되지 않는다. 함수처럼 클래스도 일급 시민이다. 즉, 클래스를 전달하고 클래스를 반환할 수 있다는 것을 의미한다. 이는 사실 클래스가 내부에서 특수한 종류의 함수이기 때문이다. 클래스는 항상 strict 모드에서 실행된다. 전체 스크립트에 대해 strict 모드를 활성화하지 않았더라도 클래스 내의 모든 코드는 strict 모드에서 실행된다.


Getter & Setter

JavaScript의 모든 객체는 세터(setter) 속성 및 게터(getter) 속성을 가질 수 있다. 이러한 특별한 속성을 접근자 속성(accessor property)이라고 하며 더 일반적인 속성은 데이터 속성(data property)이라고 한다. 게터와 세터는 기본적으로 값을 가져오고 설정하는 함수이다. 그러나 외부에서 보면 여전히 일반적인 속성처럼 보인다. 따라서, 메서드로 호출하지 않고 속성으로 사용할 수 있다. 세터와 게터는 다른 메서드와 마찬가지로 프로토타입에 설정한 일반적인 메서드이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 클래스나 객체나 똑같이 작동한다.
const history = {
  owner: "sunshim",
  titles: ["순경""경장""경사""경위""경감"],
 
  get current() {
    return this.titles.slice(-1).pop();
  },
 
  set current(title) {
    this.titles.push(title);
  },
};
 
// 메서드를 호출하는 것이 아니라 속성으로 사용한다.
console.log(history.current);
 
history.current = "경정";
console.log(history.titles);
 
class Person {
  constructor(name, birthday) {
    this.name = name;
    this.birthday = birthday;
  }
 
  calcAge() {
    console.log(2023 - this.birthday);
  }
 
  sayHi() {
    console.log(`안녕하세요! ${this.name}입니다~`);
  }
 
  get age() {
    return 2023 - this.birthday;
  }
 
  // 이미 존재하는 name 속성에 대한 세터를 생성한다.
  // 코드가 실행될 때마다, 즉 name을 this 예약어로 설정할 때마다 setter가 실행된다.
  // 세터와 생성자 함수 모두 같은 속성 name을 설정하기 때문에 오류가 발생한다.
  // 이미 존재하는 속성을 설정하려고 하는 세터가 있을 때 관례적으로 언더스코어(_)를 추가한다.
  // 새로운 속성 _name 추가한다.
  set name(name) {
    if (name.length === 3this._name = name;
    else console.error(`성과 이름을 포함하세요`);
  }
 
  get name() {
    return this._name;
  }
}
 
const sunshim = new Person("박선심"1966);
console.log(sunshim.age);
console.log(sunshim.name);
cs

Static Method

정적 메서드는 생성자 함수에 연결된 메서드로 프로토타입에 존재하지 않기 때문에 상속이 되지 않는다. 즉, 생성자 함수의 객체는 정적 메서드를 사용할 수 없다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
const Person = function (name, birthday) {
  console.log(this); // 빈 객체, Person {}
 
  this.name = name;
  this.birthday = birthday;
};
 
const sunshim = new Person("박선심"1966);
 
Person.talk = function () {
  console.log("이야기 하는 중...");
  console.log(this); // this는 Person 생성자 함수
};
 
Person.talk();
// Person 생성자 함수의 프로토타입 속성(Person.prototype)에 존재하지 않기 때문에 상속이 되지 않는다.
// 즉, Person 생성자 함수의 객체는 정적 메서드를 사용할 수 없다.
// sunshim.talk(); 함수가 아니라서 오류가 발생한다
 
 
////////////////////////////////////////////////////////
 
class Person {
  constructor(name, birthday) {
    this.name = name;
    this.birthday = birthday;
  }
 
  // 인스턴스 메서드.
  // 프로토타입 속성에 추가되어 모든 인스턴스가 접근할 수 있다.
  calcAge() {
    console.log(2023 - this.birthday);
  }
 
  sayHi() {
    console.log(`안녕하세요! ${this.name}입니다~`);
  }
 
  get age() {
    return 2023 - this.birthday;
  }
 
  set name(name) {
    if (name.length === 3this._name = name;
    else console.error(`성과 이름을 포함하세요`);
  }
 
  get name() {
    return this._name;
  }
 
  // 정적 메서드
  static talk() {
    console.log("이야기 하는 중...");
    console.log(this); // this는 Person 클래스
  }
}
 
Person.talk();
cs


Object.create()

Object.create() 메서드를 사용하여 OOP를 구현해도 프로토타입 상속이라는 개념은 여전히 존재한다. 하지만, 프로토타입 속성(prototype), 생성자 함수나 new() 연산자는 없다. 대신, Object.create() 메서드를 사용하여 객체의 프로토타입을 원하는 다른 어떤 객체로든 수동으로 설정할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 모든 Person 객체의 프로토타입
const PersonPrototype = {
  calcAge() {
    console.log(2023 - this.birthday);
  },
 
  // 생성자 함수와 비슷하지만 생성자 함수와 관련이 없다.
  // 왜냐하면 new() 연산자를 사용하는 것이 아니라 init()을 호출하고 인자를 전달한다.
  init(name, birthday) {
    this.name = name;
    this.birthday = birthday;
  },
};
 
// PersonProto 프로토타입과 연결된 새로운 객체를 반환한다. sunshim은 비어있는 객체
const sunshim = Object.create(PersonPrototype);
console.log(sunshim);
sunshim.name = "박선심";
sunshim.birthday = 1966;
sunshim.calcAge();
 
console.log(sunshim.__proto__ === PersonPrototype); // true
const neo = Object.create(PersonPrototype);
neo.init("Neo"1999);
neo.calcAge();
cs


Inheritance - Constructor Function

이전 모든 내용은 기본적으로 객체가 프로토타입에서 메서드를 상속한다. 즉, 객체의 행동을 프로토타입에 위임한다. 하지만 원래 의미의 상속은 클래스 간의 상속이다. 지금까지 한 인스턴스와 프로토타입 속성 사이의 프로토타입 상속이 아니다. 생성자 함수를 사용해서 클래스 간 상속을 구현할 경우 Object.create() 메서드를 사용해서 자식 생성자 함수의 prototype 속성의 __proto__ 속성이 부모 생성자 함수의 prototype 속성을 가리켜야 한다. Object.create() 메서드가 빈 객체({})를 반환하기 때문에 상속은 반드시 메서드 추가 전에 발생해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
const Person = function (name, birthday) {
  this.name = name;
  this.birthday = birthday;
};
 
Person.prototype.calcAge = function () {
  console.log(2023 - this.birthday);
};
 
const Student = function (name, birthday, course) {
  // new() 연산자를 사용하고 있지 않아서 일반적인 함수 호출이다.
  // 일반적인 함수 호출에서는 this 예약어는 undefined로 설정된다.
  // 수동으로 this 예약어를 설정해야 한다.
  // Person(name, birthday);
 
  // call() 메서드는 Person 생성자 함수를 호출하는데 첫 번째 인자로 this 예약어를 지정할 수 있다.
  // Person 생성자 함수 내의 this 예약어를 Student 생성자 함수 내부의 this 예약어로 지정한다.
  Person.call(thisname, birthday);
  // this.name = name;
  // this.birthday = birthday;
  this.course = course;
};
 
// Student.prototype의 __proto__ 속성을 Person.prototype으로 설정한다.
// Student.prototype은 Person.prototype을 상속한다.
// 상속은 Student.prototype에 메서드를 추가하기 전에 해야하는데 Object.create()가 빈 객체를 반환하기 때문이다.
// 즉, Student.prototype은 비어 있다.
// 프로토타입 연결
Student.prototype = Object.create(Person.prototype);
 
// Student의 prototype 속성과 Person의 prototype 속성이 정확히 동일한 객체가 된다.
// 이는 본래의 목적인 Person의 prototype 속성을 Student의 prototype의 프로토타입으로 사용하는 것이 아니다.
// 즉, 상속이 원래 목적이며 정확히 같은 객체일 필요는 없다.
// Student.prototype = Person.prototype;
 
Student.prototype.introduce = function () {
  console.log(`저는 ${this.name}이고 ${this.course}을 전공하고 있습니다.`);
};
 
const jihoon = new Student("김지훈"1996"계산과학");
jihoon.introduce();
// 객체의 프로토타입에 없는 메서드에 접근하려고 하면 프로토타입 체인을 따라 올라가서 해당 메서드를 부모 프로토타입에서 찾을 수 있는지 확인한다.
jihoon.calcAge();
 
console.log(jihoon instanceof Student); // true
console.log(jihoon instanceof Person); // true
console.log(jihoon instanceof Object); // true
 
Student.prototype.constructor = Student; // 명시적으로 constructor 속성을 Student으로 설정한다.
console.dir(Student.prototype.constructor); // 위 코드가 없을 경우 Person, Object.create()를 사용해서 Person이라고 명시해서 이러한 오류가 발생한다.
cs


Inheritance - ES6 Class

클래스는 생성자 함수의 세부 사항을 숨기는 추상화 계층이기 때문에 생성자 함수 대신 ES6 클래스를 사용해서 상속을 구현하는 것은 기본적으로 같다. ES6 클래스 간 상속을 구현하는 데 필요한 것은 extends 예약어와 super() 함수이다. extends 예약어는 수동으로 신경 쓰지 않아도 내부적으로 프로토타입을 연결한다. super() 함수는 부모 클래스의 생성자 함수와 기본적으로 비슷하다. 생성자 함수에서 발생하는 모든 것이 자동으로 발생한다. 생성자 함수처럼 부모 클래스의 이름을 지정할 필요가 없는 이유는 extends 예약어에서 처리되었기 때문이다. super 함수는 자식 클래스의 생성자 부분에서는 항상 먼저 호출되어야 하는데 super() 함수 호출은 자식 클래스에서 this 예약어를 생성하는 데 책임이 있기 때문이다. super() 함수를 호출하지 않으면 this 예약어에 접근할 수 없다. 만약 추가적인 속성이 없을 경우 자식 클래스는 단순히 새로운 메서드를 가지고 있고 부모 클래스와 모든 속성을 공유한다. 또한, 생성자 함수가 아예 필요하지 않을 경우 super() 함수가 이 생성자로 전달된 모든 인자와 함께 자동으로 호출된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class Person {
  constructor(name, birthday) {
    this.name = name;
    this.birthday = birthday;
  }
 
  calcAge() {
    console.log(2023 - this.birthday);
  }
 
  sayHi() {
    console.log(`안녕하세요! ${this.name}입니다~`);
  }
 
  get age() {
    return 2023 - this.birthday;
  }
 
  set name(name) {
    if (name.length === 3this._name = name;
    else console.error(`성과 이름을 포함하세요`);
  }
 
  get name() {
    return this._name;
  }
}
 
class Student extends Person {
  constructor(name, birthday, course) {
    // this 예약어 사용 전 항상 먼저 호출해야 한다!
    super(name, birthday);
    this.course = course;
  }
 
  introduce() {
    console.log(`저는 ${this.name}이고 ${this.course}을 전공하고 있습니다.`);
  }
 
  // 새로운 calcAge()가 이미 프로토타입 체인에 있는 것을 덮어씌운다.
  // 즉, 새로운 calcAge()가 프로토타입 체인에서 가장 먼저 나타나기 때문에 부모 클래스의 calcAge()를 덮어씌운다.
  calcAge() {
    console.log(`${2023 - this.birthday}살 입니다.`);
  }
}
 
const daeik = new Student("김대익"1996"경제학");
daeik.introduce();
daeik.calcAge();
cs


Inheritance - Object.create()

Object.create() 메서드를 사용하여 프로토타입 체인을 구현하는 방법은 클래스와 생성자 함수로 구현과 유사하다. 부모 프로토타입 객체를 직접 상속받는 자식 프로토타입 객체를 Object.create() 메서드로 생성한다. Object.create() 메서드를 사용하면 생성자, 프로토타입(prototype) 속성 그리고 new() 연산자에 신경을 쓸 필요가 없다. 생성자 함수나 ES6 클래스를 사용하는 것은 Java나 C++과 같은 다른 언어에서 클래스가 존재하는 방식을 모방하는 방법이기에 JavaScript에서 클래스를 모방하는 것보다 훨씬 나은 패턴이라고 여기는 사람도 있다. 다시말해, Object.create()는 단순히 객체들을 연결하는 것이다. 하지만, 실제로는 ES6 클래스와 생성자 함수가 훨씬 더 많이 사용된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const PersonPrototype = {
  calcAge() {
    console.log(2023 - this.birthday);
  },
 
  init(name, birthday) {
    this.name = name;
    this.birthday = birthday;
  },
};
 
const sunshim = Object.create(PersonPrototype);
 
const StudentPrototype = Object.create(PersonPrototype);
// 생성자 함수에서 사용한 방법을 사용해서 init() 메서드를 다시 쓰지 않아도 된다.
// 기본적으로 자식 프로토타입인 학생 프로토타입은 부모 프로토타입인 사람 프로토타입에서 init() 메서드를 재사용할 수 있다.
StudentPrototype.init = function (name, birthday, course) {
  PersonPrototype.init.call(thisname, birthday);
  this.course = course;
};
 
StudentPrototype.introduce = function () {
  console.log(`저는 ${this.name}이고 ${this.course}을 전공하고 있습니다.`);
};
 
// StudentPrototype 객체는 yongjun 객체의 프로토타입이다.
// 즉, StudentPrototype 객체는 yongjun의 프로토타입(__proto__)이며 PersonPrototype 객체는 StudentPrototype의 프로토타입(__proto__)이므로 PersonPrototype는 yongjun 객체의 부모 프로토타입이다.
// 이는 PersonPrototype이 yongjun의 프로토타입 체인에 존재한다는 의미이다.
const yongjun = Object.create(StudentPrototype);
yongjun.init("이용준"1996"문학비평");
yongjun.introduce();
yongjun.calcAge();
cs


Encapsulation

캡슐화는 기본적으로 클래스 내부의 일부 속성과 메서드를 외부에서 접근할 수 없도록 유지하여 비공개로 유지하는 것을 의미한다. 나머지 메서드는 기본적으로 공개 인터페이스로 노출되며 이를 API라고 할 수 있다. 캡슐화와 데이터 개인화(privacy)가 필요한 두 가지 큰 이유가 있다. 첫째로 클래스 외부의 코드가 클래스 내부의 데이터를 실수로 조작하는 것을 방지하는 것이다.이는 또한 공개 인터페이스를 구현하는 이유이다. 속성을 수동으로 변경해서는 안 되고 따라서 이를 캡슐화해야 한다. 다음으로 공개 메서드만 노출시키면 내부 메서드를 쉽게 변경할 수 있다. 외부 코드가 내부 메서드에 의존하지 않기에 내부 메서드를 변경할 때 코드가 고장나지 않는다. 접근 제한자가 추가되기 전에 보호되어야 하는 속성의 이름에 밑줄(_)을 사용해서 이를 표시했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Account {
  constructor(owner, currency, accountNumber) {
    this.owner = owner;
    this.currency = currency;
    // 보호되어야 하는 속성
    // 이름 앞에 밑줄(_)을 추가한다.
    // 실제로 속성을 완전히 비공개로 만드는 것은 아니며 단지 규칙일 뿐이다.
    this._transactions = [];
    this._accountNumber = accountNumber;
 
    console.log(`${this.owner} 고객님, 계좌를 개설해 주셔서 감사합니다.`);
  }
 
  // 공개 인터페이스
  getTransactions() {
    return this._transactions;
  }
 
  deposit(value) {
    this._transactions.push(value);
  }
 
  withdraw(value) {
    this.deposit(-value);
  }
}
 
const account = new Account("네오""KRW"32390231);
 
// 밑줄은 관습이라서 여전히 보호받는 속성에 접근할 수 있다.
// account._transactions;
account.deposit(1000);
account.withdraw(200);
console.log(account.getTransactions());
cs

비공개 클래스 필드와 비공개 메서드는 클래스 필드라고 불리는 JavaScript 클래스를 개선하고 변경하기 위한 더 큰 제안의 일부이다. 제안을 클래스 필드라고 부르는 이유는 Java 혹은 C++ 같은 전통적인 객체지향 언어에서는 속성을 보통 필드라고 하는데 새로운 제안으로 인해 JavaScript 클래스가 단순히 생성자 함수 위의 구문 설탕이라는 개념에서 벗어난다. 왜냐하면 새로운 클래스 기능으로 클래스는 이전에 생성자 함수로는 갖지 못했던 능력을 갖기 때문이다. 필드는 모든 인스턴스에 있는 속성이며 프로토타입에 존재하지 않는다. 또한, 생성자에서 필드를 정의할 수는 없으며 필드는 모든 메서드 외부에 있어야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class Account {
  // 필드는 constructor() 함수에 존재하는 것과 똑같으며 따라서 this 예약어로 참조할 수 있다.
 
  // 공개 필드 (모든 인스턴스)
  _fee = 3;
 
  // 비공개 필드 (모든 인스턴스)
  // # 기호를 사용해서 필드를 비공개로 설정한다.
  #transactions = [];
 
  // pin 값은 undefined로 설정된다.
  // 생성자에서 값을 다시 정의한다.
  #pin;
 
  constructor(owner, currency, accountNumber, pin) {
    this.owner = owner;
    this.currency = currency;
    this._accountNumber = accountNumber;
    // this._transactions = [];
    // this._fee = 3;
    // this._pin = pin;
 
    this.#pin = pin;
 
    console.log(`${this.owner} 고객님, 계좌를 개설해 주셔서 감사합니다.`);
  }
 
  // 공개 메서드
  // 공개 인터페이스
  // 모든 메서드들은 항상 프로토타입에 추가된다.
  getTransactions() {
    return this.#transactions;
  }
 
  deposit(value) {
    this.#transactions.push(value);
  }
 
  withdraw(value) {
    this.deposit(-value);
  }
 
  showFee(value) {
    console.log(`수수료는 ${this.#computeFee(value)}원 입니다.`);
  }
 
  // 비공개 메서드
  // 구현 세부사항을 숨긴다.
  #computeFee(value) {
    return (this._fee * value) / 100;
  }
}
 
const account = new Account("네오""KRW"32390231);
 
account.deposit(1000);
account.withdraw(200);
console.log(account.getTransactions());
 
// transactions 필드가 비공개라서 오류가 발생한다.
// console.log(account.#transactions);
// compueteFee() 메서드가 비공개라서 오류가 발생한다.
// console.log(account.#compueteFee(500));
 
account.showFee(500);
cs


체이닝

클래스의 메서드에 연쇄 기능을 구현하는 방법은 메서드의 끝에 객체 자체를 반환한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Account {
  _fee = 3;
 
  #transactions = [];
  #pin;
 
  constructor(owner, currency, accountNumber, pin) {
    this.owner = owner;
    this.currency = currency;
    this._accountNumber = accountNumber;
    this.#pin = pin;
 
    console.log(`${this.owner} 고객님, 계좌를 개설해 주셔서 감사합니다.`);
  }
 
  getTransactions() {
    return this.#transactions;
  }
 
  deposit(value) {
    this.#transactions.push(value);
 
    return this;
  }
 
  withdraw(value) {
    this.deposit(-value);
 
    return this;
  }
 
  ...
}
 
const account = new Account("네오""KRW"32390231);
 
account.deposit(1000).deposit(2000).withdraw(500);
console.log(account.getTransactions()); // [ 1000, 2000, -500 ]
cs

update: 2023.12.22

댓글 없음:

댓글 쓰기