230728 객체 지향 프로그래밍

나윤빈·2023년 7월 28일
0

TIL

목록 보기
26/55

📌 Class (클래스)

1. 클래스란?

  • 클래스는 객체 지향 프로그래밍(OOP)의 핵심 구성 요소 중 하나로 객체를 만들기 위한 틀(template)이다.

2. 클래스의 구성 요소

  • 속성(attribute) : 객체의 성질을 결정한다.
  • 메서드(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. 생성자(constructor)

  • 생성자는 클래스의 인스턴스를 생성하고 초기화 하는데 사용되는 특별한 메서드이다.
  • 클래스 내에서 constructor 이라는 이름으로 정의된다.
  • 인스턴스를 생성할 때 자동 호출되며, 클래스 내에서 오직 하나만 존재할 수 있다.
  • 생성자는 객체 속성을 초기화 하는 것 뿐만 아니라 객체가 생성 될 때 (초기화 할 때) 작동이 되면 좋은 로직들을 추가하여 미리 실행시킬 수 있다.

6. 클래스 접근 제한자

  • 클래스에서는 속성과 메서드에 접근 제한자를 사용해 접근을 제한할 수 있다.

  • public

    • 클래스 외부에서도 접근이 가능한 접근 제한자로 제한자가 선언 되어 있지 않다면 기본적으로 접근 제한자는 public 이다.
    • 클래스의 함수 중 민감하지 않은 객체 정보를 열람할 때나 누구나 해당 클래스의 특정 기능을 사용해야 할 때 많이 쓰인다.
  • private

    • 클래스 내부에서만 접근이 가능한 접근 제한자로 클래스의 속성은 대부분 private으로 접근 제한자를 설정한다.
    • 즉, 외부에서 직접적으로 객체의 속성을 변경할 수 없도록 제한하는 것이다.
    • 클래스의 속성을 보거나 편집하고 싶을 땐 별도의 getter/setter 메서드를 준비하는 것이 관례이다.
  • 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(상속)

1. 상속이란?

  • 상속은 객체 지향 프로그래밍에서 클래스 간의 관계를 정의하는 개념이다.
  • 상속을 통해 기존 클래스의 속성과 메서드를 물려받아 새로운 클래스를 정의할 수 있다.
  • 상속으로 인해 같은 코드를 반복적으로 작성할 필요가 없어진다.
  • 상속의 구현은 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('멍멍!'); // 부모의 makeSound 동작과 다름
  }

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

class Cat extends Animal { // Animal과 다를게 하나도 없음
}

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

const cat = new Cat('야옹이');
cat.makeSound(); // 출력: 동물 소리~
  • 여기서 Animal 을 부모 클래스, Dog, Cat 를 자식 클래스라고 한다.
  • super 키워드는 자식 클래스가 부모 클래스를 참조하는데 사용하는 키워드이다.
  • 즉, 자식 클래스에서 생성자를 정의할 때 부모 클랙스의 생성자를 호출해야 하는데 이때 사용하는 키워드 이다.
  • Cat 클래스처럼 자식 클래스가 부모 클래스의 생성자나 메서드를 그대로 사용할 경우 자식 클래스에서는 생성자나 매서드를 다시 작성할 필요가 없다.
  • Dog 클래스는 부모의 makeSound 함수의 동작을 새롭게 정의하고 있는데, 이를 오버라이팅 이라고 한다.
  • AnimalDog, Cat의 슈퍼타입이고, Dog, CatAnimal 의 서브타입이다.

2. 서브타입과 슈퍼타입

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

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

  • any 는 모든 것의 슈퍼타입이다.

3. upcasting과 downcasting

1) upcasting

  • 서브타입 → 슈퍼타입으로 변환하는 것을 말한다.
  • upcasting의 경우 타입 변환은 암시적으로 이루어지기 때문에 별도의 타입 변환 구문이 필요하지 않다.
  • 즉, 슈퍼타입 변수에 대입만 해도 TypeScript가 자동으로 타입 변환 해준다.
  • 서브타입 객체를 슈퍼타입 객체로 다루면 유연하게 활용할 수 있다.
  • 예를 들어 Dog, Cat 등 다양한 동물을 인자로 받을 수 있는 함수를 만들고 싶다면, union으로 새로운 타입을 만들어 해당 타입의 객체를 받는 것이 아니라, Animal 타입의 객체를 받으면 모두를 받을 수 있다.
let dog: Dog = new Dog('또순이');
let animal: Animal = dog; // upcasting 발동! 
animal.eat(); // 에러. 슈퍼타입(Animal)으로 변환이 되어 eat 메서드를 호출할 수 없음

2) downcasting

  • 슈퍼타입 → 서브타입으로 변환하는 것을 말한다.
  • downcasting의 경우 as 키워드로 명시적 타입 변환을 해줘야 한다.
  • downcasting을 할 일은 많지 않지만, 필요한 경우에 Dog 와 같은 서브타입 메서드를 사용해야 될 때 변신 해야 될 수 있다.
let animal: Animal;
animal = new Dog('또순이');

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

📌 추상 클래스

1. 추상 클래스란?

  • 클래스와 다르게 인스턴스화 할 수 없는 클래스이다.
  • 추상 클래스의 목적은 상속을 통해 자식 클래스에서 메서드를 제각각 구현하도록 강제 하는 용도이다.
  • 추상 클래스도 최소한의 기본 메서드는 정의할 수 있으나, 핵심 기능의 구현은 전부 자식 클래스에게 위임하는 것이다.

2. 사용 방법

  • 추상 클래스 및 추상 함수는 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;
  }
}

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();
  • abstract getArea(): number; 가 바로 추상 함수이다.
  • 이 추상 클래스를 상속 받은 자식 클래스(Circle, Rectangle)는 반드시 getArea 함수를 구현해야 한다.

📌 인터페이스

1. 인터페이스란?

  • TypeScript에서 객체의 타입을 정의하는데 사용된다.
  • 객체가 가져야 하는 속성과 메서드를 정의한다.
  • 인터페이스를 구현한 객체는 인터페이스를 반드시 준수한다.
  • 인터페이스를 사용하면 코드의 안정성을 높이고 유지 보수성을 향상 시킬 수 있다.

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

  • 구현부 제공 여부 : 추상 클래스는 클래스의 기본 구현을 제공하지만, 인터페이스는 객체의 구조만을 정의하고 기본 구현을 제공하지 않는다.
  • 상속 메커니즘 : 추상 클래스는 단일 상속만 지원하지만, 인터페이스는 다중 상속을 지원한다. 즉, 하나의 클래스는 여러 인터페이스를 구현할 수 있다.
  • 구현 메커니즘 : 추상 클래스를 상속받은 자식 클래스는 반드시 추상 함수를 구현해야 하고, 인터페이스를 구현하는 클래스는 인터페이스에 정의된 모든 메서드를 전부 구현해야 한다.

💡 기본 구현을 제공하고 상속을 통해 확장하는데 초점을 맞추고 싶다면 추상 클래스를, 객체가 완벽하게 특정 구조를 준수하도록 강제하고 싶다면 인터페이스를 사용하는 것이 적합하다.

📌 객체 지향 설계 원칙 - S.O.L.I.D

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

  • 클랙스는 하나의 책임만 가져야 한다는 매우 기본적인 원칙이다.
  • 특히, 5개의 설계 원칙 중 가장 기본적이고 중요한 원칙이다.
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}`);
  }
}

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

  • 클래스는 확장에 대해서는 열려 있어야 하고 수정에 대해서는 닫혀있어야 한다는 원칙이다.
  • 클래스의 기존 코드를 변경하지 않고도 기능을 확장할 수 있어야 한다.
  • 즉, 인터페이스나 상속을 통해서 이를 해결할 수 있다.

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

  • 서브타입은 기반이 되는 슈퍼타입을 대체할 수 있어야 한다는 원칙이다.
  • 자식 클래스는 부모 클랫의 기능을 수정하지 않고도 부모 클래스와 호환되어야 한다.
  • 즉, 논리적으로 엄격하게 관계가 정립 되어야 한다.
abstract class Bird {
  abstract move(): void;
}

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

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

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

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

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

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

  • DIP는 Java의 Sprong 프레임워크나 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("클라우드 데이터");
profile
프론트엔드 개발자를 꿈꾸는

0개의 댓글