본 내용은 내일배움캠프에서 활동한 내용을 기록한 글입니다.
클래스는 객체를 만들기 위한 설계도
같은 종류의 객체가 공통으로 가지는 속성과 메서드를 정의
인스턴스는 클래스를 통해 생성된 객체를 의미
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}살입니다.`);
}
}
기존 클래스의 속성과 메서드를 물려받아 새로운 클래스를 정의 가능
상속을 구현하려면 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
은 서브타입을 슈퍼타입으로 변환하는 것을 의미
아래 코드에서도 단지 슈퍼타입 변수에 대입만 했을 뿐임
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
할 수 있음
클래스는 하나의 책임만 가져야 함
가장 기본적이고 중요한 원칙
예를 들면 사용자 클래스에서 게시물을 관리하는 작업이 있으면 안됨
잘못된 사례
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}`);
}
}
클래스 확장에 대해서는 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다는 원칙
기존 코드를 변경하지 않고도 기능을 확장할 수 있어야 함
즉, 부모 클래스와 자식 클래스가 있을 때, 부모 클래스(원본)을 수정하지 못하도록 해야 한다는 의미
서브타입은 기반이되는 슈퍼타입을 대체할 수 있어야 한다는 원칙
즉, 논리적으로 엄격하게 관계가 정립되어야 함
잘못된 사례
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 {} // 이제 위배되는 것은 아무것도 없네요!
해당 클래스에게 무의미한 메소드의 구현을 막자는 의미
즉, 인터페이스 구현 시 필요한 만큼만 정의하고 클래스에 맞게 필요한 인터페이스을 구현해야 함
하위 수준 모듈(구현 클래스)보다 상위 수준 모듈(인터페이스)에 의존해야 함
의존 관계를 맺을 때 변화하기 쉬운 것보다는 변화하기 어려운 것, 거의 변화가 없는 것에 의존하라는 것
의존성 역전 원칙을 위반한 경우:
의존성 역전 원칙을 적용한 경우:
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 문법 종합반] 책을 반납했습니다.
독서광님이 [금쪽이 훈육하기] 책을 반납했습니다.
*/
오늘 특강, 면담, 질문, 강의 정리 등으로 생각보다 시간이 너무 잘 갔음
그래서 사실 Nest.js 강의는 조금 밖에 시청하지 못함
내일과 모레에 걸쳐서 Nest.js 강의를 끝내보는 것을 목표로 할 예정
그래야 주말부터 타입스크립트 개인과제를 진행할 수 있기에 Nest.js를 최대한 학습할 예정
오늘은 TypeScript 문법 종합 강의 나머지를 모두 시청함
객체 지향 프로그래밍에 대한 내용은 예전 강의에서 학습했지만 막상 타입스크립트를 적용한 예제를 보니 생각처럼 눈에 들어오지 않았음
그래도 도서관 프로그램 만들기 실습을 진행하니 타입, 클래스, 인터페이스가 어느정도 이해됨
타입스크립트 강의는 끝났지만 Nest.js 강의를 통해서 타입스크립트에 대한 공부는 이어갈 예정