Mission: 타입 스크립트 개념 정리
클래스
클래스와 객체
- 클래스는 객체 지향 프로그래밍(OOP)의 핵심 구성 요소 중 하나이며, 객체를 만들기 위한 틀(template)이다.
- 클래스는 객체들이 공통으로 가지는 속성(attribute)과 메서드(method)를 정의한다.
- 속성은 객체의 성질을 결정하며, 예를 들어, 팥 붕어빵과 슈크림 붕어빵은 각각 팥과 슈크림이란 속성을 가질 수 있다.
- 메서드는 객체의 성질을 변화시키거나 객체에서 제공하는 기능들을 사용하는 창구이다.
클래스 정의하기- TypeScript에서 클래스를 정의하기 위해 class 키워드를 사용한다.
- 클래스의 속성과 메서드를 정의하고, new 키워드를 사용하여 객체를 생성할 수 있다.
클래스 접근 제한자- 클래스에서 속성과 메서드에 접근 제한자를 사용해 접근을 제한할 수 있다.
TypeScript에서 제공하는 접근 제한자
public, private, protected
- public: 클래스 외부에서도 접근 가능한 접근 제한자로 기본 설정이다.
- private: 클래스 내부에서만 접근 가능한 접근 제한자로, 외부에서 직접적으로 객체의 속성을 변경할 수 없게 한다.
- protected: 클래스 내부와 해당 클래스를 상속받은 자식 클래스에서만 접근 가능한 접근 제한자이다.
// 클래스 예시 코드 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();
상속
상속이란?
- 상속은 객체 지향 프로그래밍에서 클래스 간의 관계를 정의하는 개념으로, 기존 클래스의 속성과 메서드를 물려받아 새로운 클래스를 정의하는 것이다.
- extends 키워드를 사용하여 상속을 구현한다.
서브타입, 슈퍼타입- 서브타입과 슈퍼타입은 타입 간의 관계를 나타낸다.
- 서브타입은 다른 타입의 서브클래스로 어디든 해당 타입의 객체를 안전하게 사용할 수 있다.
- 슈퍼타입은 다른 타입의 슈퍼클래스로 어디든 해당 타입을 안전하게 사용할 수 있다.
- any는 모든 것의 슈퍼타입이며, Animal은 Dog, Cat의 슈퍼타입이고, Dog, Cat은 Animal의 서브타입이다.
upcasting, downcasting- upcasting은 서브타입을 슈퍼타입으로 변환하는 것을 의미한다.
- 서브타입 객체를 슈퍼타입으로 다루면 유연하게 활용할 수 있다.
- downcasting은 슈퍼타입을 서브타입으로 변환하는 것을 의미하며, 명시적인 타입 변환(as 키워드)이 필요하다.
// 상속 예시 코드 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(); // 출력: 동물 소리~
추상 클래스
추상 클래스란?
- 추상 클래스는 클래스와는 다르게 인스턴스화할 수 없는 클래스이다.
- 추상 클래스의 주요 목적은 상속을 통해 자식 클래스에서 메서드를 구현하도록 강제하는 것이다.
- 추상 함수를 가질 수 있으며, 최소한의 기본 메서드를 정의할 수도 있다.
추상 클래스 사용 방법- 추상 클래스 및 추상 함수는 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(); // 출력: 도형 넓이: 78.53981633974483 const rectangle = new Rectangle(4, 6); rectangle.printArea(); // 출력: 도형 넓이: 24
인터페이스
인터페이스란?
- 인터페이스는 TypeScript에서 객체의 타입을 정의하는데 사용된다.
- 인터페이스는 객체가 가져야 하는 속성과 메서드를 정의한다.
- 인터페이스를 구현한 객체는 인터페이스를 반드시 준수해야하며, 이를 통해 코드의 안정성과 유지 보수성을 향상시킬 수 있다.
추상 클래스와 인터페이스 차이
- 구현부 제공 여부
추상 클래스는 클래스의 기본 구현을 제공한다.
인터페이스는 객체의 구조만을 정의하고 기본 구현을 제공하지 않는다.- 상속 메커니즘
추상 클래스는 단일 상속만 지원한다.
인터페이스는 다중 상속을 지원한다.
즉, 하나의 클래스는 여러 인터페이스를 구현할 수 있다.- 구현 메커니즘
추상 클래스를 상속받은 자식 클래스는 반드시 추상 함수를 구현해야 한다.
인터페이스를 구현하는 클래스는 인터페이스에 정의된 모든 메서드를 전부 구현해야 한다.
언제 쓰면 좋을까?
- 추상 클래스는 기본 구현을 제공하고 상속을 통해 확장하는데 초점을 맞추고 싶을 때 사용한다.
- 객체가 완벽하게 특정 구조를 준수하도록 강제하고 싶을 때는 인터페이스를 사용한다.
객체 지향 설계 원칙 - S.O.L.I.D
S(SRP. 단일 책임 원칙)
- 클래스는 하나의 책임만 가져야 한다는 원칙이다.
- 하나의 클래스는 한 가지 역할에 집중하여 유지 보수성을 높인다.
- 클래스의 기능을 분리하여 적절한 책임을 갖도록 한다.
O(OCP. 개방 폐쇄 원칙)- 클래스는 확장에 대해서는 열려 있어야 하고 수정에 대해서는 닫혀 있어야 한다.
- 기존 코드를 변경하지 않고도 기능을 확장할 수 있도록 설계한다.
- 인터페이스나 상속을 통해 기능을 확장하는 방법을 사용한다.
L(LSP. 리스코프 치환 원칙)- 서브타입은 기반이 되는 슈퍼타입을 대체할 수 있어야 한다는 원칙이다.
- 자식 클래스는 부모 클래스와 호환되어야 하며, 부모 클래스의 기능을 수정하지 않고 확장한다.
I(ISP. 인터페이스 분리 원칙)- 클래스는 자신이 사용하지 않는 인터페이스의 영향을 받지 않아야 한다.
- 필요한 인터페이스만 정의하여 클래스의 의존성을 최소화하고 무의미한 메소드의 구현을 막는다.
D(DIP. 의존성 역전 원칙)- 하위 수준 모듈(구현 클래스)보다 상위 수준 모듈(인터페이스)에 의존해야 한다는 원칙이다.
- 상위 수준 모듈에 의존하여 유연하고 확장 가능한 설계를 구현한다.
// 예시 1: 인터페이스 정의 interface IUser { id: number; name: string; email: string; getInfo(): string; } // 예시 2: 인터페이스 구현 class User implements IUser { constructor(public id: number, public name: string, public email: string) {} getInfo(): string { return `User ID: ${this.id}, Name: ${this.name}, Email: ${this.email}`; } } const user1 = new User(1, "John Doe", "john@example.com"); console.log(user1.getInfo()); // 예시 3: S.O.L.I.D 원칙 적용 예시 // 단일 책임 원칙(SRP) 예시 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}`); } } // 개방 폐쇄 원칙(OCP) 예시 interface IStorage { save(data: any): void; } class LocalStorage implements IStorage { save(data: any): void { // 로컬 스토리지에 데이터 저장하는 로직 console.log("Saving data to local storage:", data); } } class CloudStorage implements IStorage { save(data: any): void { // 클라우드 스토리지에 데이터 저장하는 로직 console.log("Saving data to cloud storage:", data); } } // 리스코프 치환 원칙(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 {} // 인터페이스 분리 원칙(ISP) 예시 interface IEmailService { sendWelcomeEmail(user: User): void; } class EmailService implements IEmailService { sendWelcomeEmail(user: User): void { console.log(`Sending welcome email to ${user.email}`); } } // 의존성 역전 원칙(DIP) 예시 class UserService { constructor(private storage: IStorage) {} saveUser(user: User): void { // 사용자 저장 로직 this.storage.save(user); } } const localDb = new LocalStorage(); const cloudDb = new CloudStorage(); const user2 = new User(2, "Jane Smith", "jane@example.com"); const userService1 = new UserService(localDb); const userService2 = new UserService(cloudDb); userService1.saveUser(user2); userService2.saveUser(user2);