본문 바로가기
  • 살짝 구운 김 유나
Web/TypeScript

[TypeScript] 객체지향 (OOP)

by yunae 2023. 2. 24.

절차 지향

: 정의된 순서대로(절차적으로) 함수가 하나씩 호출됨

  • 하나를 수정하기 위해서 전체적인 어플리케이션을 이해해야함
  • 수정했을때 side Effect가 발생할 수 있음
  • 한눈에 이해하기 어렵다

Coffee Maker 예제)

{
  /**
   * 절차지향적 프로그래밍
   */
  
  type CoffeeCup = {
    shots: number;
    hasMilk: boolean;
  };

  const BEANS_GRAMM_PER_SHOT: number = 7;

  let coffeebeans: number = 0;
  function makeCoffee(shots: number): CoffeeCup {
    // 커피 콩이 부족하다면
    if (coffeebeans < shots * BEANS_GRAMM_PER_SHOT) {
      throw new Error("Not enough coffee beans!");
    }
    coffeebeans -= shots * BEANS_GRAMM_PER_SHOT;
    return {
      shots,
      hasMilk: false,
    };
  }
  
  // 커피 콩 충전
  coffeebeans += 3 * BEANS_GRAMM_PER_SHOT
  const coffee = makeCoffee(2);
  console.log(coffee)
}

실행결과

 

 

 

 

객체 지향 (OOP)

: 서로 관련있는 데이터와 함수를 하나의 객체로 정의해서 사용

  • 관련있는 객체만 수정
  • 재사용 가능
  • 확장성

=> 생산성, 퀄리티, 빠른 속도의 개발

 

 

Class

객체는 대부분 class를 이용해서 정의! class는 템플릿같은 존재,,

객체는 class에 실제로 데이터를 넣어서 만든 instance

Coffee Maker 예제)

{
  /**
   * 객체지향 프로그래밍
   */

  type CoffeeCup = {
    shots: number;
    hasMilk: boolean;
  };

  class CoffeeMaker {
    BEANS_GRAMM_PER_SHOT: number = 7;
    coffeebeans: number = 0;

    // 인스턴스를 만들 때 호출되는 함수
    constructor(coffeeBeans: number) {
      this.coffeebeans = coffeeBeans; // 커피 콩 충전
    }

    makeCoffee(shots: number): CoffeeCup {
      // 커피 콩이 부족하다면
      if (this.coffeebeans < shots * this.BEANS_GRAMM_PER_SHOT) {
        throw new Error("Not enough coffee beans!");
      }
      this.coffeebeans -= shots * this.BEANS_GRAMM_PER_SHOT;
      return {
        shots,
        hasMilk: false,
      };
    }
  }

  // 인스턴스 생성
  const maker = new CoffeeMaker(32);
  console.log(maker)
  const maker2 = new CoffeeMaker(25);
  console.log(maker2)
}

 

 

 

Static

위의 코드에서 BEANS_GRAMM_PER_SHOT 이라는 멤버 변수는 어떤 객체에서도 같은 값을 가진다.

=> 객체를 생성할때마다 BEANS_GRAMM_PER_SHOT도 새로 만들어지기 때문에 메모리의 낭비가 발생!

이때, static 키워드를 붙여주면 object가 아닌 class와 연결되므로 object마다 생성되지 않는다.

static BEANS_GRAMM_PER_SHOT: number = 7; // class level
coffeebeans: number = 0; // instance level

=> 클래스 자체에 연결되어있기 때문에 this가 아닌 클래스 이름을 붙여주어야 한다.

CoffeeMaker.BEANS_GRAMM_PER_SHOT
{
  /**
   * 객체지향 프로그래밍
   */

  type CoffeeCup = {
    shots: number;
    hasMilk: boolean;
  };

  class CoffeeMaker {
    static BEANS_GRAMM_PER_SHOT: number = 7; // class level
    coffeebeans: number = 0; // instance level

    // 인스턴스를 만들 때 호출되는 함수
    constructor(coffeeBeans: number) {
      this.coffeebeans = coffeeBeans; // 커피 콩 충전
    }

    makeCoffee(shots: number): CoffeeCup {
      // 커피 콩이 부족하다면
      if (this.coffeebeans < shots * CoffeeMaker.BEANS_GRAMM_PER_SHOT) {
        throw new Error("Not enough coffee beans!");
      }
      this.coffeebeans -= shots * CoffeeMaker.BEANS_GRAMM_PER_SHOT;
      return {
        shots,
        hasMilk: false,
      };
    }
  }

  // 인스턴스 생성
  const maker = new CoffeeMaker(32);
  console.log(maker);
  const maker2 = new CoffeeMaker(25);
  console.log(maker2);
}

=> 클래스 레벨의 변수는 객체에 포함되어 있지 않음

함수에서도 적용 가능

// 클래스 내부의 어떤 속성도 팔요로 하지 않기 떄문에  static으로 함수 선언
static makeMachine(coffeeBeans:number): CoffeeMaker {
    return new CoffeeMaker(coffeeBeans);
}

const maker3 = CoffeeMaker.makeMachine(3);
console.log(maker3)

 

 

 

객체지향의 원칙

1. 캡슐화

: 서로 관련있는 데이터와 함수를 한 객체 안에 담아두는 것, 외부에는 내용을 보여주지 않는 것

-> 내부 상태를 외부에서 변경할 수는 없지만 외부에서 하는 행동으로 인해 내부 상태가 변경될 수는 있음

2. 추상화

: 내부의 복잡한 기능을 알고있지 않아도, 외부에서 간단한 인터페이스를 통해 기능을 이용할 수 있는것

3. 상속 (IS-A 관계)

: 한번 잘 정의해둔 클래스를 상속받아 재정의하여 사용할 수 있음

4. 다형성

: 하나의 객체가 여러가지 타입을 가질 수 있는 것

 

 

 

1. Encapsulation (캡슐화)

위의 CoffeeMaker 예제에서 클래스 외부에서 커피콩을 음수로 충전할 수있다.

=> 유효하지 않은 값도 충전할 수 있기 때문에 외부에서 값을 변경할 수 없게 해야 함

const maker = CoffeeMaker.makeMachine(3);
maker.coffeebeans = 3;
maker.coffeebeans = -33; // 유효하지 않음

 

public 클래스 외부에서 내부 접근 가능, default 값
private 클래스 외부에서 내부의 변수에 접근하지 못함
protected 상속 시에 외부에서는 접근할 수 없지만 자식 클래스에서는 접근 가능
lass CoffeeMaker {
    private static BEANS_GRAMM_PER_SHOT: number = 7; // class level
    // 자식 클래스에서는 접근 가능
    protected coffeebeans: number = 0; // instance level

    // 클래스 내부의 어떤 속성도 팔요로 하지 않기 떄문에  static으로 함수 선언
    static makeMachine(coffeeBeans: number): CoffeeMaker {
      return new CoffeeMaker(coffeeBeans);
    }
 }

외부에서 생성자를 직접 사용하는 것을 금지하고 싶다면?

constructor 메소드를 private으로 선언하여 static method를 사용하도록 권장

 // 인스턴스를 만들 때 호출되는 함수
private constructor(coffeeBeans: number) {
  this.coffeebeans = coffeeBeans; // 커피 콩 충전
}

// 클래스 내부의 어떤 속성도 팔요로 하지 않기 떄문에  static으로 함수 선언
static makeMachine(coffeeBeans: number): CoffeeMaker {
  return new CoffeeMaker(coffeeBeans);
}

 

 

 

* Getter & Setter

Getter

class User {
    firstName: string;
    lastName: string;
    fullName: string;
    constructor(firstName: string, lastName: string) {
      this.firstName = firstName;
      this.lastName = lastName;
      this.fullName = `${firstName} ${lastName}`;
    }
  }

  const user = new User("Yuna", "Kim");
  console.log(user.fullName);
  user.firstName = "Hey";
  // fullName은 한번 할당되어진대로 고정
  console.log(user.fullName);

=> user의 firstName을 수정해보았다.

실행결과

=> fullName이라는 멤버변수는 객체가 생성될 때 할당 된 대로 고정된다.

이때, get을 사용해보자

  class User {
    firstName: string;
    lastName: string;
    // fullName이 불리는 시점에 계산된 값을 반환해준다.
    get fullName(): string {
      return `${this.firstName} ${this.lastName}`;
    }
    constructor(firstName: string, lastName: string) {
      this.firstName = firstName;
      this.lastName = lastName;
    }
  }

  const user = new User("Yuna", "Kim");
  console.log(user.fullName);
  user.firstName = "Hey";
  console.log(user.fullName);

=> 함수 형태이지만 접근할 때는 멤버변수와 동일하게 접근해야한다.

=> fullName이 불리는 시점에 계산 된 값을 반환한다.

실행결과

 

Getter & Setter

  class User {
    // fullName이 불리는 시점에 계산된 값을 반환해준다.
    get fullName(): string {
      return `${this.firstName} ${this.lastName}`;
    }
    private internalAge = 4;
    // Getter
    get age(): number {
      return this.internalAge;
    }
    // Setter - 값을 할당
    set age(num: number) {
      if (num < 0) {
        throw new Error("유효하지 않은 값 입니다!");
      }
      this.internalAge = num;
    }
    // 생성자에 접근제어자를 설정하면 자동으로 멤버변수가 된다.
    constructor(private firstName: string, private lastName: string) {}
  }

  const user = new User("Yuna", "Kim");
  user.age = 6;
  console.log(user.age);
}

 

 

 

 

2. Abstraction (추상화)

: 클래스를 이용하는 사람에게 정말 필요한 함수만 노출

{
  /**
   * 추상화
   */

  type CoffeeCup = {
    shots: number;
    hasMilk: boolean;
  };

  class CoffeeMaker {
    private static BEANS_GRAMM_PER_SHOT: number = 7; // class level
    private coffeebeans: number = 0; // instance level

    // 인스턴스를 만들 때 호출되는 함수
    private constructor(coffeeBeans: number) {
      this.coffeebeans = coffeeBeans; // 커피 콩 충전
    }

    // 클래스 내부의 어떤 속성도 팔요로 하지 않기 떄문에  static으로 함수 선언
    static makeMachine(coffeeBeans: number): CoffeeMaker {
      return new CoffeeMaker(coffeeBeans);
    }

    fillCoffeeBeans(beans: number) {
      if (beans < 0) {
        throw new Error("value for beans should be greater than 0");
      }
      this.coffeebeans += beans;
    }

    private grindBeans(shots: number) {
      console.log(`grinding beans for ${shots}`);
      if (this.coffeebeans < shots * CoffeeMaker.BEANS_GRAMM_PER_SHOT) {
        throw new Error("Not enough coffee beans!");
      }
      this.coffeebeans -= shots * CoffeeMaker.BEANS_GRAMM_PER_SHOT;
    }

    private preheat(): void {
      console.log("heating up,,,");
    }

    private extract(shots: number): CoffeeCup {
      console.log(`Pulling ${shots} shots...`);
      return {
        shots,
        hasMilk: false,
      };
    }

    makeCoffee(shots: number): CoffeeCup {
      this.grindBeans(shots);
      this.preheat();
      return this.extract(shots);
    }
  }

  const maker = CoffeeMaker.makeMachine(3);
  maker.fillCoffeeBeans(32);
}

외부에서 fillCoffeeBeans와 makeCoffee에만 접근할 수 있게 하기 위해서 내부에 있는 다른 함수에 private을 붙여주었다.

* Interface 이용하는 방법

: 추상화를 더 극대화해서 사용할 수 있다.

interface를 클래스에 implement 하여 해당 클래스의 조상을 설정함과 동시에 사용할 함수, 변수를 제한할 수 있음

  // 각각의 인터페이스에서 사용할 메소드, 변수를 정의함
  interface CoffeeMaker {
    makeCoffee(shots: number): CoffeeCup;
  }

  interface CommercialCoffeeMaker {
    makeCoffee(shots: number): CoffeeCup;
    fillCoffeeBeans(beans: number): void;
    clean(): void;
  }
  
  // 인스턴스는 각각 CoffeeMaker, CommercialCoffeeMaker를 implment함
  class CoffeeMachine implements CoffeeMaker, CommercialCoffeeMaker {
    private static BEANS_GRAMM_PER_SHOT: number = 7; // class level
    private coffeeBeans: number = 0; // instance level

    // 인스턴스를 만들 때 호출되는 함수
    private constructor(coffeeBeans: number) {
      this.coffeeBeans = coffeeBeans; // 커피 콩 충전
    }

    // 클래스 내부의 어떤 속성도 팔요로 하지 않기 떄문에  static으로 함수 선언
    static makeMachine(coffeeBeans: number): CoffeeMachine {
      return new CoffeeMachine(coffeeBeans);
    }

    fillCoffeeBeans(beans: number) {
      if (beans < 0) {
        throw new Error("value for beans should be greater than 0");
      }
      this.coffeeBeans += beans;
    }

    
    private grindBeans(shots: number) {
      console.log(`grinding beans for ${shots}`);
      if (this.coffeeBeans < shots * CoffeeMachine.BEANS_GRAMM_PER_SHOT) {
        throw new Error("Not enough coffee beans!");
      }
      this.coffeeBeans -= shots * CoffeeMachine.BEANS_GRAMM_PER_SHOT;
    }
    
    private preheat(): void {
      console.log("heating up,,,");
    }
    
    private extract(shots: number): CoffeeCup {
      console.log(`Pulling ${shots} shots...`);
      return {
        shots,
        hasMilk: false,
      };
    }

    clean() {
      console.log("Cleaning the Machine");
    }
    
    makeCoffee(shots: number): CoffeeCup {
      this.grindBeans(shots);
      this.preheat();
      return this.extract(shots);
    }
  }

각각의 인터페이스로 객체 생성해보기

class AmateurUser {
    constructor(private machine: CoffeeMaker) {}
    makeCoffee() {
      const coffee = this.machine.makeCoffee(2);
      console.log(coffee);
    }
  }

  class ProBarista {
    constructor(private machine: CommercialCoffeeMaker) {}
    makeCoffee() {
      const coffee = this.machine.makeCoffee(2);
      console.log(coffee);
      this.machine.fillCoffeeBeans(45);
      this.machine.clean();
    }
  }

  const maker: CoffeeMachine = CoffeeMachine.makeMachine(32);
  // 같은 maker를 인자로 받지만 각각의 인터페이스에 규약된 함수만 사용할 수 있다.
  const amateur = new AmateurUser(maker);
  amateur.makeCoffee();
  console.log('------------------------')
  const pro = new ProBarista(maker); 
  pro.makeCoffee();

실행결과

 

 

 

 

3. Inheritance (상속)

: 부모 클래스를 상속받아  공통적인 부분은 그대로 재사용 하거나 overwriting 하여 사용

[부모 클래스]

class CoffeeMachine implements CoffeeMaker {
    private static BEANS_GRAMM_PER_SHOT: number = 7; // class level
    private coffeeBeans: number = 0; // instance level

    // 인스턴스를 만들 때 호출되는 함수
    public constructor(coffeeBeans: number) {
      this.coffeeBeans = coffeeBeans; // 커피 콩 충전
    }

    // 클래스 내부의 어떤 속성도 팔요로 하지 않기 떄문에  static으로 함수 선언
    static makeMachine(coffeeBeans: number): CoffeeMachine {
      return new CoffeeMachine(coffeeBeans);
    }

    fillCoffeeBeans(beans: number) {
      if (beans < 0) {
        throw new Error("value for beans should be greater than 0");
      }
      this.coffeeBeans += beans;
    }

    private grindBeans(shots: number) {
      console.log(`grinding beans for ${shots}`);
      if (this.coffeeBeans < shots * CoffeeMachine.BEANS_GRAMM_PER_SHOT) {
        throw new Error("Not enough coffee beans!");
      }
      this.coffeeBeans -= shots * CoffeeMachine.BEANS_GRAMM_PER_SHOT;
    }

    private preheat(): void {
      console.log("heating up,,,");
    }

    private extract(shots: number): CoffeeCup {
      console.log(`Pulling ${shots} shots...`);
      return {
        shots,
        hasMilk: false,
      };
    }

    clean() {
      console.log("Cleaning the Machine");
    }

    makeCoffee(shots: number): CoffeeCup {
      this.grindBeans(shots);
      this.preheat();
      return this.extract(shots);
    }
  }

[자식 클래스]

=> extends를 이용하여 상속

=> 변경할 부분은 오버라이팅 하여 재사용함 (여기서는 hasMik를 true로 변경해주었음)

 class CaffeLatteMachine extends CoffeeMachine {
    constructor(beans: number, public readonly serialNumber: string) {
      super(beans);
      // ...code
    }
    private steamMilk(): void {
      console.log("Steaming some milk...");
    }
    makeCoffee(shots: number): CoffeeCup {
      const coffee = super.makeCoffee(shots);
      this.steamMilk();
      return { ...coffee, hasMilk: true }; // 우유만 부어줘,,,
    }
  }

실행결과

※ 자식클래스에서 생성자(constructor)를 새로 작성하는 경우에는, super()를 호출해주어야함

// 자식 클래스
class CaffeLatteMachine extends CoffeeMachine {
    constructor(beans: number, public readonly serialNumber: string) {
      // 부모 클래스 생성시 필요한 데이터도 가져와야함
      super(beans);
      // ...code
    }
    private steamMilk(): void {
      console.log("Steaming some milk...");
    }
    makeCoffee(shots: number): CoffeeCup {
      const coffee = super.makeCoffee(shots);
      this.steamMilk();
      return { ...coffee, hasMilk: true }; // 우유만 부어줘,,,
    }
  }
  ...
    const latteMachine = new CaffeLatteMachine(23, 'sss');

 

 

 

4. Polemorphism (다형성)

우선, 설탕을 추가하는 커피 머신을 만들어보자

/ CoffeeMachine을 상속하는 클래스
  class SweetCoffeeMaker extends CoffeeMachine {
    private addSugar(): void {
      console.log("Adding some sugar...");
    }

    makeCoffee(shots: number): CoffeeCup {
      const coffee = super.makeCoffee(shots);
      this.addSugar();
      return { ...coffee, hasSugar: true }; // 우유만 부어줘,,,
    }
  }

내부적으로 구현된 다양한 클래스들이 한가지 인터페이스를 구현하거나 같은 부모로부터 상속받았을때, 어떤 클래스인지 구분하지 않고 공통된 함수(makeCoffee)를 호출할 수 있다!

모든 machine이 makeCoffee라는 공통된 함수를 호출할 수 있는 것은 ,

machines라는 배열의 타입이 CoffeMachine[] 이기 때문이다

현재는 clean, fillCoffeBeans, makeCoffee라는 함수에 모두 접근 가능

CoffeeMachine은 CoffeeMaker를 구현하므로,

이렇게도 명시할 수 있게되는데, 그럼 각 machine들은 인터페이스에 작성된 대로 makeCoffee라는 함수에만 접근할 수 있게 된다

interface CoffeeMaker {
    makeCoffee(shots: number): CoffeeCup;
  }
  
  ...
  
 machines.forEach((machine) => {
    console.log("--------------------------");
    machine.makeCoffee(1);
  });

 

 

 

 

 

 

 

연어 와구와구 먹고 아이서커림은 행복 치사량이요,

만날때 나 준다고 딸기 한박스 들고오는 친구 있으면 성공한 인생 아니냐구요,, 감사해~

'Web > TypeScript' 카테고리의 다른 글

[TypeScript] 제네릭 (Generics)  (0) 2023.03.01
[TypeScript] Composition  (0) 2023.02.25
[TypeScript] 기본 타입 #2  (0) 2023.02.22
[TypeScript] 기본 타입 #1  (0) 2023.02.21
[TypeScript] TypeScript 시작하기  (0) 2023.02.21

댓글