타알못 타입스크립트(TypeScript) 1. 객체지향

Table of Content

TypeScript

중요한 객체지향 원칙

  • 캡슐화(Encapsulation): 객체 내의 데이터를 외부에서 함부로 수정할 수 없도록 보호합니다.
  • 추상화(Abstraction): 외부에선 객체가 어떻게 작동하는지 구체적으로 알 필요가 없습니다. 객체에 포함된 멤버변수(함수)의 이름을 보고 어떤 역할을 하는지만 알면 될 뿐입니다.
  • 상속(Inheritance): 한 번 정의해둔 클래스를 재사용하여 코드의 재사용성을 높입니다.
  • 다형성(Polymorphism): 같은 이름의 멤버함수를 재구현하여 다양한 방식으로 작동할 수 있도록 합니다.

클래스 기본 구조

class Tablet {
  // 멤버 변수
  static BATTERY_CONSUMPTION_PER_HOUR= 1000;
  public model: string = '';
  private battery: number = 0;

  // 생성자
  constructor(model: string, battery: number) {
    this.model = model;
    this.battery = battery;
  }

  // 팩토리 메소드(객체를 생성하여 반환하는 메소드)
  static makeTablet(model: string, battery: number) {
    return new Tablet(model, battery);
  }

  // 멤버 함수
  useBattery(hour: number) {
    if(hour < 0) {
      throw new Error('hour must be positive number');
    }
    this.battery -= Tablet.BATTERY_CONSUMPTION_PER_HOUR * hour;
    console.log(`Curren battery: ${this.battery}mAh`);
  }
}

// 생성자를 사용하여 클래스의 객체(인스턴스)를 생성해도 되고
const ipad1 = new Tablet('iPad Pro 12.9', 10000);

// 팩토리 메소드를 별도로 만들어서 객체를 반환받아도 됨
const ipad2 = Tablet.makeTablet('iPad Pro 12.9', 10000);

ipad1.useBattery(1);
ipad2.useBattery(2);

static 멤버변수

static 멤버변수는 클래스 레벨에서 공유되는 멤버변수입니다. 즉, 이 멤버변수는 클래스 인스턴스를 생성하지 않아도 클래스_이름.STATIC_멤버변수_이름으로 바로 접근이 가능합니다. 클래스 인스턴스를 여러 개 생성해도 static 멤버변수는 여러 개 생성되지 않습니다.

class Tablet {
  static BATTERY_CONSUMPTION_PER_HOUR= 1000; // static 멤버변수
}

캡슐화

캡슐화란 객체를 외부에서 유효하지 않은 값으로 변경되지 않도록 막는 것입니다. 외부에서 멤버변수(함수)의 접근 범위를 아래 키워드를 붙여 설정합니다.

  • public: 외부에서 접근 가능. 키워드 생략 시 기본 public으로 설정됨.
  • protected: 상속받은 클래스에서만 접근 가능
  • private: 외부에서 접근 불가

생성자의 매개변수

생성자에 매개변수를 정의하고 매개변수 접근범위를 명시해주면 그 매개변수는 자동적으로 클래스의 멤버변수가 됩니다.

class Tablet {
  // 아래 두 줄은 생성자에 멤버변수로 정의되어 있으므로 생략 가능
  // private model: string;
  // private battery: string;
  constructor(private model: string, private battery: number) { }

  showTabletInfo(): void {
    console.log(`model: ${this.model}, battery: ${this.battery}`);
  }
}

const ipad: Tablet = new Tablet('iPad Pro 12.9', 10000);
ipad.showTabletInfo();

getter & setter

getter와 setter를 사용하면 멤버변수에 접근하는 방식을 세밀하게 제어할 수 있습니다.

class Tablet {
  constructor(private model: string, private battery: number) { }

  // getter: 멤버변수를 외부에서 직접 접근하지 않고도 외부에서 멤버변수를 조회할 수 있도록 함
  get modelName(): string { return this.model; }
  get batteryCapacity(): number { return this.battery; }

  // setter: 멤버변수를 외부에서 직접 접근하지 않고도 외부에서 멤버변수를 제어할 수 있도록 함
  set modelName(model: string) { this.model = model; }
  set batteryCapacity(battery: number) { this.battery = battery; }

  // 새로운 멤버변수를 만든 후 getter & setter 생성
  private processorName: string = '';
  get processor(): string { return this.processorName };
  set processor(processor: string) { this.processorName = processor; }
}

const ipad = new Tablet('iPad Pro 12.9 4th gen', 10000);
ipad.processor = 'Apple iPad Pro 12.9 4th generation';
ipad.batteryCapacity = 9720;
console.log(`model: ${ipad.modelName}, processor: ${ipad.processor}`);

추상화

추상화란 공통적으로 사용해야 하는 것을 뽑아내어 파악하는 것입니다. 타입스크립트에서는 인터페이스(Interface)를 사용하여 클래스가 구현해야 할 멤버변수(함수)를 명시합니다.

// 인터페이스를 사용하여 구현해야 하는 멤버변수(함수)를 명시
interface Camera {
  useBattery(minute: number): void;
  useCamera(minute: number): void;
}

interface Speaker {
  volume: number;
  useBattery(minute: number): void;
  setVolume(volume: number): void;
}

class Tablet implements Camera, Speaker {
  volume = 0; // Speaker 인터페이스에 명시된 멤버변수 정의 (protected, private 한정자 사용 불가)

  constructor(private model: string, private battery: number) { }

  // Camera 및 Speaker 인터페이스에 명시된 멤버함수 정의
  useBattery(minute: number) {
    console.log(`Using camera during ${minute} minute(s)...`);
  }

  useCamera(minute: number) {
    console.log('Using camera...');
  }

  setVolume(volume: number) {
    this.volume = volume;
    console.log(`Set volume: ${this.volume}`);
  }
}

const tablet = new Tablet('Tablet Pro 12.94 4th Gen', 10000);
tablet.useBattery(5);
tablet.useCamera(2);
tablet.setVolume(50);

상속

상속은 클래스의 멤버변수(함수)를 그대로 받아오는 방법입니다. 상속을 사용하여 공통적인 기능은 재사용하며 특화된 코드만 작성할 수 있습니다.

// 상속하는(부모) 클래스
class Ipad {
  constructor(private model: string, private battery: number) { }
  useSpeaker() { console.log('Using speaker...'); }
}

// 상속받는(자식) 클래스
class IpadPro extends Ipad {
  constructor(model: string, battery: number) { 
    // 자식 클래스의 생성자에서 반드시 부모 클래스의 생성자(super)를 호출해야 함
    super(model, battery); 
  }

  useFaceId() { console.log('Using Face ID...'); }
  useSpeaker() { console.log('Using Pro Speaker!!!'); } // 부모 클래스의 함수를 덮어씌움
}

const ipadPro = new IpadPro('iPad Pro 12.9 4th Gen', 10000);
ipadPro.useFaceId();
ipadPro.useSpeaker();

추상 클래스

추상 클래스는 자식 클래스에게 직접 구현해야 하거나 안 해야 하는 멤버변수(함수)를 명시합니다. 멤버변수(함수) 앞에 abstract 키워드를 붙이면 자식 클래스에서 해당 멤버변수(함수)를 반드시 구현해야 합니다.

추상 클래스의 객체(인스턴스)는 만들 수 없습니다.

abstract class Tablet {
  useSpeaker() { console.log('Using speaker...'); }
  abstract usePencil(): void; // 반드시 구현해야 하는 멤버함수
}

class Ipad extends Tablet {
  usePencil() { 
    console.log('Using apple pencil...'); // 반드시 구현해야 하는 멤버함수
  }
}

// const tablet = new Tablet(); 에러! 추상 클래스의 객체(인스턴스)는 만들 수 없음
const ipad = new Ipad();
ipad.useSpeaker();
ipad.usePencil();

다형성

다형성은 자식 클래스의 형태에 맞게 부모 클래스의 함수를 재구현하는 것입니다.

interface TabletInterface {
  useSpeaker(): void;
}

class Tablet implements TabletInterface {
  useSpeaker() { console.log('Using Speaker...'); }
}

class Ipad extends Tablet {
  useSpeaker() { console.log('Using Speaker on Apple Ipad...'); }
}

class GalaxyTab extends Tablet {
  useSpeaker() { console.log('Using Speaker on Samsung Galaxy Tab...'); };
}

class Surface extends Tablet {
  useSpeaker() { console.log('Using Speaker on Microsoft Surface...'); }
}

// 위의 클래스들의 조상은 TabletInterface이므로 TabletInterface의 배열로 타입을 정의할 수 있음
const tablets: TabletInterface[] = [
  new Tablet(),
  new Ipad(),
  new GalaxyTab(),
  new Surface(),
]

// 각 클래스 인스턴스의 함수이름은 같지만 실행 결과가 달라짐 => 다형성
tablets.forEach(tablet => { tablet.useSpeaker(); });

다중상속

타입스크립트는 클래스 다중상속을 지원하지 않습니다. 따라서 여러 클래스들의 멤버변수(함수)들을 한 번에 가져와 사용할 수 없습니다.

의존성 주입(Dependency Injection)을 사용하면 다른 클래스 객체를 생성자에 들여와 여러 클래스의 멤버변수(함수)를 사용할 수 있습니다. 하지만 클래스끼리 서로 연결되므로 복잡성이 증가하면 유지보수가 힘들어질 수 있습니다.

class Tablet {
  useSpeaker() { console.log('Using speaker...'); }
}

class GalaxyTab {
  useSpen() { console.log('Using S-Pen...'); }
}

class Ipad extends Tablet {
  // Dependency Injection: 다른 클래스 객체를 생성자에 들여와 사용 => 클래스 간 커플링
  constructor(galaxyTab: GalaxyTab) { super(); } 
  useApplePencil() { console.log('Using apple pencil...'); }
  showMessage() { console.log('이 얼마나 끔찍하고 무시무시한 혼종이니!!')}
}

클래스는 다중상속이 되지 않지만 인터페이스는 가능합니다. 따라서 여러 클래스들의 멤버변수(함수)들을 가져와 사용하려고 한다면 각 클래스들에 명시할 인터페이스를 선언한 후 이 인터페이스들을 다중상속하여 사용하면 됩니다.

interface Ipad { useApplePencil(): void; }
interface GalaxyTab { useSpen(): void; }

// 인터페이스 다중상속
class Tablet implements Ipad, GalaxyTab {
  useApplePencil() { console.log('Using apple pencil...'); }
  useSpen() { console.log('Using S-Pen...'); }
  showMessage() { console.log('이 얼마나 끔찍하고 무시무시한 혼종이니!!')}
}

const tablet = new Tablet();
tablet.useApplePencil();
tablet.useSpen();
tablet.showMessage();

댓글 남기기