객체지향 SOLID 5원칙

성석민·2023년 7월 6일
26

백엔드

목록 보기
1/1
post-thumbnail

📍코드업는 프로그래밍 채널의 내용을 바탕으로 정리했습니다.📍

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

하나의 클래스는 하나의 책임만 가져야 한다. ↔ 클래스를 변경하는 이유는 오직 한 가지뿐이어야 한다.

  • 클래스를 크기가 작고 적은 책임을 가지도록 작성해야 변경에 유연하게 대처할 수 있다.
  • 커다란 클래스는 다른 클래스와 의존성이 증가하게 되므로 변경 비용이 더 커지게 된다.

예시)
고양이는 밥을먹고, 걷고, 말을하는 행동은 당연한 기능이지만 print와 log 같은 행동은 고양이의 기능이 아니다.이것은 단일 책임 원칙에 위배된다. 따라서 다른 방식으로 두 개의 기능을 구현해야 한다.

// before
class Cat {
  constructor(private age: number, private name: string) {}

  eat() {
    console.log(`${this.name} 고양이가 밥을 먹습니다.`);
  }

  walk() {
    console.log(`${this.name} 고양이가 걷습니다.`);
  }

  speak() {
    console.log(`${this.name} 고양이가 짓습니다.`);
  }

  print() {
    console.log(`${this.name} 고양이는 ${this.age}살 입니다.`);
  }

  log() {
    console.log(`${this.name} 고양이는 ${this.age}살 입니다.`);
    logger.log(date.now()) // 예시입니다.
  }
}

// after
class Cat {
  constructor(private age: number, private name: string) {}

  eat() {
    console.log(`${this.name} 고양이가 밥을 먹습니다.`);
  }

  walk() {
    console.log(`${this.name} 고양이가 걷습니다.`);
  }

  speak() {
    console.log(`${this.name} 고양이가 짓습니다.`);
  }

  status() {
    return `${this.name} 고양이는 ${this.age}살 입니다.`
  }
}

const kitty = new Cat();
console.log(kitty.status());
logger.log(kitty.status());

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

확장에는 열려 있으나 변경에는 닫혀 있어야 한다. ↔ 높은 응집도와 낮은 결합도

  • 높은 응집도: 같은 책임, 관심사를 기반으로 하나의 객체로 설계하기 때문에 객체에 변경이 발생하더라도 다른 곳에 미치는 영향이 제한적이다.
  • 낮은 결합도: 책임과 관심사가 다른 객체 또는 모듈과는 낮은 결합도를 유지해야 한다.

예시)
동물을 나타내는 Animal 클래스가 있다고 가정한다.
각 동물들의 종류를 받고 해당 동물의 울음소리를 출력하는 hey 메서드가 있다.
개, 고양이 이외의 동물을 입력 받는 경우 해당 동물의 울음소리를 추가해주어야만 울음소리를 출력할 수 있다.
이는 확장에 닫혀있는 즉, OCP의 위배된다.

// before
class Animal {
  constructor(private type: string) {}

  hey(animal: Animal) {
    if (animal.type === 'Dog') console.log('bark');
    else if (animal.type === 'Cat') console.log('meow');
    else throw new Error('정의하지 않은 동물입니다.');
  }
}

const bingo = new Animal('Dog'); // bark
const kitty = new Animal('Cat'); // meow
const cow = new Animal('Cow'); // 정의하지 않은 동물입니다.

// after
const hey = (animal: Animal) => {
  animal.speak();
};

abstract class Animal {
  public abstract speak(): void;
}

class Dog extends Animal {
  speak(): void {
    console.log('bark');
  }
}

class Cat extends Animal {
  speak(): void {
    console.log('meow');
  }
}

const dog = new Dog();
const cat = new Cat();

hey(dog);
hey(cat);

// 개, 고양이 이외의 동물(양: sheep) 추가
class Sheep extends Animal {
  speak(): void {
    console.log('meh');
  }
}

const sheep = new Sheep();
hey(sheep);

LSP(Liskov Substitution Principle): 리스코프 치환 원칙

객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야한다.

  • 실제 구현체인 자식 인스턴스는 언제든지 부모 또는 인터페이스가 제공해야 하는 기능을 제공하는 다른 구현체로 바꿀 수 있다.

예시)
부모 클래스인 Cat을 상속받은 BlackCat 클래스가 있다고 가정한다.
고양이 인스턴스를 만든 후 해당 고양이를 서브 클래스인 검정 고양이로 치환을 해도 잘 작동해야한다.
다만, 뜬금없이 Fish로 치환하는 것은 LSP 원칙에 위배된다.

class Cat {
  speak() {
    console.log('meow');
  }
}

class BlackCat extends Cat {
  speak() {
    console.log('black meow');
  }
}

const speak = (cat: Cat) => {
  cat.speak();
};

let cat = new Cat();
speak(cat); // meow

cat = new BlackCat();
cat.speak(); // black meow

// 고양이는 생선으로 치환할 수 없다. 그렇기에 LSP 원칙에 위배된다.
class Fish extends Cat {
  speak() {
    throw new Error('생선을 말을 할 수 없어요.');
  }
}

cat = new Fish();
cat.speak(); // 생선은 말을 할 수 없어요.

ISP(Interface Segragation Principle): 인터페이스 분리 원칙

인터페이스가 클라이언트에서 필요하지 않은 메서드를 제공하지 않아야 한다.

  • 인터페이스를 기능별로잘게 쪼개어 특정 클라이언트용 인터페이스로 모아 사용하는 것이 변경에 대해 의존성을 낮추고 유연하게 대처할 수 있다.

예시) 수륙양용차: 물 위에서나 땅 위에서 모두 다닐 수 있게 만든 자동차
수륙양용차를 만들기 위해서는 물이나 땅에서 운전할 수 있는 기능이 있어야한다.
하지만 물에서만 다닐 수 있는 기능, 땅에서만 다닐 수 있는 기능을 구현하기 위해서 수륙양용차의 기능을 전부 집어넣으면 불필요한 메서드를 지닌 자동차가 만들어질것이다. 이것은 ISP 원칙에 위배된다.

// before
interface CarBoat {
  drive: () => void;
  turnLeft: () => void;
  turnRight: () => void;

  steer: () => void;
  steerLeft: () => void;
  steerRight: () => void;
}

class Genesis implements CarBoat {
  drive() {
    /* ... */
  }

  turnLeft() {
    /* ... */
  }

  turnRight() {
    /* ... */
  }

  /** 불필요한 메서드
    steer: () => void;
    steerLeft: () => void;
    steerRight: () => void;
   */
}

class Boat implements CatBoat {
  steer() {
    /* ... */
  }

  steerLeft() {
    /* ... */
  }

  steerRight() {
    /* ... */
  }

  /** 불필요한 메서드
    drive: () => void;
    turnLeft: () => void;
    turnRight: () => void;
   */
}

// after
interface Car {
  drive: () => void;
  turnLeft: () => void;
  turnRight: () => void;
}

interface Boat {
  steer: () => void;
  steerLeft: () => void;
  steerRight: () => void;
}

class Genesis implements Car {
  drive() {
    /* ... */
  }

  turnLeft() {
    /* ... */
  }

  turnRight() {
    /* ... */
  }
}

class Boat implements Boat {
  steer() {
    /* ... */
  }

  steerLeft() {
    /* ... */
  }

  steerRight() {
    /* ... */
  }
}

class CarBoat implements Car, Boat {
  drive() {
    /* ... */
  }

  turnLeft() {
    /* ... */
  }

  turnRight() {
    /* ... */
  }

  steer() {
    /* ... */
  }

  steerLeft() {
    /* ... */
  }

  steerRight() {
    /* ... */
  }
}

DIP(Dependency Inversion Principle): 의존관계 역전 원칙

추상화에 의존해야지, 구체화에 의존하면 안된다.

High Level 모듈에서 Low Level 모듈들에 의존하게 만드는 것이 아닌 중간에 추상화 모듈을 만들어 High Level 모듈과 Low Level 모듈이 의존할 수 있게 만들어준다.

예시)
High Level → Low Level
High Level → 추상화 모듈 ← Low Level

// High Level → Low Level
class Cat {
  speak() {
    console.log('meow');
  }
}

class Dog {
  speak() {
    console.log('bark');
  }
}

class Zoo {
  constructor(private cat: Cat, private dog: Dog) {
    this.cat = cat;
    this.dog = dog;
  }
}

// 양을 추가하려면 Zoo 클래스에 수동으로 추가 해야한다.
class Sheep {
  speak() {
    console.log('meh');
  }
}

class Zoo {
  constructor(private cat: Cat, private dog: Dog, private sheep: Sheep) {
    this.cat = cat;
    this.dog = dog;
    this.sheep = sheep;
  }
}

// High Level → 추상화 모듈 ← Low Level
class Animal {
  speak() {}
}

class Cat extends Animal {
  speak() {
    console.log('meow');
  }
}

class Dog extends Animal {
  speak() {
    console.log('bark');
  }
}

class Zoo {
  constructor(private animals: Animal[]) {}

  addAnimal(animal: Animal) {
    this.animals.push(animal);
  }
}

const zoo = new Zoo();
zoo.addAnimal(new Dog());
zoo.addAnimal(new Cat());

// 동물(양: sheep) 추가
class Sheep extends Animal {
  speak() {
    console.log('meh');
  }
}

zoo.addAnimal(new Sheep());

틀린 부분이 있거나 보충해야 할 내용이 있다면 댓글이나 DM(sungstonemin)으로 알려주시면 감사하겠습니다😄

profile
기록하는 개발자

5개의 댓글

comment-user-thumbnail
2023년 7월 6일

좋은 글 감사합니다~

1개의 답글
comment-user-thumbnail
2023년 7월 9일

SOLID 개념에 대해 예시를 들어 주셔서 이전에 배운 개념을 다시 반추할 수 있는 깔끔한 글이네요 :D 잘 읽고갑니다!
의견이 한 가지 있긴한데, 마지막 DIP 관련한 코드에서 Animal 이라는 추상화 클래스를 만들고 Dog, Cat 클래스가 이를 Implement 하고 있는 것 같아요, 여기서 새로 추가된 Sheep 클래스의 경우 Animal 을 Implement 하고 있지는 않은 것 같은데 혹시 의도하셨던 부분이 있는걸까요~?

1개의 답글
comment-user-thumbnail
2023년 8월 9일

잘 읽었습니다~

답글 달기