카테고리 없음

[내일배움캠프] React 플러스 (클래스, 상속, 추상 클래스, 인터페이스, 객체 지향 설계 원칙 S.O.L.I.D, Typescript 도서관 프로그램 실습)

코찡 2023. 7. 27. 12:29

[5-1] 클래스

1. 클래스?

: 객체 지향 프로그래밍(OOP)의 핵심 구성 요소 중 하나로, 객체를 만들기 위한 틀(template)

2. 클래스의 구성 요소

1) 속성(attribute): 객체의 성질

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

3. 객체?

: 클래스 기반으로 생성되는 인스턴스(instance)

4. TypeScript에서 클래스 정의 방법

-> class의 속성 & 메서드 정의 후, new 키워드로 객체 생성

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

5. Class 접근 제한자

1) public

  • 클래스 외부에서도 접근 가능 (default)
  • 누구나 해당 클래스의 특정 기능을 사용해야 할 때

2) private

  • 클래스 내부에서만 접근 가능
  • 보통 클래스의 속성은 대부분 private으로 접근 제한자 설정
  • 클래스 속성을 조회/편집하고 싶다면 별도의 getter / setter 메서드 사용

3) protected

  • 클래스 내부 & 해당 클래스를 상속받은 자식 클래스에서만 접근 가능
// 예시 코드

class Person {
  private name: string;
  private age: number;

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

  // public은 생략 가능 (default 값이라)
  // name, age가 private로 선언되었기 때문에 별도 수정은 불가 (최초 대입값만 볼 수 있다.)
  // 만약 name, age 수정하고 싶다면 setter 추가해서 this.name, this.age 수정..
  public sayHello() {
    console.log(`안녕하세요! 제 이름은 ${this.name}이고, 나이는 ${this.age}살입니다.`);
  }
}

[5-2][5-3] 상속

1. 상속?

: 객체 지향 프로그래밍에서 클래스 간 관계를 정의하는 개념 

-> 상속을 통해 기존 클래스의 속성&메서드를 물려받은 새로운 클래스 정의 가능 (extends 키워드 사용)

// 1. 부모 class (base Class)
class Animal {
  name: string;

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

  makeSound() {
    console.log('동물 소리~');
  }
}

// 2. 자식 class -> 커스텀해서 상속받기
class Dog extends Animal {
  age: number;

  constructor(name: string) {
    super(name); // super: 부모 클래스를 참조하는 데 사용 (base Class의 생성자를 호출)
    this.age = 5;
  }

  // 메서드 "오버라이딩" -> base Class의 정의를 따르지 않고, 재정의!
  makeSound() {
    console.log('멍멍!'); // 부모의 makeSound 동작과 달라요!
  }

  eat() { // Dog 클래스만의 새로운 함수 정의
    console.log('강아지가 사료를 먹습니다.');
  }
}

// 3. 자식 class -> 그대로 상속받기
class Cat extends Animal { // Animal과 다를게 하나도 없어요!
}

// 4. 실행
const dog = new Dog('누렁이');
dog.makeSound(); // 출력: 멍멍!

const cat = new Cat('야옹이');
cat.makeSound(); // 출력: 동물 소리~

2. 서브타입, 슈퍼타입

1) 서브 타입

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

2) 슈퍼 타입

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

3) 예시

  • any: 모든 것의 슈퍼타입
  • Animal: Dog, Cat의 슈퍼타입
  • Dog, Cat: Animal의 서브타입

3. 타입 변환: upcasting, downcasting

1) upcasting: 서브타입 -> 슈퍼타입

: 암시적 타입 변환 이루어져 별도의 타입 변환 구문이 필요 없음! 단지 슈퍼타입 변수에 대입만

let dog: Dog = new Dog('또순이');
let animal: Animal = dog; // 슈퍼 타입 변수에 서브 타입 객체를 넣으면 -> upcasting 발동! 
animal.eat(); // 에러. 슈퍼타입(Animal)으로 변환이 되어 eat 메서드를 호출할 수 없어요! (eat은 Dog 클래스에만 있던 함수라)

2) downcasting: 슈퍼타입 -> 서브타입

: as 키워드를 통해 명시적 타입 변환 필요!

let animal: Animal; // 슈퍼 타입 Animal
animal = new Dog('또순이'); // 슈퍼 타입의 객체(인스턴스)로 Dog

let realDog: Dog = animal as Dog; // 타입 자체는  animal인데 Dog로 명시적 변환
realDog.eat(); // 서브타입(Dog)로 변환이 되었기 때문에 eat 메서드를 호출할 수 있죠!

[5-4] 추상 클래스

1. 추상 클래스?

: 클래스와 다르게, 인스턴스화 할 수 없는 Class ( = new 해서 생성 불가)

2. 추상 클래스의 목적

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

-> 최소한의 기본 메서드는 정의 가능하지만, 핵심 기능의 구현을 자식 class에 위임!

3. 사용 방법

-> abstract 키워드 사용, 1개 이상의 추상 함수

// 추상 클래스 생성
abstract class Shape {
  // 추상 함수 정의!!! -> 자식 니가 스스로 해결해! 
  abstract getArea(): number;
  // 얘는 그냥 함수
  printArea() {
    console.log(`도형 넓이: ${this.getArea()}`);
  }
}

// 상속받은 자식 클래스 1
class Circle extends Shape {
  // 필요한 속성 정의
  radius: number;
  // 생성자 호출
  constructor(radius: number) {
    super();
    this.radius = radius;
  }
  // 추상 함수는 반드시 구현해주어야 한다. 아니면 에러남 ! (추상 클래스 상속했으므로)
  getArea(): number { // 원의 넓이를 구하는 공식은 파이 X 반지름 X 반지름
    return Math.PI * this.radius * this.radius;
  }
}

// 상속받은 자식 클래스 2
class Rectangle extends Shape {
  // 필요한 속성 정의
  width: number;
  height: number;
  // 생성자 호출
  constructor(width: number, height: number) {
    super();
    this.width = width;
    this.height = height;
  }
  // 추상 함수는 반드시 구현해주어야 한다. 아니면 에러남 ! (추상 클래스 상속했으므로)
  getArea(): number { // 사각형의 넓이를 구하는 공식은 가로 X 세로
    return this.width * this.height;
  }
}

const circle = new Circle(5);
circle.printArea();

const rectangle = new Rectangle(4, 6);
rectangle.printArea();

[5-5] 인터페이스

1. 인터페이스?

1) 객체의 "타입 정의"

2) 객체가 가져야 하는 "속성"과 "메서드" 정의

3) 규약과 같은 것 -> 인터페이스를 구현한 객체는 반드시 인터페이스를 준수해야 함!

     -> 코드의 안정성, 유지 보수성 향상

2. 추상 클래스 VS 인터페이스

  추상 Class Interface
구현부 제공 여부 클래스의 기본 구현 제공 객체의 구조만 정의, 기본 구현X
상속 메커니즘 단일 상속 다중 상속 지원
(하나의 class가 여러 interface 구현 가능)
구현 메커니즘 추상 클래스를 상속받은 자식 클래스는
반드시 추상 함수를 구현해야 함
인터페이스를 구현하는 클래스는 인터페이스에 정의된 모든 메서드 전부 구현해야 함
언제 쓰면 좋을까? 기본 구현 제공 +상속을 통해 확장 객체가 특정 구조를 준수하도록 강제

[5-6][5-7][5-8] 객체 지향 설계 원칙 S.O.L.I.D.

1. S (SRP. 단일 책임 원칙)

1) class는 하나의 책임만 갖는다.

2) 예시

// 각각의 서비스르 독립적으로 분리해서 관리
// 예를 들어, 유저 서비스라는 class에서는 유저 관련된 액션만!

// 1. 유저 서비스
class UserService {
  constructor(private db: Database) {}

  getUser(id: number): User {
    // 사용자 조회 로직
    return this.db.findUser(id);
  }

  saveUser(user: User): void {
    // 사용자 저장 로직
    this.db.saveUser(user);
  }
}

// 2. 이메일 서비스
class EmailService {
  // 이메일 관련된 기능은 이메일 서비스에서 총괄하는게 맞습니다.
  // 다른 서비스에서 이메일 관련된 기능을 쓴다는 것은 영역을 침범하는 것이에요!
  sendWelcomeEmail(user: User): void {
    // 이메일 전송 로직
    console.log(`Sending welcome email to ${user.email}`);
  }
}

2. O (OCP. 개방 폐쇄 원칙) 

1) class: 확장에 대해서는 open & 수정에 대해서는 close

2) 즉, class의 기존 코드를 변경하지 않고도 기능을 확장할 수 있어야 한다!

3) 활용

  • 인터페이스 -> 객체의 구조가 잘 정의되어 있으니, 참고해서 확장
  • 상속 -> 부모 Class의 기본 기능 + 필요한 건 오버라이딩(재정의) 및 기능 추가

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

1) 서브타입은 기반이 되는 슈퍼타입을 대체할 수 있어야 한다.

2) 부모 Class의 기능을 수정하지 않고도, 자식 Class는 부모 Class와 호환되어야 함

3) 예시

// 추상 클래스 생성
abstract class Bird {
  abstract move(): void; // 추상 함수 (강제)
}

// 부모를 상속받은 자식 class 1
class FlyingBird extends Bird {
  move() {
    console.log("펄럭펄럭~");
  }
}

// 부모를 상속받은 자식 class 2
class NonFlyingBird extends Bird {
   move() {
    console.log("뚜벅뚜벅!");
  }
}

// 펭귄은 자식 class 2를 상속받도록 
class Penguin extends NonFlyingBird {} // 이제 위배되는 것은 아무것도 없네요!

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

1) 클래스는 자신이 사용하지 않는 인터페이스의 영향을 받지 않아야 한다. (무의미한 메소드의 구현 막기)

2) 인터페이스를 너무 크게 정의하기 보다 필요한 만큼씩만 정의

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

1) 하위 수준 모듈(구현한 클래스)보다 상위 수준 모듈(인터페이스)에 의존해야 함

2) 예시

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) {}

  // MyLocalStorage가 들어오든, MyCloudStorage가 들어오든 각각의 구현에 맞게 save 되도록
  saveData(data: string): void {
    this.storage.save(data);
  }
}

// 실행 코드에 MyLocalStorage, MyCloudStorage 만든 후
const myLocalStorage = new MyLocalStorage();
const myCloudStorage = new MyCloudStorage();

// 각각 Database라는 class의 생성자에 type을 다르게 넣어준다!
const myLocalDatabase = new Database(myLocalStorage);
const myCloudDatabase = new Database(myCloudStorage);

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

[5-9][5-10][5-11] 세 번째 TypeScript 프로그램 실습: 도서관 프로그램

1. 프로그램 기능

-> 클래스, 상속, 인터페이스를 활용한 프로그램 작성

 

  • 도서 추가 기능 - 사서
  • 도서 삭제 기능 - 사서
  • 도서 대여 기능 - 유저
  • 도서 반납 기능 - 유저

2. 프로젝트 세팅

1) 프로젝트 디렉토리 생성

2) package.json과 tsconfig.json 생성하는 명령어

// node.js -> package.json 생성
npm init -y

// typescript -> tsconfig.json 생성
tsc --init --rootDir ./src --outDir ./dist --esModuleInterop --module commonjs --strict true --allowJS true --checkJS true

3) package.json의 "scripts" 항목 아래와 같이 변경

"scripts": {
    "start": "tsc && node ./dist/index.js",
    "build": "tsc --build",
    "clean": "tsc --build --clean"
},

4) src 디렉토리 생성 후 -> src 폴더 하위에 index.ts 파일 생성

3. 도서관 프로그램 코딩

 1) Role이라는 enum을 정의

enum Role {
  LIBRARIAN, // 사서
  MEMBER, // 멤버
}

2) User라는 추상 클래스를 정의 (name, age 인자 & 추상함수 getRole)

abstract class User {
  constructor(public name: string, public age: number) {}
  abstract getRole(): Role; // Role을 반환하는 추상 함수 -> User 클래스 상속받는 자식 클래스들은 getRole 구현 필수
}

3) User 클래스를 상속받는 Member 클래스 & Librarian 클래스 정의

// 추상 클래스 User를 상속받는 Member 클래스
class Member extends User {
  constructor(name: string, age: number) {
    super(name, age);
  }
  // 추상 클래스의 추상 함수 구현 (필수)
  getRole(): Role {
    return Role.MEMBER;
  }
}
// 추상 클래스 User를 상속받는 Librarian 클래스
class Librarian extends User {
  constructor(name: string, age: number) {
    super(name, age);
  }
  getRole(): Role {
    return Role.LIBRARIAN;
  }
}

4) Book 클래스 정의 (title, author, publishDate 인자)

// Book 클래스 정의
class Book {
  constructor(
    public title: string,
    public author: string,
    public publishDate: Date
  ) {}
}

5) RentManager라는 인터페이스 정의 (도서관이 꼭 갖추어야 할 기능을 정의하는 명세서)

// 인터페이스 정의 (모든 메서드들 !)
interface RentManager {
  getBooks(): Book[]; // 현재 도서 목록 확인하는 함수
  addBook(user: User, book: Book): void; // 사서가 새로운 도서 추가할 때 호출하는 함수
  removeBook(user: User, book: Book): void; // 사서가 도서 폐기할 때 호출하는 함수
  rentBook(user: Member, book: Book): void; // 사용자가 도서 빌릴 때 호출하는 함수
  returnBook(user: Member, book: Book): void; // 사용자가 도서를 반납할 때 호출하는 함수
}

  6) RentManager를 구현하는 Library 클래스 생성

// RentManager 인터페이스를 구현하는 Library 클래스

class Library implements RentManager {
  private books: Book[] = [];
  private rentedBooks: Map<string, Book> = new Map<string, Book>(); // rentedBooks는 멤버의 대여 이력을 관리

  // books를 깊은 복사 하여 외부에서 books를 수정하는 것을 방지
  getBooks(): Book[] {
    return JSON.parse(JSON.stringify(this.books));
    // return this.books; // [얕은 복사] 이렇게 하면 getBooks를 호출받은 객체가 books를 수정 가능하다고..?
  }

  // 사서만 호출할 수 있게
  addBook(user: User, book: Book): void {
    if (user.getRole() !== Role.LIBRARIAN) {
      console.log("사서만 도서를 추가할 수 있습니다.");
      return;
    }
    this.books.push(book);
  }

  // 사서만 호출할 수 있게
  removeBook(user: User, book: Book): void {
    if (user.getRole() !== Role.LIBRARIAN) {
      console.log("사서만 도서를 삭제할 수 있습니다.");
      return;
    }
    const index = this.books.indexOf(book);
    if (index !== -1) {
      this.books.splice(index, 1);
    }
  }

  // 멤버만 호출할 수 있게
  rentBook(user: User, book: Book): void {
    if (user.getRole() !== Role.MEMBER) {
      console.log("유저만 도서를 대여할 수 있습니다.");
      return;
    }
    // 다른 책을 대여한 멤버는 책을 대여할 수 없도록
    if (this.rentedBooks.has(user.name)) {
      console.log(
        `${user.name}님은 이미 다른 책을 대여중이라 빌릴 수 없습니다.`
      );
    } else {
      this.rentedBooks.set(user.name, book);
      console.log(`${user.name}님이 [${book.title}] 책을 빌렸습니다.`);
    }
  }

  // 멤버만 호출할 수 있게
  returnBook(user: User, book: Book): void {
    if (user.getRole() !== Role.MEMBER) {
      console.log("유저만 도서를 반납할 수 있습니다.");
      return;
    }
    //도서 대여한 사람만 반납할 수 있게
    if (this.rentedBooks.get(user.name) === book) {
      this.rentedBooks.delete(user.name);
      console.log(`${user.name}님이 [${book.title}] 책을 반납했어요!`);
    } else {
      console.log(`${user.name}님은 [${book.title}] 책을 빌린적이 없어요!`);
    }
  }
}

7) 테스트 함수 코드

function main() {
  // 도서관 생성
  const myLibrary = new Library();
  // User 생성
  const librarian = new Librarian("르탄이", 30);
  const member1 = new Member("예비개발자", 30);
  const member2 = new Member("독서광", 28);

  // 도서 생성
  const book = new Book("TypeScript 문법 종합반", "강창민", new Date());
  const book2 = new Book("금쪽이 훈육하기", "오은영", new Date());
  const book3 = new Book("요식업은 이렇게!", "백종원", new Date());

  // 도서관에 도서 추가 (사서 ONLY)
  myLibrary.addBook(librarian, book);
  myLibrary.addBook(librarian, book2);
  myLibrary.addBook(librarian, book3);
  
  // 도서관에서 도서 조회
  const books = myLibrary.getBooks();
  console.log("대여할 수 있는 도서 목록:", books);

  // 도서 대여
  myLibrary.rentBook(member1, book);
  myLibrary.rentBook(member2, book2);

  // 도서 반납
  myLibrary.returnBook(member1, book);
  myLibrary.returnBook(member2, book2);
}

main();

8) 테스트 코드 실행 & 결과

npm run build
npm run start

 


cf) 추가 학습 자료

 

The starting point for learning TypeScript

Find TypeScript starter projects: from Angular to React or Node.js and CLIs.

www.typescriptlang.org

 

Handbook - The TypeScript Handbook

Your first step to learn TypeScript

www.typescriptlang.org

 

시작하기 - TypeScript Deep Dive

타입스크립트는 자바스크립트로 컴파일이 되고 자바스크립트는 브라우저 또는 서버에서 실행될 것 입니다. 그래서 다음에 정의된 목록이 타입스크립트를 시작하는데 필요할 것 입니다.

radlohead.gitbook.io