본문 바로가기
Programming & Platform/TypeScript

TypeScript 객체지향 설계 원칙 SOLID 설명, 예시 코드

by 코드스니펫 2023. 12. 20.
반응형

TypeScript 객체지향 설계 원칙 SOLID

 

typescript logo

 

TypeScript 객체지향 설계시 SOLID 원칙을 지켜 작성해야합니다. 이렇게 함으로써 프로그램이 유연해지고, 확장 가능하며, 이해하기 쉬운 구조를 가지게 됩니다. 아래 글에 소개하는 SOLID 원칙을 보며 효율적인 객체지향 설계에 많은 인사이트가 되시길 바랍니다.

 

 

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

 

클래스는 하나의 책임만을 가져야 한다는 매우 기본적인 원칙입니다.  SOLID의 5가지 설계 원칙 중 가장 기본적이고 중요한 원칙입니다. 쉽게 말해 클래스 하나에는 하나의 기능만을 수행해야 합니다.

 

잘못된 예시

UserService 클래스 안에 갑자기 뜬금없이 이메일 전송하는 로직이 들어있습니다. 이는 UserService와는 관련없는 기능으로 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);
  }
}

 

 

올바른 예시

위의 잘못된 예시에서 이메일 전송하는 로직을 EmailService라는 클래스 안에 새로 넣어둠으로써 제 1원칙을 지켜낼 수 있게 되었습니다. 

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

 

 

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

 

클래스는 확장에 대해서 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 하는 원칙을 말합니다. 클래스의 기존 코드는 변경하지 않고 기능을 확장할 수 있어야 합니다. 

 

이는 인터페이스상속을 통해 해결할 수 있습니다. 

 

잘못된 예시

class Rectangle {
  constructor(private width: number, private height: number) {}

  calculateArea(): number {
    return this.width * this.height;
  }
}

class AreaCalculator {
  calculateArea(rectangle: Rectangle): number {
    return rectangle.calculateArea();
  }
}

 

이 코드에서는 AreaCalculator 클래스가 직접적으로 Rectangle 클래스에 의존하고 있습니다. 이 경우, 새로운 도형이 추가되면 AreaCalculator 클래스를 수정해야 합니다. 이는 OCP를 위반하는 것입니다.

 

 

올바른 예시

interface Shape {
  calculateArea(): number;
}

class Rectangle implements Shape {
  constructor(private width: number, private height: number) {}

  calculateArea(): number {
    return this.width * this.height;
  }
}

class Triangle implements Shape {
  constructor(private base: number, private height: number) {}

  calculateArea(): number {
    return 0.5 * this.base * this.height;
  }
}

class AreaCalculator {
  calculateArea(shape: Shape): number {
    return shape.calculateArea();
  }
}

 

이 코드에서는 Shape 인터페이스를 도입하여 각 도형 클래스가 해당 인터페이스를 구현하도록 합니다. 이제 AreaCalculator 클래스는 Shape 인터페이스에 의존하므로, 새로운 도형이 추가되더라도 AreaCalculator 클래스를 수정할 필요가 없습니다. 이러한 방식으로 코드는 확장 가능하며 수정에는 닫혀 있게 됩니다.

 

 

SOLID 제3원칙 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 {} // 이제 위배되는 것은 아무것도 없네요!

 

 

SOLID 제4원칙 I(ISP, 인터페이스 분리 원칙)

 

클래스는 자신이 사용하지 않는 인터페이스의 영향을 받아서는 안된다는 원칙입니다. 해달 클래스에 무의미한 메소드 구현을 막자는 뜻입니다. 그렇기 때문에 인터페이스 구현에 있어서 크게 정의하기 보다는 필요한만큼만 정의하는 것이 무엇보다 중요합니다. 

 

잘못된 예시

interface Worker {
  work(): void;
  eat(): void;
}

class SuperWorker implements Worker {
  work(): void {
    // 복잡한 작업 수행
  }

  eat(): void {
    // 식사
  }
}

class NormalWorker implements Worker {
  work(): void {
    // 간단한 작업 수행
  }

  eat(): void {
    // 식사
  }
}

 

이 코드에서는 Worker 인터페이스가 work와 eat 두 가지 메서드를 포함하고 있습니다. 그러나 일반 작업자(NormalWorker)나 슈퍼 작업자(SuperWorker)가 모두 work와 eat 메서드를 구현해야 하는데, 이는 ISP를 위반하는 것입니다. 일반 작업자는 실제로 슈퍼 작업자가 수행하는 복잡한 작업과는 무관한 간단한 작업만 필요할 수 있습니다.

 

 

올바른 예시

interface Workable {
  work(): void;
}

interface Eatable {
  eat(): void;
}

class SuperWorker implements Workable, Eatable {
  work(): void {
    // 복잡한 작업 수행
  }

  eat(): void {
    // 식사
  }
}

class NormalWorker implements Workable, Eatable {
  work(): void {
    // 간단한 작업 수행
  }

  eat(): void {
    // 식사
  }
}

 

이 코드에서는 Worker 인터페이스를 Workable과 Eatable로 나누었습니다. 이제 각 클래스는 필요한 인터페이스만 구현하면 되므로, 일반 작업자는 간단한 작업을 수행하고, 슈퍼 작업자는 복잡한 작업을 수행할 수 있습니다. 이렇게 하면 각 클래스가 자신이 필요로 하는 메서드에만 의존하게 되어 ISP를 지키게 됩니다.

 

 

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

 

Java의 Spring 프레임워크나 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("클라우드 데이터");

 

이 코드는 SOLID 원칙 중 DIP(의존성 역전 원칙)를 따르고 있습니다. DIP는 상위 수준 모듈이 하위 수준 모듈에 의존하면 안 되며, 양쪽 모두 추상화에 의존해야 한다는 원칙입니다.

 

  1. MyStorage 인터페이스: 데이터를 저장하는 기능을 정의한 인터페이스입니다. 이 인터페이스를 구현하는 클래스들은 저장 기능을 제공해야 합니다.
  2. MyLocalStorage 클래스: MyStorage 인터페이스를 구현한 클래스로, 로컬 스토리지에 데이터를 저장하는 기능을 구현합니다.
  3. MyCloudStorage 클래스: 마찬가지로 MyStorage 인터페이스를 구현한 클래스로, 클라우드에 데이터를 저장하는 기능을 구현합니다.
  4. Database 클래스: 상위 수준 모듈로, MyStorage 인터페이스에 의존하고 있습니다. 따라서 실제 저장소 구현에 대한 세부 사항을 몰라도 됩니다. 이는 DIP를 준수하고 있습니다. saveData 메서드를 통해 데이터를 저장할 수 있습니다.
  5. myLocalStorage와 myCloudStorage 인스턴스: 각각 로컬 스토리지와 클라우드 스토리지를 사용하는 MyLocalStorage와 MyCloudStorage 인스턴스를 생성합니다.
  6. myLocalDatabase와 myCloudDatabase 인스턴스: 각각 로컬 스토리지와 클라우드 스토리지를 사용하는 Database 인스턴스를 생성합니다. 이때, 생성자를 통해 해당 스토리지를 주입받습니다.

 

이 코드는 추상화를 통해 상위 수준 모듈이 하위 수준 모듈에 직접 의존하지 않고, 인터페이스를 통해 의존성을 역전시킴으로써 변경에 유연하고 확장 가능한 코드를 만들 수 있게 합니다. 즉, 나중에 다른 저장소가 추가되더라도 Database 클래스를 수정하지 않고 새로운 저장소 구현을 주입함으로써 기능을 확장할 수 있습니다.

 

▼ 함께 보면 좋은 글 ▼

 

 

내일배움캠프 백오피스 프로젝트 - 펫시터 매칭 사이트 후기, 소감

내일배움캠프 백오피스 프로젝트 - 펫시터 매칭 사이트 후기, 소감 일주일간 팀원과 작업한 펫시터 매칭 사이트가 끝났습니다. 여러 우여곡절이 있었지만 목표한 대로 마쳤기에 만족하고 있습

lemonlog.tistory.com

 

 

부트스트랩 달력 특정일자 비활성화 하는 방법 bootstrap datepicker

부트스트랩 달력 특정일자 비활성화 하는 방법 bootstrap datepicker 부트스트랩에서 달력을 사용하다가 특정일자 비활성화를 해야하는 일이 생겼습니다. 다른 서비스에서는 여러 보긴 했지만 부트

lemonlog.tistory.com