Language/Javascript

[Javascript] 자바스크립트 객체 지향 프로그래밍 - 추상, 캡슐, 상속, 다형

재은초 2023. 7. 30. 22:46
반응형

객체지향 프로그래밍 (Object-Oriented Programming)

  • 객체지향 프로그래밍은 함수들의 집합 혹은 단순한 컴퓨터의 명령어들의 목록이라는 전통적인 절차지향 프로그래밍과는 다른, 관계성있는 객체들의 집합이라는 관점으로 접근하는 소프트웨어 디자인으로 볼 수 있다.
  • 각 객체는 메시지를 받을 수도 있고, 데이터를 처리할 수도 있으며, 또다른 객체에게 메시지를 전달할 수도 있다. 각 객체는 별도의 역할이나 책임을 갖는 작은 독립적인 기계 또는 부품으로 볼 수 있다.
  • 객체지향 프로그래밍은 보다 유연하고 유지보수하기 쉬우며 확장성 측면에서서도 유리한 프로그래밍을 하도록 의도되었고, 대규모 소프트웨어 개발에 널리 사용되고 있다.
  • 자바스크립트는 멀티-패러다임 언어로 명령형(imperative), 함수형(functional), 프로토타입 기반(prototype-based) 객체지향 언어다. 비록 다른 객체지향 언어들과의 차이점에 대한 논쟁들이 있긴 하지만, 자바스크립트는 강력한 객체지향 프로그래밍 능력들을 지니고 있다. 간혹 클래스가 없어서 객체지향이 아니라고 생각하는 사람들도 있으나 프로토타입 기반의 객체지향 언어다.

 

추상화(Abstraction)

  • 객체를 만들고 나면 그 객체를 사용하는 사람은 객체 내부에 존재하는 복잡한 원리를 모르더라도 객체 외부에 공개된 프로퍼티나 메소드만을 가지고도 객체를 문제없이 잘 사용할 수 있다. 이를 위해서는 프로퍼티와 메소드의 이름을 누구나 이해하기 쉽게 잘 지어야하고, 필요한 경우 이렇게 주석을 달거나 그 내용을 문서화하여 공개하기도 하는데 이를 추상화라고 한다.
class User {
  constructor(email, birthdate) {
    // 사용자의 이메일 주소
    this.email = email;
    // 사용자의 생일
    this.birthdate = birthdate;
  }

  // 물건 구매하기
  buy(item) {
    console.log(`${this.email} buys ${item.name}`);
  }
}

 

캡슐화(Encapsulation)

  • 캡슐화는 객체 외부에서 함부로 접근하면 안되는 프로퍼티나 메소드에 직접 접근할 수 없도록 하고, 필요한 경우 공개된 다른 메소드를 통해서만 접근할 수 있도록 하는 것을 의미한다.
  • 아래 코드에서 사용자의 이메일 주소를 나타내는 프로퍼티는 사실 _email 이고, getter/setter 메소드의 이름이 email 다. 그래서 마치 email 프로퍼티에 접근하는 것 같은 코드를 작성하더라도 지금 주석에 적힌 것처럼 사실은 email이라는 getter 메소드 또는 setter 메소드가 실행된다. 이렇게 코드를 작성하면 _email 프로퍼티가 보호받고 있는 프로퍼티라는 것을 알 수 있다.
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');
    }
  }
}
  • 하지만 아래와 같은 방법으로 보호받는 변수에 직접 접근할 수 있기 때문에 완벽한 캡슐화는 아니다. Java에서는 캡슐화하고 싶은 변수나 메소드 앞에 붙이는 private 키워드를 붙이면 되지만 자바스크립트에는 그러한 문법이 없다. 하지만 클로저(Closure)라는 것을 사용해서 우회적으로 완벽한 캡슐화를 구현할 수는 있다.
console.log(user1._email);
user1._email = 'chris robert';

클로저(Closure)

  • 클로저란 자바스크립트에서 어떤 함수와 그 함수가 참조할 수 있는 값들로 이루어진 환경을 하나로 묶은 것을 의미힌다. 즉, 함수가 정의된 당시에 참조할 수 있었던 변수들을 계속 참조할 수 있는 상태의 함수를 클로저라고 한다.
  • 예를 들어, 아래 createUser 함수가 실행되는 시점에 email이라는 getter/setter 메소드는 _email 이라는 변수의 값에 접근할 수 있는 상태입니다. 그리고 여기서 핵심은 이 email getter/setter 메소드들은 메소드를 갖고 있는 객체가 리턴된 이후더라도 여전히 _email에 접근하는 것이 가능하다는 점입니다. 보통 다른 프로그래밍 언어였다면 createUser 함수 내부가 실행될 때만 email getter/setter 메소드가 _email 변수에 접근할 수 있었겠지만, 자바스크립트에서는 클로저라는 개념으로 해당 환경을 함수와 함께 그대로 유지시켜준다.
function createUser(email, birthdate) {
  let _email = email;

  const user = {
    birthdate,

    get email() {
      return _email;
    },

    set email(address) {
      if (address.includes('@')) {
        _email = address;
      } else {
        throw new Error('invalid email address');
      }
    },
  };

  return user;
}

const user1 = createUser('chris123@google.com', '19920321');
console.log(user1.email);
  • 자신을 포함하고 있는 외부함수보다 내부함수가 더 오래 유지되는 경우, 외부 함수 밖에서 내부함수가 호출되더라도 외부함수의 지역 변수에 접근할 수 있는데 이러한 함수를 클로저(Closure)라고 부른다.
  • 즉, 클로저는 반환된 내부함수가 자신이 선언됐을 때의 환경(Lexical environment)인 스코프를 기억하여, 자신이 선언됐을 때의 스코프 밖에서 호출되어도 그 스코프에 접근할 수 있는 함수를 말한다.
  • 클로저에 의해 참조되는 외부함수의 변수 즉 outerFunc 함수의 변수 x를 자유변수(Free variable)라고 부른다. 클로저라는 이름은 자유변수에 함수가 닫혀있다(closed)라는 의미로 의역하면 자유변수에 엮여있는 함수라는 뜻이다.
function outerFunc() {
  var x = 10;
  var innerFunc = function () { console.log(x); };
  innerFunc();
}

// 이미 종료되어 실행에서 제거된 함수 outerFunc의 지역변수 x가 다시 부활이라도 한 듯이 동작
outerFunc(); // 10

클로저 사용의 목적

  • 상태유지와 전역 변수의 사용 억제: 클로저는 현재 상태를 기억하고 이 상태가 변경되어도 최신 상태를 유지해야 하는 상황에 매우 유용하다. 만약 자바스크립트에 클로저라는 기능이 없다면 상태를 유지하기 위해 전역 변수를 사용할 수 밖에 없다. 전역 변수는 언제든지 누구나 접근할 수 있고 변경할 수 있기 때문에 많은 부작용을 유발해 오류의 원인이 되므로 사용을 억제해야 한다.
  • 정보의 은닉: 아래에서 생성자 함수 Counter 내에서 선언된 변수 counter는 생성자 함수 Counter 외부에서 접근할 수 없다. 하지만 생성자 함수 Counter가 생성한 인스턴스의 메소드인 increase, decrease는 클로저이기 때문에 자신이 생성됐을 때의 렉시컬 환경인 생성자 함수 Counter의 변수 counter에 접근할 수 있다. 이러한 클로저의 특징을 사용해 클래스 기반 언어의 private 키워드를 흉내낼 수 있다.
function Counter() {
  // 카운트를 유지하기 위한 자유 변수
  var counter = 0;

  // 클로저
  this.increase = function () {
    return ++counter;
  };

  // 클로저
  this.decrease = function () {
    return --counter;
  };
}

const counter = new Counter();

console.log(counter.increase()); // 1
console.log(counter.decrease()); // 0

 

상속(Inheritance)

  • 상속은 부모 클래스의 프로퍼티와 메소드를 자식 클래스가 그대로 물려받는 것을 말한다.
  • 상속을 적용하면 똑같은 코드를 또다시 작성하지 않아도 되며, '코드의 재사용성(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;
  }

  streamMusicForFree() {
    console.log(`Free music streaming for ${this.email}`);
  }
}

 

다형성(Polymorphism)

  • 다형성은 하나의 변수가 다양한 종류의 클래스로 만든 여러 객체를 가리킬 수 있음을 의미한다.
  • 이 코드를 보면 지금 forEach 문 안의 user는 User 클래스로 만든 객체를 가리킬 때도 있고, PremiumUser 클래스로 만든 객체를 가리킬 때도 있습니다. 매번 user 객체의 buy 메소드가 호출된다는 점은 같지만, 구체적으로 무슨 클래스로 만든 객체의 buy 메소드가 호출되느냐에 따라 결과가 달라지는데요. 이렇게 단순한 코드로 다양한 결과를 낼 수 있는 건 다형성 덕분이다.
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);
});

 

Reference

반응형