개발 알다가도 모르겠네요

객체 지향 프로그래밍의 4가지 특징 본문

웹/Javascript

객체 지향 프로그래밍의 4가지 특징

이재빵 2021. 7. 23. 00:14
728x90

1. 추상화(Abstraction)

우리가 객체를 만드는 과정은 현실 또는 가상의 존재를 프로그램 내에서 사용할 용도에 맞게 적절하게 설계하는 과정입니다.

이때 객체를 만들고 나면 그 객체를 사용하는 사람은 객체 내부에 존재하는 복잡한 원리를 모르더라도

객체 외부에 공개된 프로퍼티나 메소드만을 가지고도 객체를 문제없이 잘 사용할 수 있어야 합니다.

이를 위해서는 프로퍼티와 메소드의 이름을 누구나 이해하기 쉽게 잘 지어야하고, 필요한 경우 이렇게 주석을 달거나

class User {
  constructor(email, birthdate) {
    // 사용자의 이메일 주소
    this.email = email;
    // 사용자의 생일
    this.birthdate = birthdate;
  }

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

그 내용을 문서화하여 공개하기도 합니다. 이는 비단 하나의 객체 뿐만 아니라 여러 객체가 모인 라이브러리나 프레임워크의 경우에도 마찬가지입니다. 

우리가 아주 세밀한 원리까지 속속들이 알고 있지 않은 유명한 라이브러리나 프레임워크를 문제없이 사용할 수 있는 것은 그것들이 적절하게 추상화되어 있기 때문입니다.

 

 

 

 

2. 캡슐화(Encapsulation)

캡슐화는 객체 외부에서 함부로 접근하면 안되는 프로퍼티나 메소드에 직접 접근할 수 없도록 하고, 필요한 경우 공개된 다른 메소드를 통해서만 접근할 수 있도록 하는 것을 의미합니다. 아래의 코드를 보면

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 프로퍼티가 보호받고 있는 프로퍼티라는 것을 알 수 있습니다. 

 

 

 

사실 다른 언어에서는 해당 언어의 문법 차원에서(ex. Java에서 캡슐화하고 싶은 변수나 메소드 앞에 붙이는 private 키워드) 캡슐화를 지원하는 경우가 많지만 자바스크립트에는 그러한 문법이 없습니다. 

하지만 클로저(Closure)라는 것을 사용해서 우회적으로 완벽한 캡슐화를 구현할 수는 있습니다.

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);

지금 이 코드를 보면 createUser라고 하는 Factory function이 보입니다.

그런데 생성하려는 user 객체 안에 _email 프로퍼티가 있는 게 아니라, createUser 함수 안에, 그리고 user 객체 바깥에 _email이라는 변수가 있습니다.

대신에 user 객체 안에는 _email 변수의 값을 읽고 쓸 수 있는 email이라는 getter/setter 메소드가 있습니다.

지금 마지막 부분에서 createUser라는 Factory function으로 user1이라는 객체를 생성하고, user1 객체의 email getter 메소드를 호출했습니다. 이 코드의 실행 결과를 확인해보면,

 

 

클로저란 자바스크립트에서 어떤 함수와 그 함수가 참조할 수 있는 값들로 이루어진 환경을 하나로 묶은 것을 의미합니다. 예를 들어, 지금 createUser 함수가 실행되는 시점에 email이라는 getter/setter 메소드는 _email 이라는 변수의 값에 접근할 수 있는 상태입니다. 그리고 여기서 핵심은 이 email getter/setter 메소드들은 메소드를 갖고 있는 객체가 리턴된 이후더라도 여전히 _email에 접근하는 것이 가능하다는 점입니다.

바로 이렇게 함수가 정의된 당시에 참조할 수 있었던 변수들을 계속 참조할 수 있는 상태의 함수를 클로저라고 합니다. 

보통 다른 프로그래밍 언어였다면 createUser 함수 내부가 실행될 때만 email getter/setter 메소드가 _email 변수에 접근할 수 있었겠지만, 자바스크립트에서는 클로저라는 개념으로 해당 환경을 함수와 함께 그대로 유지시켜주는 것입니다.

만약 클로저가 아닌 경우에는 _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); // _ 추가

user1 객체의 _email 프로퍼티에 접근하려고 하면, user1 객체 자체 내에는 _email이라고 하는 프로퍼티가 없고, 바깥의 _email 변수에 현재 접근할 수도 없기 때문에 undefined가 출력됩니다.

 

 

 

 

3. 상속(Inheritance)

상속은 부모 클래스의 프로퍼티와 메소드를 자식 클래스가 그대로 물려받는 것입니다.

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)할 수도 있는데요. 이 오버라이딩은 바로 다음에 나오는 '다형성'과 연관이 깊습니다.

 

 

 

4. 다형성(Polymorphism)

다형성은 하나의 변수가 다양한 종류의 클래스로 만든 여러 객체를 가리킬 수 있음을 의미합니다.

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 메소드가 호출되느냐에 따라 결과가 달라집니다.