2024.10.07 SOLID

장재영·2024년 10월 7일
0

단일 책임의 원칙 (Single Responsibility Principle, SRP)

  • 하나의 객체는 단 하나의 책임을 가진다

예를들어 아래는 캐릭터 클래스가 공격, 방어, 이동 3개의 기능을 가지고있다.

class Character {
  attack(target) {
    console.log(`${this.name}${target.name}을(를) 공격.`);
  }

  move(position) {
    console.log(`${this.name}${position}으로 이동.`);
  }

  defence(target) {
    console.log(`${this.name}${target.name}의 공격을 방어.`);
  }
}

이걸 아래처럼 한가지 책임만 가지게 수정한다.

/* 수정 */
class Character {
  constructor(name) {
    this.name = name;
  }

  attack(target) {
    console.log(`${this.name}${target.name}을(를) 공격.`);
  }
  
  defence(target) {
    console.log(`${this.name}${target.name}의 공격을 방어.`);
  }
}

class Movement {
  move(character, position) {
    console.log(`${character.name}${position}으로 이동.`);
  }
}

어? 뭔가 이상한거같은데요?
=> 아니오
공격과 방어는 전투라는 책임에 포함된 기능들 이므로 책임은 하나임
기능과 책임의 차이를 구분해야함.


개방-폐쇄 원칙 (Open-Closed Principle, OCP)

  • '기존' 코드를 수정하지 않고도 새로운 기능을 추가할 수 있어야 함.

예시로 아래는 캐릭터 클래스의 공격 메서드에서 새로운 무기(예로 지팡이)를 추가하려면 클래스 내부 코드를 건드려야함

class Character {
  ...
  attack(weaponType, target) {
    if (weaponType === 'sword') {
      console.log(`${this.name}${target.name}을 칼로 공격.`);
    } else if (weaponType === 'bow') {
      console.log(`${this.name}${target.name}을 활로 공격.`);
    }
  }
}

아래처럼 수정하면 새로운 무기만 만들고 다른 코드를 건드릴 필요가 없어짐

/* 수정 */
class Character {
  ...
  attack(target) {
    this.weapon.attack(this, target);
  }
}

class Sword extends Weapon {
  attack(character, target) {
    console.log(`${character.name}${target.name}을 칼로 공격.`);
  }
}

class Bow extends Weapon {
  attack(character, target) {
    console.log(`${character.name}${target.name}을 활로 공격.`);
  }
}

리스코프 치환 원칙 (Liskov substitution principle, LSP)

  • 서브 클래스는 부모 클래스의 기능을 그대로 가지면서, 추가적인 기능을 제공할 수는 있지만 부모 클래스의 동작을 변경하거나 정지시키는 방식으로 작동해서는 안 된다

예를들어 아래는 새라는 카테고리에 포함된 펭귄을 예시로 들었다.
새는 하늘을 나는 것을 전재로 하는 bird클래스와 bird클래스를 상속받고 있지만 날지못하는 펭귄의 안타까운 사연을 보자
펭귄은 bird클래스의 기본기능을 제대로 작동시키지 않으므로 LSP위반이다.

class Bird {
  fly() {
    console.log('새가 하늘을 날아갑니다.');
  }
}

class Penguin extends Bird {
  fly() {
    throw new Error('펭귄은 날 수 없습니다.');
  }
}

애초에 부모클래스에서 새는 '이동을한다'로 바꿔보자

/* 수정 */
class Bird {
  move() {
    console.log('새가 이동합니다.');
  }
}

class FlyingBird extends Bird {
  move() {
    this.fly();
  }

  fly() {
    console.log('하늘을 날아갑니다.');
  }
}

class Penguin extends Bird {
  move() {
    console.log('펭귄이 이동을 합니다.');
  }
}

이렇게 하면 날 수 있는 새는 날 수 있게 그냥 이동하는 새는 이동하게 바꿀 수 있다.


인터페이스 분리 원칙 (Interface segregation principle, ISP)

  • 클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않도록 인터페이스를 작게 분리해야 한다

  • 간단하게 말해서 쓸모없는 기능을 넣지 말라는 것 이지만... 글로만 설명하기가 애매하므로 예시를보자

아래는 쓸때없이 Character에 이것 저것 밖아놔서 크기만 커진 결과다.

class Character {
  attack() { /* ... */ }
  fly() { /* ... */ }
  swim() { /* ... */ }
}

class GroundCharacter extends Character {
  attack() {
    console.log('지상 캐릭터가 공격.');
  }

  fly() {
    throw new Error('지상 캐릭터는 날 수 없습니다.');
  }

  swim() {
    throw new Error('지상 캐릭터는 수영할 수 없습니다.');
  }
}

이걸 아래와같이 수정하면

/* 수정 */
class Attackable {
  attack()
}

class Flyable {
  fly()
}

class Swimmable {
  swim()
}

class GroundCharacter extends Attackable {
  attack() {
    console.log('지상 캐릭터가 공격.');
  }
}

class FlyingCharacter extends Attackable, Flyable {
  attack() {
    console.log('비행 캐릭터가 공격.');
  }

  fly() {
    console.log('비행 캐릭터가 날아갑니다.');
  }
}

지상 캐릭터는 지상캐릭터답게 나는 캐릭은 날게 할 수 있다.

뭔가 어디서 본거같다고? 정답!
SRP를 지키면 ISP는 '거의' 해결된다.


의존성 역전 원칙 (Dependency Inversion Principle, DIP)

  • 고수준 모듈이 저수준 모듈을 직접적으로 의존해서는 안된다(추상화를 통해서만 의존)
  • 추상화는 세부사항에 의존해서는 안된다.(세부사항이 추상화에 의존)
    - 고수준 모듈: 프로그램의 중요한 로직으로 사용자 관리, 주문 처리, 게임 로직 등
    - 저수준 모듈: 데이터베이스, 파일 시스템, 네트워크 통신 등과 같은 구체적인 기술적인 부분

아래는 DIP를 위반한 예시
아래 코드는 DB를 만약 다른 DB로 연결한다고 하면 메인 클래스로직도 수정해야함

class MySQLDatabase {
  connect() {
    console.log("MySQL 데이터베이스에 연결.");
  }
}

class UserManager {
  constructor() {
    this.database = new MySQLDatabase();
  }

  saveUser(user) {
    this.database.connect();
    console.log(`${user}을(를) 저장.`);
  }
}

이를 메인 클래스를 수정하는 것이 아닌 외부에서만 수정을 거칠 수 있게 바꿈

// 데이터베이스 인터페이스 (추상화)
class Database {
  connect()
}

// MySQL 데이터베이스 구현 (저수준 모듈)
class MySQLDatabase extends Database {
  connect() {
    console.log("MySQL 데이터베이스에 연결.");
  }
}

// PostgreSQL 데이터베이스 구현 (저수준 모듈)
class PostgreSQLDatabase extends Database {
  connect() {
    console.log("PostgreSQL 데이터베이스에 연결.");
  }
}

// UserManager (고수준 모듈)
class UserManager {
  constructor(database) {
    this.database = database;  // 추상화에 의존
  }

  saveUser(user) {
    this.database.connect();
    console.log(`${user}을(를) 저장.`);
  }
}

// 이제 UserManager는 구체적인 데이터베이스 구현이 아닌 추상화에 의존.
const mysqlDatabase = new MySQLDatabase();
const userManager = new UserManager(mysqlDatabase);
userManager.saveUser('Alice');

// 데이터베이스를 쉽게 변경.
const postgresDatabase = new PostgreSQLDatabase();
const userManager2 = new UserManager(postgresDatabase);
userManager2.saveUser('Bob');

즉, 추상화된 인터페이스나 추상 클래스를 중간다리로 사용한다.

profile
개발 하고 싶은 비버

0개의 댓글