객체 지향 프로그래밍은 실세계에 존재하는 객체(Object)
를 소프트웨어의 세계에서 표현하기 위해, 객체의 핵심적인 개념 또는 기능만을 추출하는 추상화(abstraction)
를 통해 모델링하려는 프로그래밍 패러다임을 의미한다.
다시 말해, 프로그램을 여러 개의 독립된 단위, 즉 "객체" 들의 관계성있는 집합이라는 관점으로 접근하는 방식으로 볼 수 있다.
클래스(Class)
- 같은 종류의 집단이 가지는
속성
과 행위인메서드
를 정의한 것이다.- 객체 지향 프로그램의 기본적인 사용자 정의 데이터형(추상 자료형)이라고 할 수 있다.
- 객체 생성에 사용되는 청사진일뿐이며, 실질적으로 사용되는 객체를 생성하기위해서는
new
연산자를 통한 인스턴스화 과정이 필요하다.객체(Object)
- 클래스의 인스턴스이다.
- 객체는 자신 고유의 속성을 가지며, 클래스에서 정의한 메서드를 수행할 수 있다.
- 모든 인스턴스는 클래스에서 정의된 범위 내에서만 작동하며, 런타임에 그 구조를 변경할 수 없다.
외부로 노출해야 하는 값과 내부에서만 사용하는 값을 구분하여, 내부 데이터에 바로 접근을 하지 못하게 하고 필요한 메서드만 열어두는 특성을 말한다.
속성과 메서드를 클래스와 같은 하나의 틀 안에 담고 외부에 공개될 필요가 없는 정보는 private 변수 등으로 숨길 수 있다.
이러한 방법은 객체 외부에는 제한된 접근 권한을 제공하여 내부를 보호해서 객체의 안정성을 높이고, 필요한 메서드만 열어둠으로써 객체의 재사용성을 높일 수 있다.
// class
class Character {
name = "Yong"
#hp = 300
#mp = 500
attck() {...}
useSkill() {... this.#mp -= 50; ... }
moveTo(toX, toY) {...}
}
// object
var character = new Character();
character.name = "Yong" // public한 필드는 외부에서 수정이 가능하여 문제를 일으킬 수 있다.
// private을 이용하면 mp를 외부에서 함부로(?) 수정할 수 없다.
character.mp = 3000 // Error TS18013: Property '#mp' is not accessible outside class 'Human' because it has a private identifier.
객체에 공통된 부분만 따로 만들고 그 코드를 같이 물려 받아서 같은 부분은 재사용하고, 나머지 다른 부분들은 각각 코딩하는 방식을 바로 상속이라 한다. 즉, 객체의 일부분을 재사용하는 방법이다.
예를 들어, 다음과 같은 저글링 클래스를 구현했다고 해보자.
// class
class Zergling {
name = "저글링"
hp = 35
die() {...}
attack() {...}
moveTo(toX, toY) {...}
}
이제 히드라 클래스를 만드려고하니 저글링 클래스와 동일한 로직이 많다는 것을 알게 되었다. 또, 모션이나 공격 방식 등 다르게 구현해야 하는 로직도 존재한다는 것을 알게 되었다.
class Hydra {
name = "히드라"
hp = 75
die() {...}
attack() {...}
moveTo(toX, toY) {...}
}
이처럼 한 객체에 공통된 부분만 따로 만들고 그 코드를 같이 상속 받아서 같은 부분은 재사용하고, 나머지 다른 부분들은 각각 코딩할 수 있다.
상속을 통해 새로운 클래스가 기존의 클래스의 속성과 메서드를 물려받을 수 있다.
이 때, 상속을 받는 새로운 클래스를 하위 클래스 혹은 자식 클래스라고 하고, 새로운 클래스가 상속하는 기존의 클래스를 상위 클래스 혹은 부모 클래스라고 한다.
상속은 코드 재사용의 관점에서 매우 유용하다. 새롭게 정의할 클래스가 기존에 있는 클래스와 유사하다면 상속을 통해서 다른 점만 구현하면 된다.
추상화는 불필요한 정보는 숨기고 중요한 정보만을 표현하는 것이다. 위 상속의 예시에서 살펴본 것처럼 공통적인 부분을 모아서 상위의 개념으로 새롭게 이름을 붙이는 것도 추상화의 예시라고 할 수 있다.
// 상속과 추상화 예시
class Unit {
name = ""
hp = 0
die() {...}
attack() {...}
moveTo(toX, toY) {...}
}
class Zergling extends Unit {
name = "저글링"
hp = 35
attack() {...}
}
class Hydralisk extends Unit {
name = "히드라리스크"
hp = 70
attack() {...}
}
추상화를 통해 정의된 자료형을 추상 자료형이라고 한다. 추상 자료형은 자료형의 자료 표현과 자료형의 연산을 캡슐화한 것으로 자료형의 정보를 은닉할 수 있다.
객체 지향 프로그래밍에서 일반적으로 추상 자료형을 클래스
, 추상 자료형의 인스턴스를 객체
, 추상 자료형에서 정의된 연산을 메소드
라고 한다.
프로그램의 각 요소들(상수, 변수, 식, 객체, 함수, 메서드 등)이 다양한 자료형(type)에 속할 수 있는 성질을 가리킨다.
쉽게 설명하면 저글링, 히드라, 오버로드 등이 함께 섞인 유닛들이 각자 이동하는 속도나 방법이 다르지만, 이들을 하나의 이동이 가능한 유닛으로 취급하여 같은 타입으로 취급할 수 있다.
이처럼 추상화된 Unit 이라는 타입이 저글링, 히드라, 오버로드 등 하위 타입인 여러 가지 타입으로 참조할 수 있다는 개념이 바로 다형성이다.
let zergling1 = new Zergling()
let zergling2 = new Zergling()
let hydralisk1 = new Hydralisk()
let hydralisk2 = new Hydralisk()
let mutalisk = new Mutalisk()
let overload = new Overload()
let units = [zergling1, zergling2, hydralisk1, hydralisk2, mutalisk, overload]
// 모두 같은 이름의 moveTo 메소드를 호출하지만 각자의 방식대로 동작한다.
units.forEach(unit => unit.moveTo(300, 400))
객체 지향 프로그래밍에서는 다음과 같은 방식으로 다형성을 구현할 수 있다.
오버라이딩(overriding)
: 같은 이름의 메서드가 여러 클래스에서 다른 기능을 하는 것을 뜻한다. 즉, 부모 클래스로부터 상속받은 메서드를 자식 클래스에서 재정의하는 것을 뜻한다.
오버로딩(overloading)
: 하나의 클래스 내에서 같은 이름의 메서드가 매개변수의 개수나 자료형에 따라서 다른 기능을 하는 것을 뜻한다.
자바스크립트는 멀티 패러다임 언어로 함수형, 프로토타입 기반 객체 지향 언어이다. 그 중 프로토타입 기반 객체 지향 언어의 특징을 살펴보자. 프로토타입 객체는 메소드와 속성들을 상속받기 위한 템플릿 객체로, 모든 객체들이 가지고 있다.
자바스크립트는 클래스가 없는 프로토타입 기반 객체 지향 언어이다. 이 때문에 별도의 객체 생성 방법이 존재한다.
객체 리터럴
Object() 생성자 함수
생성자 함수
// 객체 리터럴
let obj1 = {};
obj1.name = "Yong";
// Object() 생성자 함수
let obj2 = new Object();
obj2.name = "Yong";
// 생성자 함수
function F() {}
let obj3 = new F();
obj3.name = "Yong";
자바스크립트는 클래스 기반 객체 지향 언어와 다르게, 이미 생성된 객체의 자료구조와 기능을 동적으로 변경할 수 있다는 특징이 있다. 즉, 자바스크립트는 동적인 언어이다.
프로토타입 기반 객체 지향에서 객체 지향의 상속, 캡슐화 등의 개념은 프로토타입 체인과 클로저 등으로 구현할 수 있다.
참고로, ES6에서 새롭게 Class가 도입되었다. 다만, 자바스크립트의 새로운 객체지향 모델을 제공하는 것은 아니다. 새로 도입된 Class도 사실 함수이고, 기존 프로토타입 기반 패턴의 Syntatic sugar이다.
본래는 클래스 기반 언어에서는 class
와 new
연산자를 통해서 객체를 생성하지만, 자바스크립트는 클래스 대신 생성자 함수를 사용해서 객체를 생성할 수 있다. 이 때, 생성자 함수는 클래스이자 생성자의 역할을 한다.
// 생성자 함수
function Person(name) {
this.name = name;
this.setName = function (name) {
this.name = name;
};
this.getName = function () {
return this.name;
};
}
// 객체 생성
let me = new Person("Yong");
// 객체의 메서드 호출
me.getName(); // Yong
위와 같이 생성자 함수 방식으로 생성한 객체 자체는 문제가 없지만, 여러 개의 객체를 생성한다면 문제가 발생할 수 있다. 아래 예시를 보면 각각의 객체마다 중복되어 생성된 메서드를 볼 수 있다. 즉, 각 객체가 동일한 메서드를 각자 소유하게 된다. 이는 메모리 낭비 문제로 귀결될 수 있다.
let me = new Person('Yong');
let you = new Person('Kim');
let him = new Person('Choi');
console.log(me); // Person { name: 'Yong', setName: [Function], getName: [Function] }
console.log(you); // Person { name: 'Kim', setName: [Function], getName: [Function] }
console.log(him); // Person { name: 'Choi', setName: [Function], getName: [Function] }
이같은 문제를 해결하기 위해서는 자바스크립트의 특징인 프로토타입을 사용할 수 있다.
자바스크립트의 모든 객체는 프로토타입이라고 하는 객체를 가리키는 내부적인 링크를 가지고 있다. 즉, 프로토타입을 통해 객체들을 연결할 수 있는데, 이를 프로토타입 체인
이라고 한다.
생성자 함수 내부에 작성했던 메서드를 생성자 함수의 prototype 프로퍼티
가 가리키는 프로토타입 객체로 이동시키면, 생성자 함수에 의해 생성된 모든 객체는 프로토타입 체인을 통해 프로토타입 객체의 메서드를 참조할 수 있다.
function Person(name) {
this.name = name;
}
// 프로토타입 객체에 메소드 정의
Person.prototype.setName = function (name) {
this.name = name;
};
// 프로토타입 객체에 메소드 정의
Person.prototype.getName = function () {
return this.name;
};
let me = new Person('Yong');
let you = new Person('Kim');
let him = new Person('choi');
console.log(Person.prototype);
// Person { setName: [Function], getName: [Function] }
console.log(me); // Person { name: 'Yong' }
console.log(you); // Person { name: 'Kim' }
console.log(him); // Person { name: 'choi' }
me.getName(); // Yong
자바스크립트는 클래스가 없기 때문에 일반적으로는 클래스의 변수나 메서드들을 물려받을 수 없다.
대신 프로토타입 객체를 통해 객체들을 연결한다. 즉, 프로토타입을 통해 객체가 다른 객체로 직접 상속된다. 현재 객체에 원하는 속성이 없으면 프로토타입 객체에서 찾는 방식으로 상속을 구현하고, 프로토타입 객체에도 있고 현재 객체에도 있는 속성은 현재 객체에 있는걸 사용해서 다형성을 구현한다.
다시 말하면 프로토타입 객체가 부모의 역할을 하고, 이들로부터 물려받는 객체가 자식의 역할을 한다.
var animal = {
name: "",
age: 0
sleep: function() { console.log("sleep... zzz", this.name) }
speak: function() { console.log("...", this.name) }
}
var cat = {
name: "냥이",
age: 1
speak: function() { console.log("야옹~~~", this.name, this.age) }
}
// 상속을 받는(척 하지만 prototype 연결)
cat.__proto__ = animal
cat.sleep() // cat에는 sleep없으니 연결된 animal를 찾아가서 sleep을 호출.. 이걸로 마치 상속 해결!
cat.speak() // cat에 있는 속성이니 본인의 speak호출! 이걸로 다형성 해결!
자바스크립트의 상속 구현 방식은 크게 두 가지이다.
의사 클래스 상속(Pseudo-classical Inheritance)
프로토타입 패턴 상속(Prototypal Inheritance)
의사 클래스 상속은 자식 생성자 함수의 prototype 프로퍼티
를 부모 생성자 함수의 인스턴스로 교체하여 상속을 구현하는 방식이다. 다시 말해, 자식 생성자 함수의 프로토타입 객체를 부모 생성자 함수의 인스턴스로 교체한다. 다만 의사 클래스 상속 방식은 여러 단점을 가지고 있기 때문에 프로토타입 패턴 상속만 알아보겠다.
프로토타입 패턴 상속은 Object.create
함수를 사용하여 객체에서 다른 객체로 직접 상속을 구현하는 방식이다. Object.create
함수는 매개변수에 프로토타입으로 설정할 객체를 전달하고, 이를 상속하는 새로운 객체를 생성한다.
// 부모 생성자 함수
let Parent = (function () {
// Constructor
function Parent(name) {
this.name = name;
}
// method
Parent.prototype.sayHi = function () {
console.log('Hi! ' + this.name);
};
// return constructor
return Parent;
}());
// create 함수의 인수는 프로토타입이다.
let child = Object.create(Parent.prototype);
child.name = 'child';
child.sayHi(); // Hi! child
console.log(child instanceof Parent); // true
자바스크립트는 public
또는 private
등의 키워드를 제공하지 않지만, 정보 은닉이 불가능한 것은 아니다.
let Person = function(arg) {
const name = arg ? arg : ''; // ①
this.getName = function() {
return name;
};
this.setName = function(arg) {
name = arg;
};
}
let me = new Person('Yong');
let name = me.getName();
console.log(name); // Yong
me.setName('Kim');
name = me.getName();
console.log(name); // Kim
위 예시를 살펴 보면, 함수 레벨의 스코프를 제공하므로 함수 내부의 변수는 외부에서 참조할 수 없다. 즉, 위 Person 생성자 함수 내부에서 선언된 name은 private 변수가 된다.
private 변수는 외부에서 직접 접근할 수 없고, 위처럼 생성자 함수 내에 생성된 getName 메서드 같은 클로저를 통해 접근할 수 있다. 이러한 방식이 기본적인 자바스크립트의 캡슐화이다.
위 예시를 조금 더 정리해보자. person 함수는 객체를 반환하고, 이 객체 내의 메서드는 클로저로서 private 변수에 접근할 수 있다. 이러한 방식을 모듈 패턴이라고 하며 캡슐화와 정보 은닉을 제공한다.
let person = function(arg) {
let name = arg ? arg : '';
return {
getName: function() {
return name;
},
setName: function(arg) {
name = arg;
}
}
}
let me = person('Yong'); /* or let me = new person('Yong'); */
let name = me.getName();
console.log(name);
me.setName('Kim');
name = me.getName();
console.log(name);
다만 이러한 모듈 패턴은 다음과 같은 주의할 점이 있다.
let Person = function() {
let name;
let F = function(arg) { name = arg ? arg : ''; };
F.prototype = {
getName: function() {
return name;
},
setName: function(arg) {
name = arg;
}
};
return F;
}();
let me = new Person('Yong');
console.log(Person.prototype === me.__proto__);
console.log(me.getName());
me.setName('Kim')
console.log(me.getName());
캡슐화를 구현하는 패턴은 다양하며 각각의 패턴에는 장단점이 있다. 다양한 패턴의 장단점을 분석하고 파악하는 것이 보다 효율적인 코드를 작성하는데 중요하다.
자바스크립트는 멀티 패러다임 언어이다. 객체지향을 배제할 필요도, 맹목적으로 객체지향스럽게 설계해야 할 필요도 없다. 객체지향의 개념으로 데이터와 메서드를 가지는 객체를 통해 독립적이고 작은 모듈로 만들어 편리하게 재사용하는 장점을 취하면서, 객체들의 결합이 높아져 프로그램이 복잡해질 때는 함수형으로 만들어서 사용하면 된다. 결국 자바스크립트는 자바스크립트스럽게 사용하면 된다.
https://ko.wikipedia.org/wiki/%EA%B0%9D%EC%B2%B4_%EC%A7%80%ED%96%A5_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D
https://poiemaweb.com/js-object-oriented-programming
https://yozm.wishket.com/magazine/detail/1396/