본문 바로가기

코딩도 합니다/JS

[자바스크립트 js] 객체 지향 프로그래밍 4가지 개념 / 추상화 / 캡슐화 / 상속 / 다형성 / super / instanceof



  1. 추상화 (Abstraction)

  • 우리가 객체를 만드는 과정은 현실 또는 가상의 존재를 프로그램 내에서 사용할 용도에 맞게 적절하게 설계하는 과정.
  • class를 설계하는 것도 추상화에 해당한다
[ user class를 만들 때 ]
user 객체의 속성 - 프로퍼티
user 객체의 행동 - 메소드
  • class 이름과 프로퍼티, 메소드 이름을 잘 지어야 한다.
  • 이름이 직관적으로 이해하기 쉬워야 다른 개발자들과 협업하기에 좋다.
  • 이름을 의미가 있는 단어들로 짓기.
  • 프로퍼티와 메소드에 그 의미를 나타내는 간단한 주석을 적어주기.

 

 

 

  2. 캡슐화 (Encapsulation)

  • 객체의 특정 프로퍼티에 직접 접근하지 못하도록 막는 것. 이를 통해 프로그래밍의 안전성을 높일 수 있다.
  • 특정 프로퍼티에 대한 접근을 미리 정해진 메소드들을 통해서만 가능하게 하는 것.
  • 캡슐화는 객체 외부에서 함부로 접근하면 안되는 프로퍼티나 메소드에 직접 접근할 수 없도록 한다.

 

 

2-1. setter 메소드

  • 프로퍼티를 보호한다.
  • 프로퍼티의 값을 설정한다.
  • setter 메소드를 설정하면 해당 객체를 사용하는 다른 개발자가 프로퍼티에 이상한 값을 설정하는 것을 방지할 수 있다.
  • 언더바가 붙은 프로퍼티는 새롭게 설정한 프로퍼티.
    숨기고자 하는 프로퍼티 이름 앞에 언더바를 사용한다.

 

 

2-2. getter 메소드

  • 프로퍼티를 보호한다.
  • 프로퍼티의 값을 구하는 함수.
  • 프로퍼티의 값을 읽는 용도로 사용하기 때문에 파라미터를 따로 써줄 필요 없다.

 

 

 

2-3. 캡슐화 하는 방법

_email 프로퍼티에 직접 접근하는 방법 대신에, email이라는 getter/setter 메소드로만 접근한다.

class User {
  constructor(email, birthdate) {
    this.email = email;
    this.birthdate = birthdate;
  }

  buy(item) {
    console.log(`${this.email} buys ${item.name}`);
  }

  // getter
  get email() {
    return this._email;
  }

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

const user1 = new User('doordie@took.com', '1992-09-23');
user1.email = 'newDoordie@took.com';
console.log(user1.email);
  • 위 코드는 완벽한 캡슐화가 된 상태가 아니다.
    보호하려는 프로퍼티 _email에 아래와 같은 방법으로 접근이 가능하기 때문이다.
  • 자바스크립트에는 캡슐화를 자체적으로지원하는 문법이 없다.
  • 클로저(Clouser)라는 개념으로 우회해서 완벽한 캡슐화를 할 수 있다.
console.log(user1._email);
user1._email = 'tooktak';

 

 

 

2-4. 클로저(Clouser) : 완벽한 캡슐화

어떤 함수와 그 함수가 참조할 수 있는 값들로 이루어진 환경을 하나로 묶은 것.

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('doordie@took.com', '1992-09-23');
console.log(user1.email);
  • 위 코드를 보면
    1. createUser 함수 안에
    2. user 객체 바깥에
    _email 이라는 변수가 있다.
  • user 객체 안에는 _email 변수의 값을 읽고 쓸 수 있는 email 이라는 getter/setter 메소드가 있다.

 

  • 마지막 부분에서 createUser라는 factory functon으로 user1이라는 객체를 생성했다.
    user1 객체의 email getter 메소드를 호출했고, 결과는 'doordie@took.com'이다.
    즉, 함수 안의 변수의 값을 이미 리턴된 객체에서 읽었다. = 클로저로 인해 가능.

 

  • createUser 함수가 실행되는 시점에 email 이라는 getter/setter 메소드는 _email 이라는 변수의 값에 접근할 수 있다.
    핵심은 이 email getter/setter 메소드들은 메소드를 갖고 있는 객체가 리턴된 이후여도 여전히 _email 에 접근이 가능하다.
    이렇게 함수가 정의된 당시에 참조할 수 있었던 변수들을 계속해서 참조할 수 있는 상태의 함수를 클로저라고 한다.

 

 

 

2-5. 메소드도 캡슐화 가능

function createUser(email, birthdate) {
  const _email = email;
  let _point = 0;

  function increasePoint() {
    _point += 1;
  }

  const user = {
    birthdate,

    get email() {
      return _email;
    },

    get point() {
      return _point;
    },

    buy(item) {
      console.log(`${this.email} buys ${item.name}`);
      increasePoint();
    },
  };

  return user;
}

const item = {
  name: '맥북',
  price: 10000,
};

const user1 = createUser('doordie@took.com', '1992-09-23');
user1.buy(item);
user1.buy(item);
user1.buy(item);
console.log(user1.point);
  • _point 변수 추가. 사용자가 물건을 살 때마다 1포인트씩 적립해줄 목적으로 만든 변수.
  • _point 변수를 1씩 늘려주는 함수는 increasePoint 함수.
  • point 메소드 정의해둔 상태
  • incresePoint 라는 함수는 유저 객체의 buy 메소드 안에서 쓰이고 있다.
  • buy 메소드를 실행할 때 그 안에서 increasePoint 함수를 호출.
  • 맨 마지막 부분의 코드를 보면 user1 객체의 buy 메소드를 호출하여 point getter 메소드를 호출하고 있다.
// 결과
// doordie@took.com buys 맥북
// doordie@took.com buys 맥북
// doordie@took.com buys 맥북
// 3
  • increasePoint 함수는 보호 받고 있기 때문에, user1 객체로 바로 increasePoint 함수를 호출할 수 없다. 
  • 이런식으로 메소드 혹은 함수도 프로퍼티와 마찬가지로 클로저를 통해 캡슐화를 해서 보호할 수 있다.
const user1 = createUser('doordie@took.com', '1992-09-23');
user1.buy(item);
user1.buy(item);
user1.buy(item);
console.log(user1.point);

user1.increasePoint(); // user1 객체로 increasePoint 직접 호출 => 에러 발생

 

 

 

 

  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)할 수 있다.
    => '다형성'과 연관.
  • derived class : 자식 클래스

 

 

 

  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: 10000, 
};

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 메소드가 호출되느냐에 따라 결과가 달라진다 => 단순한 코드로 다양한 결과를 낼 수 있는 건 다형성 덕분.
  • super : 자식 클래스에서 부모 클래스의 생성자 함수를 호출할 때 / 부모 클래스의 일반 메소드를 호출할 때 사용.

 

 

 

 

  instanceof 연산자

현재 변수가 가리키는 객체가 정확히 어느 클래스로 만든 객체인지 확인하는 법.

 

1. 

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) => {
	console.log(user instanceof PremiumUser);
});


// 결과
// false
// true
// false
// true
// false
// true

 

 

2. PremiumUser가 User 클래스를 상속했기 때문에 instanceof User 하면 모두 true 결과값이 나온다.

자식 클래스로 만든 객체는 부모 클래스로 만든 객체로도 인정이 된다.

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) => {
	console.log(user instanceof User);
});


// 결과
// true
// true
// true
// true
// true
// true

 

 

 

 

  클래스는 파일 하나당 하나씩 만들 것 ★

  • 한 눈에 코드를 보기 위해 해당 게시물들의 코드들은 하나의 코드블럭 작성되었다.
  • export와 import를 사용하여 파일별로 클래스와 메인 로직을 쪼개서 작성하는 것이 좋다.
728x90