TIL | 클래스

·2023년 7월 28일

TIL # WIL

목록 보기
38/65

23.07.28

5-1 클래스

  1. 클래스 : 객체를 만들기 위한 틀, 객체들이 공통으로 가지는 속성(attribute)과 메서드(method)를 정의
    ex. 붕어빵 틀

그렇다면 여기서 속성과 메서드란 ?

속성 : 객체의 성질을 결정하는 것
ex. 팥 붕어빵 - 팥이란 속성을 가짐, 슈크림 붕어빵 - 슈크림이란 속성을 가짐

메서드 : 객체의 성질을 변화시키거나 객체에서 제공하는 기능들을 사용
ex. 붕어빵 주인은 붕어빵을 팥붕에서 슈붕으로 전환할 수 있음
손님들은 붕어빵의 가격을 알 수 있음

  1. 객체 : 클래스를 통해 생성되는 것, 인스턴스(instance)라고도 함
    ex. 붕어빵
  1. 클래스 및 객체 정의 방법
  • 클래스 정의 : 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();

  • 생성자(constructor) = (인스턴스(객체)를 생성하고 초기화하는데 사용되는) 메소드 !!!
    ex. 틀에 반죽과 팥 등을 넣어서 붕어빵을 만드는 과정
  1. 인스턴스를 생성할때 자동 실행
  2. constructor라는 이름으로 정의
  3. 클래스 내에서 오직 하나만 존재 가능
  • 클래스 접근 제한자

접근 제한자가 선언이 안되어있다면 default는 public

  1. public : 외부에서도 접근 가능한 접근 제한자 => 즉, 민감하지 않은 객체 정보를 열람할 때 사용
  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 sayHello() {
console.log(안녕하세요! 제 이름은 ${this.name}이고, 나이는 ${this.age}살입니다.);
}
}

https://velog.io/@hhjj0513/TIL-JavaScript-%EC%8A%A4%EC%BD%94%ED%94%84-%EC%8B%A4%ED%96%89-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8-Promise-async-await-%EA%B0%9D%EC%B2%B4-

5-2 ~ 5-3 상속

  1. 상속 : 기존 클래스의 속성과 메서드를 물려받아 새로운 클래스를 정의하는 것, 상속을 함으로써 똑같은 코드를 반복적으로 작성할 필요 없음

사용법 => class 자식 클래스 extends 기존 클래스 !!!

  • 메서드 오버라이딩 : 기존 클래스의 메서드를 따르지않고 새롭게 메서드를 ‘재정의’하는 것
  • super : 자식 클래스가 부모 클래스를 참조하는데 사용하는 키워드

  • 즉, 자식 클래스에서 생성자를 정의할 때 부모 클래스의 생성자를 호출해야 하는데 이 때 씁니다!

  • 자식 클래스가 부모 클래스의 생성자나 메서드를 그대로 사용하고 싶다면 자식 클래스에선 다시 작성하지 않아도 됨! → Cat 클래스를 보세요!

=> 아래에서는 Dog 클래스에 age라는 속성이 추가되었기 때문에 super 키워드를 사용함

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('멍멍!'); // 부모의 makeSound 동작과 달라요!
}

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

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

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

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

  1. 서브타입 vs 슈퍼타입
  • any, unknown는 모든 것의 슈퍼타입이에요.
  • Animal은 Dog, Cat의 슈퍼타입이고요. Dog, Cat은 Animal의 서브타입이에요!
  1. upcasting vs downcasting
  • upcasting과 downcasting은 슈퍼타입, 서브타입으로 변환할 수 있는 타입 변환 기술 (형변환)
  • upcasting : 서브타입 -> 슈퍼타입 / 타입 변환은 암시적으로 이루어져 별도의 타입 변환 구문이 필요가 없어요! / TS가 자동으로 해줌 / 단지 슈퍼타입 변수에 대입만 해주면 됨

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

그렇다면 upcasting을 ꖶዞ 사용해야할까 ?

  • 예를 들어, Dog, Cat, Lion 그리고 기타 등등 다양한 동물을 인자로 받을 수 있는 함수를 만들고 싶다면?
    • 올바른 선택: 아! Animal 타입의 객체를 받으면 모두 다 받을 수 있겠구나!
    • 잘못된 선택: 아! union으로 새로운 타입을 만들어서 해당 타입의 객체를 받게해야겠구나!
  • downcasting : 슈퍼타입 -> 서브타입 / as 키워드로 명시적으로 타입 변환을 해줘야 함

let animal: Animal;
animal = new Dog('또순이');

let realDog: Dog = animal as Dog;
realDog.eat(); // 서브타입(Dog)로 변환이 되었기 때문에 eat 메서드를 호출

5-4 추상클래스

  1. 추상클래스 : 클래스와 달리 인스턴스화를 할 수 없는 클래스

(목적)

  • 상속을 통해 자식 클래스에서 메서드를 제각각 구현하도록 강제를 하는 용도 => 즉, 기본적인 메서드를 제외하고 핵심 기능의 구현을 자식 클래스에게 위임을 하는 클래스
  • abstract 키워드 사용
  • 1개 이상의 추상 함수가 필요

=> 즉, 추상 클래스를 상속 받은 자식 클래스들은 반드시 추상 함수를 구현해야함 , 그렇지 않으면 오류 !

5-5 인터페이스 interface

  1. 인터페이스 : TS에서 객체의 타입을 정의하는데 사용
    => 즉, 객체가 가져야 하는 속성과 메서드를 정의
    => 인터페이스 === 규약 !!!! 즉, 인터페이스를 구현한 객체는 인터페이스를 준수해야함
    => 사용하는 이유는 안정성과 유지 보수성 향상 (why ? 규약이기 때문에 어길 수 없어서, 어기면 에러 발생)
  1. 인터페이스 vs 추상 클래스
  • 구현부 제공 여부
    인 : ‘객체’의 구조만을 정의, 기분 구현 제공 안함
    추 : ‘클래스’의 기본 구현 제공

  • 상속 메커니즘
    인 : 다중 상속(보다는 인터페이스)을 지원, (=> 즉 하나의 클래스에서 여러 개의 인터페이스 구현 가능)
    추 : 단일 상속을 지원

  • 구현 메커니즘
    인 : 인터페이스 를 구현하는 클래스는 인터페이스 정의된 모든 메서드를 전부 구현
    추 : 추상 클래스를 상속받은 자식 클래스는 반드시 추상 함수를 구현

인터페이스 : 객체가 완벽하게 특정 구조를 준수하도록 강제하고자 할 때 사용하면 적합
추상 클래스 : 기본 구현을 제공하고 상속을 통해 확장하고자 할 때 사용하면 적합

5-6 객체 지향 설계원칙 - S.O.L.I.D
객체 지향 설계를 할 때는 SOLID 원칙을 따르는게 필수 => 이렇게 설계하게 되면 프로그램이 유연, 확장 가능, 이해하기 쉬운 구조

  1. S(SRP 단일 책임 원칙) ⭐️🔆
  • 클래스는 하나의 책임만 가져야 한다는 매우 기본적이고 중요한 원칙
    ex. 유저 서비스라는 클래스에서는 유저 관련된 액션만 해야되고 다른 액션을 해서는 안됨

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

  1. O(OCP 개방 폐쇄 원칙)
  • 확장에 대해서는 열려있어야하고 수정에 대해서는 닫혀있어야 함
  • 즉, 클래스의 기존 코드를 변경하지 않고도 기능을 확장할 수 있어야 함 => 인터페이스 or 상속을 통해 가능 !!!!!!!!!!!!!!!!!!!!
  1. L(LSP 리스코프 치환 원칙)
  • 서브타입은 기반이 되는 슈퍼타입을 대체할 수 있어야 한다는 원칙 => 즉, 자식 클래슨느 부모 클래스의 기능을 수정하지 않고도 부모 클래스와 호환되어야 함 => 한마디로 논리적으로 엄격하게 관계가 정립되어야 함

논리적으로 제대로 된 구조인가 !!!!
올바르게 성립이 되는가 !!!
reasonable한가 !!!

abstract class Bird {
abstract move(): void;
}

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

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

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

  1. I(ISP 인터페이스 분리 원칙)
  • 클래스는 자신이 사용하는 인터페이스의 영향을 받지 않아야 함 => 즉, 해당 클래스에게 무의미한 메소드의 구현을 막자는 의미
  • 따라서, 인터페이스는 필요한 만큼만 정의하고 클래스는 입맛에 맞게 필요한 인터페이스들을 구현하도록 유도
  1. D(DIP 의존성 역전 원칙) : 웹 서버 프레임워크(ex. Java의 Spring 프레임워크, Node.js의 Nest.js 프레임워크) 내에서 많이 나오는 원칙
  • 하위 수준 모듈(구현 클래스)보다 상위 수준 모듈(인터페이스)에 의존을 해야한다는 의미
    ex. 데이터베이스라는 클래스가 있다고 가정을 해보겠습니다. 데이터베이스의 원천은 로컬 스토리지가 될 수도 있고 클라우드 스토리지가 될 수도 있어요. 이 때, 데이터베이스의 원천을 로컬 스토리지 타입 혹은 클라우드 스토리지 타입으로 한정하는 것이 아닙니다. 그보다 상위 수준인 스토리지 타입으로 한정을 하는 것이 맞아요!

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("클라우드 데이터");

5-9 ~ 5-11 실습 !

interface => initialState

✅ 제대로 이해했는지 체크해봅시다.
  • (4주차) enum, object literal의 차이점을 설명할 수 있다.
  • (4주차) 각 유틸리티 타입(Partial, Required …)에 대해 설명할 수 있으며, 이를 이용하여 복잡한 타입을 쉽게 조작하는 코드를 작성할 수 있다.
  • (5주차) 클래스, 상속, 추상 클래스의 인터페이스의 개념에 대해 설명할 수 있다.
  • (5주차) 객체지향 설계원칙인 SOLID를 설명할 수 있다.

0개의 댓글