본문 바로가기
내일배움캠프/Today I Learned

[TIL 2023.07.28] 타입스크립트 객체 지향 프로그래밍

by 괴코딩 2023. 7. 31.

💻오늘 배운 내용

클래스

객체를 만들기 위한 틀(template)

객체들이 공통으로 가지는 속성(attribute)과 메서드(method)를 정의

- 속성: 객체의 성질

- 메서드: 객체의 성질을 변화시키거나 객체에서 제공하는 기능들을 사용하는 창구

 

❓객체: 클래스를 기반으로 생성. 클래스의 인스턴스(instance)

 

사용예시

class Person {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  sayHello() {
    console.log(`안녕하세요! 제 이름은 ${this.name}이고, 나이는 ${this.age}살입니다.`);
  }
}

const person = new Person('Spartan', 30);
person.sayHello();

TypeScript에서 클래스를 정의하려면 class 키워드를 사용

클래스의 속성과 메서드를 정의하고 new 키워드를 사용하여 객체를 생성

 

생성자(constructor)

클래스의 인스턴스를 생성하고 초기화하는 데 사용되는 특별한 메서드

인스턴스를 생성할 때 자동으로 호출

클래스 내에 오직 하나만 존재

 

접근 제한자

  • public
    • 클래스 외부에서도 접근이 가능한 접근 제한자
    • 접근 제한자가 선언이 안되어있다면 기본적으로 접근 제한자는 public
    • 민감하지 않은 객체 정보를 열람할 때나 누구나 해당 클래스의 특정 기능을 사용해야 할 때 사용
  • private
    • 클래스 내부에서만 접근이 가능한 접근 제한자
    • 보통은 클래스의 속성은 대부분 private으로 접근 제한자를 설정 > 외부에서 직접적으로 객체의 속성을 변경할 수 없게 제한
    • 클래스의 속성을 보거나 편집하고 싶다면 별도의 getter/setter 메서드를 이용해야 한다.
  • protected
    • 클래스 내부와 해당 클래스를 상속받은 자식 클래스에서만 접근이 가능한 접근 제한자

 

상속

상속은 객체 지향 프로그래밍에서 클래스 간의 관계를 정의

상속을 통해 기존 클래스의 속성과 메서드를 물려받아 새로운 클래스를 정의할 수 있다.

똑같은 코드를 계속 반복적으로 작성할 필요 없는 장점

 

상속을 구현하려면 extends 키워드를 사용

class Animal {
 ...생략
}

class Dog extends Animal {
  ...생략
}

super 키워드는 자식 클래스가 부모 클래스를 참조하는데 사용하는 키워드

(자식 클래스에서 생성자를 정의할 때 부모 클래스의 생성자를 호출)

 

*서브타입과 슈퍼타입

서브타입 : 두 개의 타입 A와 B가 있고 B가 A의 서브타입이면 A가 필요한 곳에는 어디든 B를 안전하게 사용

슈퍼타입 : 두 개의 타입 A와 B가 있고 B가 A의 슈퍼타입이면 B가 필요한 곳에는 어디든 A를 안전하게 사용

 

upcasting과 downcasting은 슈퍼타입, 서브타입으로 변환할 수 있는 타입 변환 기술

 

☑️ upcasting

서브타입 → 슈퍼타입으로 변환을 하는 것을 upcasting

타입 변환은 암시적으로 이루어져 별도의 타입 변환 구문이 필요가 없다.

upcasting이 필요한 이유는 서브타입 객체를 슈퍼타입 객체로 다루면 유연하게 활용할 수 있기 때문

 

☑️ downcasting

슈퍼타입 → 서브타입으로 변환을 하는 것을 downcasting

as 키워드로 명시적으로 타입 변환을 해줘야 한다.

서브타입의 메서드를 사용해야 될 때 사용

 

추상 클래스

클래스와는 다르게 인스턴스화를 할 수 없는 클래스

상속을 통해 자식 클래스에서 메서드를 제각각 구현하도록 강제를 하는 용도

최소한의 기본 메서드는 정의를 할 수 있다.

하지만!! 핵심 기능의 구현은 전부 자식 클래스에게 위임을 하는 것이 주 목적

 

추상 클래스 및 추상 함수는 abstract 키워드를 사용하여 정의

1개 이상의 추상 함수가 있는 것이 일반적

 

예시코드

abstract class Shape {
  abstract getArea(): number; // 추상 함수 정의!!!

  printArea() {
    console.log(`도형 넓이: ${this.getArea()}`);
  }
}

class Circle extends Shape {
  radius: number;

  constructor(radius: number) {
    super();
    this.radius = radius;
  }

  getArea(): number { // 원의 넓이를 구하는 공식은 파이 X 반지름 X 반지름
    return Math.PI * this.radius * this.radius;
  }
}

이 추상 클래스를 상속 받은 자식 클래스들은 반드시 getArea 함수를 구현해야 한다.

 

인터페이스

TypeScript에서 객체의 타입을 정의하는데 사용한다.

객체가 가져야 하는 속성과 메서드를 정의

인터페이스를 구현한 객체는 인터페이스를 반드시 준수해야 한다. 규약과 같아서 어길 수가 없음

인터페이스를 사용하면 코드의 안정성을 높이고 유지 보수성을 향상시킬 수 있다.

 

추상 클래스와 인터페이스의 차

  추상클래스 인터페이스
구현부 제공 여부 클래스의 기본 구현을 제공 객체의 구조만을 정의하고 기본 구현을 제공하지 않
상속 메커니즘 단일 상속만 지원 다중 상속을 지원
(하나의 클래스는 여러 인터페이스를 구현할 수 있다)
구현 메커니즘 추상 클래스를 상속받은 자식 클래스는 반드시 추상 함수를 구현해야 한다. 인터페이스에 정의된 모든 메서드를 전부 구현해야 한다.
용도 기본 구현을 제공하고 상속을 통해 확장 객체가 완벽하게 특정 구조를 준수하도록 강제

 

S.O.L.I.D 원칙

객체 지향 설계를 할 때는 S.O.L.I.D 원칙을 따라서 설계를 하는 것이 필수

프로그램이 유연하고, 확장 가능하며, 이해하기 쉬운 구조를 가지도록 도와준다.

 

S(SRP. 단일 책임 원칙) → ⭐ 매우 중요 ⭐

  • 클래스는 하나의 책임만 가져야 한다는 매우 기본적인 원칙
  • 특히, 5개의 설계 원칙 중 가장 기본적이고 중요

O(OCP. 개방 폐쇄 원칙) → 인터페이스 혹은 상속을 잘 쓰자!

  • 클래스는 확장에 대해서는 열려 있어야 하고 수정에 대해서는 닫혀 있어야 한다
  • 클래스의 기존 코드를 변경하지 않고도 기능을 확장할 수 있어야 한다.
  • 즉, 인터페이스나 상속을 통해서 이를 해결할 수가 있다. (부모 클래스의 기존 코드 변경을 하지 않고 기능을 확장하는데 아무런 문제가 없으니까)

L(LSP. 리스코프 치환 원칙)

  • 서브타입은 기반이 되는 슈퍼타입을 대체할 수 있어야 한다
  • 다시 말해, 자식 클래스는 부모 클래스의 기능을 수정하지 않고도 부모 클래스와 호환되어야 한다.
  • >> 논리적으로 엄격하게 관계가 정립이 되어야 한다.

I(ISP. 인터페이스 분리 원칙)

  • 클래스는 자신이 사용하지 않는 인터페이스의 영향을 받지 않아야 한다.
  • 즉, 해당 클래스에게 무의미한 메소드의 구현을 막자는 의미
  • 따라서, 인터페이스를 너무 크게 정의하기보단 필요한 만큼만 정의하고 클래스는 입맛에 맞게 필요한 인터페이스들을 구현하도록 유도

D(DIP. 의존성 역전 원칙)

  • Java의 Spring 프레임워크나 Node.js의 Nest.js 프레임워크와 같이 웹 서버 프레임워크 내에서 많이 나오는 원칙
  • 하위 수준 모듈(구현 클래스)보다 상위 수준 모듈(인터페이스)에 의존을 해야한다

 

예를 들어서, 데이터베이스라는 클래스가 있다고 가정을 한다면

데이터베이스의 원천은 로컬 스토리지가 될 수도 있고 클라우드 스토리지가 될 수도 있다.

이 때, 데이터베이스의 원천을 로컬 스토리지 타입 혹은 클라우드 스토리지 타입으로 한정하는 것이 아니다.

그보다 상위 수준인 스토리지 타입으로 한정을 하는 것이 맞음!

interface MyStorage {
  save(data: string): void;
}

class MyLocalStorage implements MyStorage {
  save(data: string): void {
    console.log(`로컬에 저장: ${data}`);
  }
}

class MyCloudStorage implements MyStorage {
  save(data: string): void {
    console.log(`클라우드에 저장: ${data}`);
  }
}

class Database {
  // 상위 수준 모듈인 MyStorage 타입을 의존! 
  // 여기서 MyLocalStorage, MyCloudStorage 같은 하위 수준 모듈에 의존하지 않는게 핵심!
  constructor(private storage: MyStorage) {}

  saveData(data: string): void {
    this.storage.save(data);
  }
}

const myLocalStorage = new MyLocalStorage();
const myCloudStorage = new MyCloudStorage();

const myLocalDatabase = new Database(myLocalStorage);
const myCloudDatabase = new Database(myCloudStorage);

myLocalDatabase.saveData("로컬 데이터");
myCloudDatabase.saveData("클라우드 데이터");
반응형