저장을 습관화

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

공부/TypeScript

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

ctrs 2023. 8. 1. 23:09

1. S(Single Responsibility Principle, 단일 책임 원칙)

SOLID 중에서 가장 중요한 사항이다.

 

하나의 클래스는 다른 클래스의 역할과 권한을 침범해서는 안되며,

하나의 클래스가 해야하는 역할과 권한은 해당 클래스만 가질 수 있도록 보장되어야 한다.

 

 

잘못된 예시)

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

 

 

2. O(Open-Closed Principle, 개방 폐쇄 원칙)

클래스는 확장에 대해서는 열려 있어야 하되, 수정에 대해서는 닫혀 있어야 한다.

클래스의 기존 코드를 변경하지 않고도 기능을 확장할 수 있어야한다.

 

인터페이스나 상속을 생각해보자

자식 클래스는 부모 클래스의 기본 코드를 변경하지 않고도 기능을 확장하였다.

 

 

3. L(Liskov Substitution Principle, 리스코프 치환 원칙)

서브타입은 그 기반이 되는 슈퍼타입을 대체할 수 있어야 한다.

 

자식 클래스는 부모 클래스의 기능을 수정하지 않고도 부모 클래스와 호환되어야 하며

논리적으로 엄격하게 관계가 정립 되어야 한다.

 

잘못된 예시)

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 {}

 

 

4. I(Interface Segregation Principle, 인터페이스 분리 원칙)

클래스는 자신이 사용하지 않는 인터페이스의 영향을 받지 않아야 한다.

 

클래스에게 무의미한 메소드의 구현을 막을 수 있게끔

인터페이스를 너무 크게 정의하기보단 필요한 만큼한 적절히 정의하고

클래스는 필요에 맞는 인터페이스를 구현하도록 유도해야한다.

 

 

5. D(Dependency Inversion Principle, 의존성 역전 원칙)

Java-Spring이나 Node.js-Nest.js와 같이 웹 서버 프레임워크 내에서 많이 나오는 원칙이며,

하위 수준 모듈(구현 클래스)보다 상위 수준 모듈(인터페이스)에 의존해야한다는 의미이다.

 

예를 들어 클래스 'DataBase'가 있다.

'DataBase'의 원천은 'LocalStorage'가 될 수도 있고, 'CloudStorage'가 될 수도 있다.

이 때 'DataBase'의 원천을 'LocalStorage' 타입이나 'CloudStorage' 타입으로 한정하지 않고,

그보다 상위 수준인 'Storages' 타입으로 한정하여야 한다.

 

예시)

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 {
  // MyLocalStorage, MyCloudStorage 같은 하위 수준 모듈에 의존하지 않고,
  // 상위 수준 모듈인 MyStorage 타입을 의존한다.
  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("클라우드 데이터");