[2024.06.26 TIL] 내일배움캠프 50일차 (TypeScript 종합 강의, 객체지향프로그래밍, 클래스, 상속, 추상클래스, 인터페이스, SOLID)

My_Code·2024년 6월 26일
0

TIL

목록 보기
65/112
post-thumbnail

본 내용은 내일배움캠프에서 활동한 내용을 기록한 글입니다.


💻 TIL(Today I Learned)

📌 Today I Done

✏️ 클래스(class)란?

  • 클래스는 객체를 만들기 위한 설계도

  • 같은 종류의 객체가 공통으로 가지는 속성과 메서드를 정의

    • 속성은 객체의 성질 (변수)
    • 메서드는 객체의 성질을 변화시키거나 객체에서 제공하는 기능을 사용하기 위한 도구
  • 인스턴스는 클래스를 통해 생성된 객체를 의미


✏️ 클래스 사용 기본 예제

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

✏️ 접근 제한자

  • public : 클래스 외부에서도 접근 가능한 접근 제한자

  • private : 클래스 내부에서만 접근 가능한 접근 제한자

  • protected : 클래스 내부와 상속받은 자식 클래스에서만 접근 가능한 접근 제한자

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

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

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

✏️ 상속(inheritance)이란?

  • 기존 클래스의 속성과 메서드를 물려받아 새로운 클래스를 정의 가능

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

class Animal {
  name: string;

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

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

class Dog extends Animal {
  age: number;

  constructor(name: string) {
    super(name); // 부모 클래스를 참조하는데 사용하는 키워드
    this.age = 5;
  }

  makeSound() {
    console.log('멍멍!'); // 오버라이딩
  }

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

class Cat extends Animal { // Animal과 다를게 하나도 없어요!
}

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

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

✏️ upcasting, downcasting

  • 부모 클래스와 부모 클래스를 상속받은 자식 클래스를 슈퍼타입, 서브타입이라고 부름

  • upcasting은 서브타입을 슈퍼타입으로 변환하는 것을 의미

  • 아래 코드에서도 단지 슈퍼타입 변수에 대입만 했을 뿐임

let dog: Dog = new Dog('또순이');
let animal: Animal = dog; // upcasting 발동! 
animal.eat(); // 에러. 슈퍼타입(Animal)으로 변환이 되어 eat 메서드를 호출할 수 없어요! 
  • downcasting은 슈퍼타입을 서브타입으로 변환하는 것을 의미

  • 이 경우에는 as 키워드로 명시적으로 타입 변환을 해줘야 함

  • 쓸일이 생각보다 없음


✏️ 추상 클래스

  • 추상 클래스는 클래스와 다르게 인스턴스화 할 수 없는 클래스

  • 단지 클래스의 구조를 보여주기 위한 클래스

  • 추상 클래스의 목적은 상속을 통해 자식 클래스에서 메서드를 제각각 구현하는 것을 강제하기 위한 것

  • 즉, 핵심 기능의 구현은 전부 자식 클래스에게 맡기는 것

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;
  }
}

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

✏️ 인터페이스

  • 인터페이스는 TypeScript에서 객체의 타입을 정의하는데 사용

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

  • 즉, 객체의 구조만 정의하고 기본 구현을 제공하지 않음

  • 일종의 규약이기 때문에 인터페이스를 implements 하는 클래스는 반드시 인터페이스의 양삭을 지켜야 함

  • 다중 상속 가능하고, 하나의 클래스는 여러 인터페이스를 implements 할 수 있음


✏️ 객체 지향 설계 원칙 S(SRP. 단일 책임 원칙) → ⭐ 매우 중요 ⭐

  • 클래스는 하나의 책임만 가져야 함

  • 가장 기본적이고 중요한 원칙

  • 예를 들면 사용자 클래스에서 게시물을 관리하는 작업이 있으면 안됨

  • 잘못된 사례

class UserService {
  constructor(private db: Database) {}

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

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

  sendWelcomeEmail(user: User): void {
    // 갑분 이메일 전송 로직이 여기 왜?
    const emailService = new EmailService();
    emailService.sendWelcomeEmail(user);
  }
}
  • 올바른 사례
class UserService {
  constructor(private db: Database) {}

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

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

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

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

  • 클래스 확장에 대해서는 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다는 원칙

  • 기존 코드를 변경하지 않고도 기능을 확장할 수 있어야 함

  • 즉, 부모 클래스와 자식 클래스가 있을 때, 부모 클래스(원본)을 수정하지 못하도록 해야 한다는 의미


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

  • 서브타입은 기반이되는 슈퍼타입을 대체할 수 있어야 한다는 원칙

  • 즉, 논리적으로 엄격하게 관계가 정립되어야 함

  • 잘못된 사례

class Bird {
  fly(): void {
    console.log("펄럭펄럭~");
  }
}

class Penguin extends Bird {
  // 으잉? 펭귄이 날 수 있나요? 펭귄이 펄럭펄럭~ 한다는 것은 명백한 위반이죠.
}
  • 올바른 사례
abstract class Bird {
  abstract move(): void;
}

class FlyingBird extends Bird {
  move() {
    console.log("펄럭펄럭~");
  }
}

class NonFlyingBird extends Bird {
   move() {
    console.log("뚜벅뚜벅!");
  }
}

class Penguin extends NonFlyingBird {} // 이제 위배되는 것은 아무것도 없네요!

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

  • 해당 클래스에게 무의미한 메소드의 구현을 막자는 의미

  • 즉, 인터페이스 구현 시 필요한 만큼만 정의하고 클래스에 맞게 필요한 인터페이스을 구현해야 함


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

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

  • 의존 관계를 맺을 때 변화하기 쉬운 것보다는 변화하기 어려운 것, 거의 변화가 없는 것에 의존하라는 것

  • 의존성 역전 원칙을 위반한 경우:

    • 엔진 모듈(고수준 모듈)이 브레이크 모듈(저수준 모듈)에 직접 의존하고 있음
    • 이 경우 엔진 모듈이 브레이크 모듈의 구체적인 구현 방식에 종속되게 됨
    • 만약 브레이크 모듈의 구현이 변경되면, 엔진 모듈도 영향을 받게 됨
  • 의존성 역전 원칙을 적용한 경우:

    • 듈과 브레이크 모듈 모두 "자동차 부품 인터페이스"라는 추상화에 의존하게 됨
    • 엔진 모듈과 브레이크 모듈은 서로 독립적으로 개발될 수 있음
    • 추상화인 "자동차 부품 인터페이스"가 구체적인 구현 세부 사항에 의존하지 않음
    • 이렇게 함으로써 모듈 간의 결합도가 낮아지고, 변경에 대한 영향을 최소화할 수 있음
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("클라우드 데이터");

✏️ 도서관 프로그램 만들기 실습

enum Role {
  LIBRARIAN,
  MEMBER,
}

// 사용자 클래스
abstract class User {
  constructor(public name: string, public age: number) {
    this.name = name;
    this.age = age;
  }

  abstract getRole(): Role;
}

// 멤버 클래스
class Member extends User {
  constructor(name: string, age: number) {
    super(name, age);
  }

  getRole(): Role {
    return Role.MEMBER;
  }
}

// 사서 클래스
class Librarian extends User {
  constructor(name: string, age: number) {
    super(name, age);
  }

  getRole(): Role {
    return Role.LIBRARIAN;
  }
}

// 책 클래스
class Book {
  constructor(public title: string, public author: string, public date: Date) {
    this.title = title;
    this.author = author;
    this.date = date;
  }
}

// RentManager 인터페이스
interface RentManager {
  getBooks(): Book[]; // 도서 목록 조회
  addBook(user: User, book: Book): void; // 새로운 도서 추가 (사서)
  removeBook(user: User, book: Book): void; // 기존 도서 폐기 (사서)
  rentBook(member: Member, book: Book): void; // 도서 대여 (멤버)
  returnBook(member: Member, book: Book): void; // 도서 반납 (멤버)
}

class Library implements RentManager {
  private books: Book[] = [];
  private rentList: Map<string, Book> = new Map<string, Book>();

  // 도서 목록 조회
  getBooks(): Book[] {
    // 깊은 복사를 통해 외부에서 books를 수정하는 것을 방지
    return JSON.parse(JSON.stringify(this.books));
  }

  // 새로운 도서 추가
  addBook(user: User, book: Book) {
    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(member: Member, book: Book): void {
    if (member.getRole() !== Role.MEMBER) {
      console.log('멤버만 사용 가능합니다.');
      return;
    }

    if (this.rentList.has(member.name)) {
      console.log(`${member.name}님은 이미 다른 책을 대여중입니다.`);
    } else {
      this.rentList.set(member.name, book);
      console.log(`${member.name}님이  [${book.title}] 책을 빌렸습니다.`);
    }
  }

  // 도서 반납
  returnBook(member: Member, book: Book): void {
    if (member.getRole() !== Role.MEMBER) {
      console.log('멤버만 사용 가능합니다.');
      return;
    }

    if (this.rentList.has(member.name)) {
      console.log(`${member.name}님이  [${book.title}] 책을 반납했습니다.`);
      this.rentList.delete(member.name);
    } else {
      console.log(
        `${member.name}님이  [${book.title}] 책을 빌리지 않았습니다.`
      );
      return;
    }
  }
}

function main() {
  const myLibrary = new Library();
  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());

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

// 출력 결과
/*
대여할 수 있는 도서 목록: [
  {
    title: 'TypeScript 문법 종합반',
    author: '강창민',
    date: '2024-06-26T11:18:24.768Z'
  },
  {
    title: '금쪽이 훈육하기',
    author: '오은영',
    date: '2024-06-26T11:18:24.768Z'
  },
  {
    title: '요식업은 이렇게!',
    author: '백종원',
    date: '2024-06-26T11:18:24.768Z'
  }
]
예비개발자님이  [TypeScript 문법 종합반] 책을 빌렸습니다.
독서광님이  [금쪽이 훈육하기] 책을 빌렸습니다.
예비개발자님이  [TypeScript 문법 종합반] 책을 반납했습니다.
독서광님이  [금쪽이 훈육하기] 책을 반납했습니다.
*/


📌 Tomorrow's Goal

✏️ Nest.js 강의 시청

  • 오늘 특강, 면담, 질문, 강의 정리 등으로 생각보다 시간이 너무 잘 갔음

  • 그래서 사실 Nest.js 강의는 조금 밖에 시청하지 못함

  • 내일과 모레에 걸쳐서 Nest.js 강의를 끝내보는 것을 목표로 할 예정

  • 그래야 주말부터 타입스크립트 개인과제를 진행할 수 있기에 Nest.js를 최대한 학습할 예정



📌 Today's Goal I Done

✔️ TypeScript 문법 종합 강의 시청

  • 오늘은 TypeScript 문법 종합 강의 나머지를 모두 시청함

  • 객체 지향 프로그래밍에 대한 내용은 예전 강의에서 학습했지만 막상 타입스크립트를 적용한 예제를 보니 생각처럼 눈에 들어오지 않았음

  • 그래도 도서관 프로그램 만들기 실습을 진행하니 타입, 클래스, 인터페이스가 어느정도 이해됨

  • 타입스크립트 강의는 끝났지만 Nest.js 강의를 통해서 타입스크립트에 대한 공부는 이어갈 예정


profile
조금씩 정리하자!!!

0개의 댓글