우리가 객체를 만드는 과정은 현실 또는 가상의 존재를 프로그램 내에서 사용할 용도에 맞게 적절하게 설계하는 과정입니다. 이때 객체를 만들고 나면 그 객체를 사용하는 사람은 객체 내부에 존재하는 복잡한 원리를 모르더라도 객체 외부에 공개된 프로퍼티나 메소드만을 가지고도 객체를 문제없이 잘 사용할 수 있어야 합니다. 이를 위해서는 프로퍼티와 메소드의 이름을 누구나 이해하기 쉽게 잘 지어야하고, 필요한 경우 이렇게 주석을 달거나
class User {
constructor(email, birthdate) {
// 사용자의 이메일 주소
this.email = email;
// 사용자의 생일
this.birthdate = birthdate;
}
// 물건 구매하기
buy(item) {
console.log(`${this.email} buys ${item.name}`);
}
}
그 내용을 문서화하여 공개하기도 합니다. 이는 비단 하나의 객체 뿐만 아니라 여러 객체가 모인 라이브러리나 프레임워크의 경우에도 마찬가지입니다. 우리가 아주 세밀한 원리까지 속속들이 알고 있지 않은 유명한 라이브러리나 프레임워크를 문제없이 사용할 수 있는 것은 그것들이 적절하게 추상화되어 있기 때문입니다.
캡슐화는 객체 외부에서 함부로 접근하면 안되는 프로퍼티나 메소드에 직접 접근할 수 없도록 하고, 필요한 경우 공개된 다른 메소드를 통해서만 접근할 수 있도록 하는 것을 의미합니다. 아래의 코드를 보면
class User {
constructor(email, birthdate) {
this.email = email;
this.birthdate = birthdate;
}
buy(item) {
console.log(`${this.email} buys ${item.name}`);
}
get email() {
return this._email;
}
set email(address) {
if (address.includes('@')) {
this._email = address;
} else {
throw new Error('invalid email address');
}
}
}
사용자의 이메일 주소를 나타내는 프로퍼티는 사실 _email 이고, 그 getter/setter 메소드의 이름이 email입니다. 그래서 마치 email 프로퍼티에 접근하는 것 같은 코드를 작성하더라도
const user1 = new User('charlie123@google.com', '2000-12-05');
console.log(user1.email); // email이라는 getter 메소드 실행
user1.email = 'new123@google.com'; // email이라는 setter 메소드 실행
지금 주석에 적힌 것처럼 사실은 email이라는 getter 메소드 또는 setter 메소드가 실행되는 것이죠. 이렇게 코드를 작성하면 _email 프로퍼티가 보호받고 있는 프로퍼티라는 것을 알 수 있습니다. 하지만 이렇게 해도 완벽한 캡슐화는 아니라고 했었죠? 여전히
console.log(user1._email);
이런 식으로 보호받는 변수에 직접 접근할 수 있기 때문입니다.
사실 다른 언어에서는 해당 언어의 문법 차원에서(ex. Java에서 캡슐화하고 싶은 변수나 메소드 앞에 붙이는 private 키워드) 캡슐화를 지원하는 경우가 많지만 자바스크립트에는 그러한 문법이 없습니다. 하지만 클로저(Closure)라는 것을 사용해서 우회적으로 완벽한 캡슐화를 구현할 수는 있습니다.('캡슐화 더 알아보기' 노트) 어쨌든 중요한 것은 객체를 사용하는 입장에서는 사용하라고 공개된 것 이외에는 되도록 접근하지 말고, 객체를 만드는 입장에서도 미리 보호해야할 프로퍼티나 메소드를 캡슐화해두어야 한다는 점입니다.
상속은 부모 클래스의 프로퍼티와 메소드를 자식 클래스가 그대로 물려받는 것입니다.
class User {
constructor(email, birthdate) {
this.email = email;
this.birthdate = birthdate;
}
buy(item) {
console.log(`${this.email} buys ${item.name}`);
}
}
class PremiumUser extends User {
constructor(email, birthdate, level) {
super(email, birthdate);
this.level = level;
}
streamMusicForFree() {
console.log(`Free music streaming for ${this.email}`);
}
}
지금 이 코드에서는 PremiumUser 클래스가 User 클래스에 있는 email, birthdate 프로퍼티와 buy 메소드를 그대로 물려받고 있습니다. 이렇게 상속을 적용하면 똑같은 코드를 또다시 작성하지 않아도 됩니다. 즉, '코드의 재사용성(reusability)'이 좋아집니다. 만약 두 클래스에 개념적으로 포함되는 관계가 성립한다고 하면 상속을 적용해보는 것도 좋습니다.
필요한 경우에는 자식 클래스에서 부모 클래스와 동일한 이름의 메소드를 재정의(오버라이딩, overriding)할 수도 있는데요. 이 오버라이딩은 바로 다음에 나오는 '다형성'과 연관이 깊습니다.
다형성은 하나의 변수가 다양한 종류의 클래스로 만든 여러 객체를 가리킬 수 있음을 의미합니다.
class User {
constructor(email, birthdate) {
this.email = email;
this.birthdate = birthdate;
}
buy(item) {
console.log(`${this.email} buys ${item.name}`);
}
}
class PremiumUser extends User {
constructor(email, birthdate, level) {
super(email, birthdate);
this.level = level;
}
buy(item) {
console.log(`${this.email} buys ${item.name} with a 5% discount`);
}
streamMusicForFree() {
console.log(`Free music streaming for ${this.email}`);
}
}
const item = {
name: '스웨터',
price: 30000,
};
const user1 = new User('chris123@google.com', '19920321');
const user2 = new User('rachel@google.com', '19880516');
const user3 = new User('brian@google.com', '20051125');
const pUser1 = new PremiumUser('niceguy@google.com', '19891207', 3);
const pUser2 = new PremiumUser('helloMike@google.com', '19900915', 2);
const pUser3 = new PremiumUser('aliceKim@google.com', '20010722', 5);
const users = [user1, pUser1, user2, pUser2, user3, pUser3];
users.forEach((user) => {
user.buy(item);
});
이 코드를 보면 지금 forEach 문 안의 user는 User 클래스로 만든 객체를 가리킬 때도 있고, PremiumUser 클래스로 만든 객체를 가리킬 때도 있습니다. 매번 user 객체의 buy 메소드가 호출된다는 점은 같지만, 구체적으로 무슨 클래스로 만든 객체의 buy 메소드가 호출되느냐에 따라 결과가 달라지는데요. 이렇게 단순한 코드로 다양한 결과를 낼 수 있는 건 다형성 덕분인 겁니다.
자, 이때까지 객체 지향 프로그래밍의 4개의 기둥을 복습해보았는데요. 모두 잘 이해하셨나요? 이것들을 잘 이해해야 자바스크립트로 객체 지향 프로그래밍을 잘 할 수 있으니까 혹시 이해가 안 되는 부분이 있다면 해당 영상을 꼭 복습하세요.