객체 지향 프로그래밍은 많은 프로그래밍 언어의 기본이 되는 프로그래밍 설계 패러다임(혹은 패턴) 중 하나이다.
OOP는 함수와 로직이 아닌 클래스와 객체를 기반으로 하는 프로그래밍 패러다임이다.
OOP를 정의하는 네 가지 핵심 개념은 다음과 같다.
그리고 기본적으로 Object와 Classes and instances 와 같은 개념들을 이해해야 한다.
상위 개념을 추적해나가면서 설계하는 게 OOP의 기초.
템플릿이 되는 개념을 클래스(class)라고 부르며, 계속해서 상위 개념을 만들어나가는 과정 자체를 추상화(Abstraction/공통되는 특성을 묶어 이름 붙이는 것)이라고 한다.
템플릿을 통해 찍어낸 인스턴스(instance)들을 객체(object)라고 한다. 즉, 클래스는 일종의 설계도이고 이걸 사용해서 실제 물건을 만들어낼 수 있는데, 이렇게 만들어낸 실제 물건이 객체(object)인 것이다.
객체 지향이라는 의미는, 클래스를 사용해서 추상적인 개념을 정의하고 그 클래스를 사용해서 실제 사용할 객체를 만들어냄으로써 현실 세계의 모든 것을 표현할 수 있다는 아이디어.
Object는 속성(프로퍼티)과 메서드를 포함하는 고유한 entity이다. 예를 들어, 실생활에서 자동차는 색상, 종류, 모델, 마력 등 몇 가지 특성을 가지며 운전과 같은 특정 작업을 수행하는 Object이다.
OOP에서 객체의 특성을 프로퍼티, 동작을 메서드라고 한다.
Object는 class의 instance이다. 자바스크립트에서 Object는 모든 곳에 존재한다. function, array, or string 등 거의 모든 게 Object이다.
자바스크립트에서 Object를 생성하는 방법은 2가지이다.
//Defining object
let person = {
first_name:'Mukul',
last_name: 'Latiyan',
//method
getFunction : function(){
return (`The name of the person is
${person.first_name} ${person.last_name}`)
},
//object within object
phone_number : {
mobile:'12345',
landline:'6789'
}
}
console.log(person.getFunction());
console.log(person.phone_number.landline);
//using a constructor
function person(first_name,last_name){
this.first_name = first_name;
this.last_name = last_name;
}
//creating new instances of person object
let person1 = new person('Mukul','Latiyan');
let person2 = new person('Rahul','Avasthi');
console.log(person1.first_name);
console.log(`${person2.first_name} ${person2.last_name}`);
메서드를 사용해서도 새로운 Object를 만들 수 있다.
Object.create()
메서드를 사용하면 기존 객체를 새로운 객체의 프로토타입으로 사용해서 새로운 객체를 만들 수 있다.
// Object.create() example a
// simple object with some properties
const coder = {
isStudying : false,
printIntroduction : function(){
console.log(`My name is ${this.name}. Am I
studying?: ${this.isStudying}.`)
}
}
// Object.create() method
const me = Object.create(coder);
// "name" is a property set on "me", but not on "coder"
me.name = 'Mukul';
// Inherited properties can be overwritten
me.isStudying = true;
me.printIntroduction();
클래스는 Object의 청사진이다. 클래스는 일종의 template이고, Object는 클래스의 인스턴스(instance)이므로 클래스는 많은 Object를 가질 수 있다.
클래스는 변수(인스턴스 변수라고 한다)와 메서드로 구성된다. 클래스에서 인스턴스화된 모든 객체는 해당 객체를 인스턴스화한 클래스의 모든 속성을 갖는다.
다른 객체 지향 언어와 달리 자바스크립트에는 클래스가 없고 객체Object만 존재한다. 더 정확히 말하면 자바스크립트는 프로토타입 기반 객체 지향 언어이다. 즉, 클래스가 없고 생성자(constructor) 함수를 사용해 동작을 정의한 다음 프로토타입을 사용해서 재사용한다.
ECMAScript2015에 도입된 자바스크립트의 class도 기존 프토토타입 기반의 객체 지향 상속 모델을 쉽게 사용하게 해주는 구문이다. 즉, 자바스크립트에서의 class도 결국은 객체(Object)이다.
Classes
new 키워드를 사용해서 인스턴스화할 수 있다.
클래스는 OOP에서 코드 중복을 방지하고 새 객체를 생성 및 관리하며 상속을 지원하는 데 사용된다.
// Defining class using es6
class Vehicle {
constructor(name, maker, engine) {
this.name = name;
this.maker = maker;
this.engine = engine;
}
getDetails(){
return (`The name of the bike is ${this.name}.`)
}
}
// Making object with the help of the constructor
let bike1 = new Vehicle('Hayabusa', 'Suzuki', '1340cc');
let bike2 = new Vehicle('Ninja', 'Kawasaki', '998cc');
console.log(bike1.name); // Hayabusa
console.log(bike2.maker); // Kawasaki
console.log(bike1.getDetails());
전통적인 방법
// Defining class in a Traditional Way.
function Vehicle(name,maker,engine){
this.name = name,
this.maker = maker,
this.engine = engine
};
Vehicle.prototype.getDetails = function(){
console.log('The name of the bike is '+ this.name);
}
let bike1 = new Vehicle('Hayabusa','Suzuki','1340cc');
let bike2 = new Vehicle('Ninja','Kawasaki','998cc');
console.log(bike1.name);
console.log(bike2.maker);
console.log(bike1.getDetails());
이제부터 OOP의 핵심 개념들을 typescript와 비교해보며 이해할 것이다. typescript는 javascript에서 객체 지향 프로그래밍을 구현하는 데 상당한 영향을 끼쳤다.
OOP의 첫번째 개념은 추상화이다. OOP의 추상화는 클래스 사용자에게 필요한 정보만 노출하는 것을 말한다. TS에서 추상화를 구현하기 위해서는, abstract class/method, interfaces, and types 등 다양한 방법이 존재한다.
abstract class Character {
public name: string
public damage: number
public attackSpeed: number
constructor(name: string, damage: number, speed: number){
this.name = name
this.damage = damage
this.attackSpeed = speed
}
public abstract damagePerSecond(): number
}
class Goblin extends Character {
constructor(name: string, damage: number, speed: number){
super(name, damage, speed)
}
public damagePerSecond(): number {
return this.damage * this.attackSpeed
}
}
위 코드에서 추상 클래스는 Character이며, Goblin 클래스는 추상 클래스인 Character 클래스에 의존하고 있다.
ts에서 이렇듯 추상 클래스나 추상 메서드를 구현할 때 앞에 abstract 키워드를 붙인다.
추상 메서드를 보면 정의만 존재할 뿐, body가 구현되어 있지 않은 걸 볼 수 있따. 추상 클래스를 상속하는 클래스에서는 반드시 추상 메서드를 구현해야 한다. 물론 추상 메서드가 아니라 실제로 기능이 담긴 메서드도 구현할 수는 있다.
다만 추상 클래스는 말 그대로 클래스와 달리 인스턴스를 생성하지 않는다.
OOP의 두번째 개념은 캡슐화이다. 캡슐화는 데이터를 숨긴다는 아이디어를 기반으로 한다. 특정 프로퍼티 혹은 메서드에 대한 엑세스를 제한한다.
OOP에서 캡슐화는 특정 속성에 대한 무단 엑세스 및 변형을 제한하는 걸 의미한다.
ts에서는 기본적으로 3가지 access modifier가 존재한다.
public
- 어디서나 자유롭게 접근 가능, 클래스 바디에서 생략 가능protected
- 나와 파생된 후손 클래스 내에서 접근 가능 (클래스 바깥에서 사용할 수 없음)private
- 내 클래스에서만 접근 가능이 외에도 ts에는 2개의 access modifier가 존재한다.
static
: static(정적) 키워드가 붙은 속성과 메서드는 클래스에서 인스턴스화된 객체가 아닌 클래스 내에서만 직접 엑세스할 수 있다. 이들은 상속받을 수도 없다. readonly
: 수정할 수 없는 속성이 된다. 읽을 수만 있다. 수정할 수 없으므로 클래스 선언이나 생성자 함수 내부에서 설정해야 한다. class Character {
private _name: string;
constructor(name: string) {
this._name = name;
}
public get name(): string {
return this._name;
}
public set name(value: string) {
this._name = value;
}
}
위 코드에서 _name
프로퍼티는 비공개이다. 즉, 클래스 외부에서 이 프로퍼티에 엑세스할 수 없다. 이러한 private 프로퍼티에 엑세스하기 위해 소위 getter과 setter 메서드를 사용한다.
ts에서는 이렇게 private, public과 같은 Access Modifiers를 제공하지만 기존 js에서는 이들을 제공해주지 않는다. 그래서 js에서는 일반적으로 클래스/객체 내에서 변수를 특정하는 방법으로 구현하곤 했다. (명시적으로 _
와 같은 기호를 붙여서 구분한다든가..)
세부 구현 사항은 고려하지 않아도 되며, 객체의 인터페이스를 사용해 작업하면 된다. 때때로 캡슐화는 데이터 숨기기(hiding of data) 혹은 데이터 추상화(data Abstraction)를 의미하기도 한다.
대부분의 OOP 언어는 변수의 범위를 제한하는 access modifiers를 제공하지만, 자바스크립트는 따로 access modifiers가 있지는 않는다.
public, private, protected 등 정보 은닉 개념이다. 자바스크립트에서는 그냥 _
를 앞에 붙여서 구분하는 하거나 아래와 같이 단순 정의 방법으로 구현할 수 있다.
function Person(fname,lname){
let _firstName = fname
let _lastName = lname
constructor(fname, lname);{
this.fname = fname
this.lname = lname
}
let _getDetails_noaccess = function(){
return (`First name is: ${_firstName} Last
name is: ${_lastName}`)
}
this.getDetails_access = function(){
return (`First name is: ${_firstName}, Last
name is: ${_lastName}`)
}
}
let person1 = new Person('Mukul','Latiyan')
console.log(person1._firstName) // output 1
console.log(person1._getDetails_noaccess) // output 2
console.log(person1.getDetails_access()) // output 3
console.log(person1.fname) // output 4
ts에서는 생성자에서 인스턴스 변수를 초기화하는 간단한 방법을 제공한다.
class A {
constructor(public variable: string){}
}
const object = new A('value')
이 경우 인스턴스 변수의 개수가 늘어나면 지저분해질 수 있으므로 변수가 적을 때 사용하는 게 좋다.
속성에 이런 식으로 특정한 access modifiers를 설정해둘 때, 문제는 해당 속성은 접근할 필요가 있을 수 있다는 것이다.
그렇다고 이때마다 접근 제어자를 변경하는 것은 불편하다.
ts에서는 이때 getter, setter 메서드를 통해 접근 제어자로 인해 접근이 불가능한 속성도 접근할 수 있게 해준다.
setter는 인스턴스 변수의 값을 설정하는 클래스 내부의 메서드이다.
getter는 인스턴스 변수의 값을 반환하는 클래스 내부의 메서드이다.
setter 및 getter는 속성 읽기와 쓰기 사이에 일부 논리를 추가하기 위해 구현된다. 메서드이기 때문에 조건이 충족되어야 변형이 발생하거나 속성 값을 읽을 수 있다.
TypeScript에서 setter는 set 키워드를 사용하여 구현되고 getter는 get 키워드를 사용하여 구현된다.
js에서도 static 키워드를 사용할 수 있다.
그리고 ES2019 에서는 해쉬 # prefix 를 추가해 private class 필드를 선언할 수 있게 되었다.
Private class fields
class ClassWithPrivateField {
#privateField
}
class ClassWithPrivateMethod {
#privateMethod() {
return 'hello world'
}
}
class ClassWithPrivateStaticField {
static #PRIVATE_STATIC_FIELD
}
상속은 코드를 재사용할 수 있다는 의미를 갖는다. 기존 부모 클래스가 가지고 있는 기능을 새로운 클래스에서 구축하려 한다면, 이 새로운 클래스에서 상속이 발생하는 것이다.
이때 새로운 클래스는 하위 클래스, 기존 클래스는 상위 클래스로 보면 된다.
객체Object의 일부 속성(프로퍼티)와 메서드를 다른 객체에서 사용하는 개념이다. 클래스가 클래스를 상속하는 대부분의 OOP 언어와 달리, 자바스크립트는 객체Object를 상속한다. 즉, 한 객체의 특성(속성 및 메서드)를 다른 객체에서 재사용할 수 있다.
js에서도 extends, super 같은 예약어로 구현 가능하다.
class Character {
public name: string;
public damage: number;
constructor(name: string, damage: number) {
this.name = name;
this.damage = damage;
}
public talk(): void {
console.log('Says something ...');
}
}
class Orc extends Character {
public weapon: string;
constructor(name: string, damage: number, weapon: string) {
super(name, damage);
this.weapon = weapon;
}
public attack(): void {
console.log(`Attacks his target with his ${this.weapon}.`);
}
}
extends 키워드는 하위 클래스가 상속의 이점을 얻도록 하기 위해 상위 클래스의 속성과 메서드에 대한 엑세스를 제공한다.
반면 ts에서만 사용 가능한 implements 키워드는 상위 클래스(수퍼 클래스)를 인터페이스로 취급하여 하위 클래스가 해당 수퍼 클래스의 형태를 준수하도록 하는 기능을 갖는다.
class Human {
name: string;
gender: string;
constructor(name:string, gender:string){
this.name = name;
this.gender = gender;
}
speak() {
return `I am speaking`;
}
}
class Doctor implements Human {
name: string;
gender: string;
constructor(name:string, gender:string){
this.name = name;
this.gender = gender;
}
speak() {
return 'I am a doctor';
}
}
서브클래스가 슈퍼클래스에서 속성과 메서드를 상속하면 상속된 속성을 수정하거나 확장할 수 있다. 상속된 속성을 수정하는 이 프로세스를 오버라이딩(overriding
)이라고 한다.
동일한 메서드가 호출될 때, 하위클래스가 상위클래스와 다른 논리를 실행해야 하는 경우 overriding이 구현된다.
class A {
print() {
console.log('I am class A');
}
}
class B extends A {
print() {
console.log('I am class B');
}
}
다른 시나리오가 있을 수 있다.
예를 들어 기능을 완전히 재정의(overriding)하는 게 아니라 확장(extended)해야 할 수 있다.
이때는 먼저 super 키워드로 호출한 다음에 새 기능을 구현하면 된다.
class B extends A {
print() {
super.print(); //I am class A
console.log('I am class B');
}
}
const object = new B();
object.print();// I am class A, I am class B
++참고로 오버로딩과 오버라이딩은 다르다.
오버라이딩이 덮어 쓰는 거라면, 오버로딩은 형태는 같은데 여러 타입을 쓰고 싶을 때 사용하는 방법이다.
다중 상속(Multiple inheritance)은 하나 이상의 상위 클래스에서 상속되는 하위 클래스를 말한다.
예를 들어 아래와 같이 하나의 슈퍼 클래스에서 두 개의 클래스가 상속되고, 다른 클래스는 이전에 생성된 슈퍼 클래스 아래에 있는 자식 클래스에서 상속될 때 발생하는 문제이다.
class A {};
class B extends A {};
class C extends A {};
class D extends B, C {};// This will throw an error
이러한 다중 상속 문제를 해결하기 위해서는
클래스 대신 인터페이스를 사용하는 것이다.
클래스 대신 인터페이스를 사용하면,
하위 클래스가 상위 클래스를 확장하지 extends
하지 않고 implements
한다.
class A {};
interface B extends A {};
interface C extends A {};
class D implements B, C {};
다형성은 하나 이상의 형식으로 클래스를 만드는 기능이다. 즉, 클래스는 동일한 메서드를 지니지만, 구현을 다르게 할 수 있다.
java에서는 추상클래스(abstract class), 인터페이스(interface), overloading.
자바스크립트에서는 예를 들어, Animal 클래스를 선언하고 extends로 구체적인 동물들에게 상속하는 정도로 밖에 불가능하다. ts는 이 문법들을 js에 도입해서 OOP 구현을 도와준다.
OOP에서 다형성은 여러 클래스가 부모 클래스에 상속되고 특정 기능을 override(재정의)하는 상황을 나타낸다. 즉, 상속된 메서드나 속성을 재정의하는 것이 다형성이다.
class Character {
public name: string;
public damage: number;
constructor(name: string, damage: number) {
this.name = name;
this.damage = damage;
}
public talk(): void {
console.log('Says something ...');
}
public attack(): void {
console.log(`Attacks his target with his fists.`);
}
}
class Orc extends Character {
public weapon: string;
constructor(name: string, damage: number, weapon: string) {
super(name, damage);
this.weapon = weapon;
}
public talk(): void {
console.log('Says something but in orcish ...');
}
public attack(): void {
console.log(`Attacks his target with his ${this.weapon}.`);
}
}
위 코드를 보면, 자식 클래스인 Orc
가 부모 클래스의 프로퍼티와 메서드를 덮어 쓰는 걸 볼 수 있다. 이게 바로 다형성이다.
abstract Classes and Members
4 Pillars of Object-oriented Programming with TypeScript
Object-oriented programming
Introduction to Object Oriented Programming in JavaScript
알고 보면 재밌는 객체 지향 프로그래밍, OOP 흝어보기