CS/프로그래밍

솔리드 원칙

H.E 2024. 1. 20. 17:31

솔리드 원칙은 소프트웨어 디자인의 다섯 가지 기본 원칙

이 원칙들은 소프트웨어를 더 견고하고 유연하며 유지보수가 쉽도록 만드는 데 도움이 되는 지침을 제공


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

클래스는 단 하나의 책임만 가져야 함

 

이유와 장점

  1. 유지보수성 향상
    1. 각 클래스나 모듈이 하나의 책임만을 갖도록 설계하면 코드의 유지보수가 쉬워짐
    2. 특정 기능을 변경해야 할 때 해당 기능에 대한 코드만 이해하고 수정할 수 있음
  2. 코드의 가독성 증가
    1. 각 클래스 또는 모듈이 특정 책임에 집중하면 코드가 명확하고 가독성이 향상
    2. 다른 개발자들도 해당 코드를 이해하기 쉽게 되어 협업이 용이
  3. 재사용성 증가
    1. 단일 책임을 갖는 모듈은 다른 부분에서 쉽게 재사용
    2. 특정 기능이 변경되어도 해당 모듈만 수정하면 되므로 다른 부분에 영향을 미치지 않음
  4. 테스트 용이성
    1. 작은 단위의 책임을 가진 모듈들은 테스트하기 쉬움
    2. 한 모듈의 기능을 테스트할 때 다른 책임에 영향을 받지 않아 테스트 코드 작성이 간단해짐

 

예시

나쁜 예시

class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

  validateCredentials() {
    // 사용자 자격 증명을 검증하는 코드
  }

  saveToDatabase() {
    // 데이터베이스에 사용자 정보를 저장하는 코드
  }
}

 

User 클래스는 사용자 객체를 표현할 뿐만 아니라, 자격 증명을 검증하고 데이터베이스에 저장하는 두 가지 역할을 가지고 있음

 

좋은 예시

class UserValidator {
  validateCredentials(user) {
    // 사용자 자격 증명을 검증하는 코드
  }
}

class UserManager {
  saveToDatabase(user) {
    // 데이터베이스에 사용자 정보를 저장하는 코드
  }
}

class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }
}

 

User 클래스는 단순히 사용자 정보를 가지고 있으며, UserValidator 클래스는 사용자 자격 증명을 검증하고, UserManager 클래스는 사용자 정보를 데이터베이스에 저장하는 역할을 각각 담당함

이렇게 하면 각 클래스가 하나의 책임만을 갖게 되어 코드의 유지보수와 확장성이 향상됨


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

소프트웨어 엔티티(클래스, 모듈, 함수 등) 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 함

 

이유와 장점

  1. 확장성 향상
    1. 개방/폐쇄 원칙을 지키면 기존의 코드를 수정하지 않고도 새로운 기능이나 모듈을 추가할 수 있음
    2. 코드를 확장할 때는 기존 코드를 수정하지 않고 새로운 코드를 추가함으로써 시스템을 확장할 수 있음
  2. 유지보수성 향상
    1. 기능의 변경이나 확장이 필요한 경우 해당 기능을 제공하는 클래스를 수정하지 않아도 됨
    2. 새로운 기능을 추가하거나 기존 기능을 변경할 때 다른 부분에 영향을 덜 주면서 작업할 수 있음
  3. 코드 안정성
    1. 기존 코드를 변경하지 않고 새로운 코드를 추가함으로써 기존 기능이나 모듈에 문제가 발생할 확률을 낮춤
      따라서 시스템의 안정성을 유지할 수 있습니다.

 

예시

나쁜예시

class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }
}

class AreaCalculator {
  calculateArea(rectangle) {
    return rectangle.width * rectangle.height;
  }
}

class Circle {
  constructor(radius) {
    this.radius = radius;
  }
}

// 새로운 도형이 추가될 때마다 AreaCalculator를 수정해야 함

 

AreaCalculator 클래스는 Rectangle Circle의 넓이를 계산하도록 구현되어 있습니다. 하지만, 새로운 도형이 추가될 때마다 AreaCalculator를 수정해야 하는 문제가 있음

 

좋은 예시

class Shape {
  area() {
    throw new Error("Subclasses must implement the area method");
  }
}

class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }

  area() {
    return this.width * this.height;
  }
}

class Circle extends Shape {
  constructor(radius) {
    super();
    this.radius = radius;
  }

  area() {
    return 3.14 * this.radius ** 2;
  }
}

// 새로운 도형이 추가되어도 AreaCalculator를 수정할 필요가 없음

 

개선된 예시에서 AreaCalculator 클래스 대신 Shape 클래스가 도입됨

Shape 클래스는 넓이를 계산하는 area 메서드를 정의하고, 이를 구현하는 각 도형 클래스는 Shape 클래스를 상속받음

이렇게 하면 새로운 도형이 추가될 때 AreaCalculator를 수정할 필요 없이, 간단히 새로운 도형 클래스를 추가하면 됨

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

자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 함

즉, 부모 클래스의 인스턴스 대신에 자식 클래스의 인스턴스를 사용해도 기능이 변하지 않아야 합니다.

 

이유와 장점

  1. 다형성 강화
    1. 리스코프 치환 원칙을 지키면 서브타입은 언제나 기본 타입으로 대체될 수 있음
    2. 이는 코드에서 다형성을 강화하고, 서브타입을 사용하는 부분에서 기대한 동작을 항상 보장
  2. 모듈성 향상
    1. 서브타입을 기본 타입으로 대체할 수 있기 때문에, 코드의 모듈성이 향상
    2. 새로운 서브타입이 추가되거나 변경되더라도 해당 서브타입을 사용하는 부분에서는 영향을 최소화할 수 있음
  3. 코드 예측 가능성
    1. 리스코프 치환 원칙을 지키면 서브타입을 사용하는 코드에서 예측 가능성이 증가
    2. 즉, 코드를 이해하고 유지보수하기 쉬워짐

 

예시

나쁜 예시

class Bird {
  fly() {
    console.log("날다");
  }
}

class Ostrich extends Bird {
  // 날지 못함
}

 

Ostrich 클래스가 Bird 클래스를 상속하고 있습니다. 그러나 Ostrich 클래스에서는 fly 메서드를 구현하지 않아 날지 못하는 새를 나타냅니다. 이는 리스코프 치환 원칙을 위반하는 것

 

좋은 예시

class Bird {
  fly() {
    // 날다
  }
}

class Sparrow extends Bird {
  // 날다
}

class Ostrich extends Bird {
  // 날지 못해도 구현
}

 

Ostrich 클래스도 fly 메서드를 구현하게 되었음. 이렇게 함으로써 Ostrich 클래스를 Bird 클래스의 서브타입으로서 자유롭게 사용할 수 있게 되며, 리스코프 치환 원칙을 준수하게 됩니다

▶ 인터페이스 분리 원칙 (Interface Segregation Principle - ISP)

클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않도록 하며, 한 클래스는 자신이 사용하지 않는 인터페이스에 의존해서는 안 됨. 즉, 큰 덩어리의 인터페이스보다는 작은 여러 개의 인터페이스가 낫다는 말

 

이유와 장점

  1. 클라이언트 코드에 불필요한 의존성 제거
    1. 클라이언트 코드가 자신이 사용하지 않는 메서드에 의존하지 않도록 하는 것은 인터페이스 분리 원칙의 핵심
      이를 통해 클라이언트 코드는 필요한 인터페이스에만 의존하게 되어 불필요한 의존성이 제거됩니다.
  2. 클래스의 응집성 향상
    1. 클래스가 자신과 무관한 메서드를 구현하거나 인터페이스를 포함할 필요가 없기 때문에 클래스의 응집성(cohesion)이 향상
    2. 각 클래스는 자신의 주요 기능과 관련된 메서드만을 포함하게 되어 코드가 더 명확해짐
  3. 클래스의 재사용성 증가
    1. 인터페이스 분리를 통해 작은 규모의 인터페이스로 나누면, 이 인터페이스를 구현하는 클래스는 필요한 기능만 구현하게 되므로 다른 클래스에서도 쉽게 재사용

 

예시

나쁜 예시

class Worker {
  work() {
    // 일하다
  }

  eat() {
    // 식사하다
  }
}

// 사용자는 일할 때만 필요한데, 일하지 않는 동안에도 eat() 메서드에 의존
const user = new Worker();
user.work();

 

Worker 클래스는 work eat 두 가지 메서드를 포함하고 있음

하지만 사용자는 일할 때만 work 메서드가 필요하므로, eat 메서드에는 의존하지 않아야 함

 

좋은 예시

class Workable {
  work() {
    // 일하다
  }
}

class Eatable {
  eat() {
    // 식사하다
  }
}

class Worker {
  constructor(workable) {
    this.workable = workable;
  }

  work() {
    this.workable.work();
  }
}

// 사용자는 일할 때만 필요한 workable에만 의존
const workableUser = new Worker(new Workable());
workableUser.work();

 

Worker 클래스는 Workable 인터페이스에만 의존하도록 되어 있음

사용자는 일할 때만 필요한 Workable 객체를 주입받아 사용하며, Eatable에는 의존하지 않음

이렇게 함으로써 인터페이스 분리 원칙을 지킴

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

고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 함

즉, 추상화된 것은 구체적인 것에 의존하면 안 되며, 구체적인 것이 추상화된 것에 의존해야 함

 

이유와 장점

  1. 결합도 감소
    1. 의존성 역전 원칙을 지키면 고수준 모듈은 저수준 모듈에 의존하지 않고, 양쪽 모두 추상화에 의존하게 됨
      이렇게 함으로써 모듈 간의 결합도가 감소하고, 한 모듈을 변경할 때 다른 모듈에 미치는 영향이 줄어듬
  2. 유연성 향상
    1. 의존성 역전을 통해 추상화를 통해 상위 수준 모듈이 하위 수준 모듈에 대한 세부 구현을 몰라도 되므로, 새로운 구현을 쉽게 추가하거나 기존 구현을 변경할 수 있음
  3. 테스트 용이성
    1. 의존성을 주입하는 방식을 사용하면 모듈의 의존성을 쉽게 대체
    2. 유닛 테스트 작성 시에 모의 객체(Mock Objects) 등을 이용하여 의존성을 주입하여 테스트하기 용이하게 만듬

 

예시

나쁜 예시

class LightBulb {
  turnOn() {
    console.log("전구를 켭니다.");
  }

  turnOff() {
    console.log("전구를 끕니다.");
  }
}

class Switch {
  constructor() {
    this.bulb = new LightBulb();
  }

  operate() {
    if (this.isOn) {
      this.bulb.turnOff();
      this.isOn = false;
    } else {
      this.bulb.turnOn();
      this.isOn = true;
    }
  }
}

const switchButton = new Switch();
switchButton.operate();  // 전구를 켭니다.

 

Switch 클래스가 LightBulb 클래스에 직접 의존하고 있음

 

좋은 예시

class Switchable {
  turnOn() {
    throw new Error("구현되지 않은 메서드: turnOn");
  }

  turnOff() {
    throw new Error("구현되지 않은 메서드: turnOff");
  }
}

class LightBulb extends Switchable {
  turnOn() {
    console.log("전구를 켭니다.");
  }

  turnOff() {
    console.log("전구를 끕니다.");
  }
}

class Switch {
  constructor(device) {
    this.device = device;
  }

  operate() {
    if (this.isOn) {
      this.device.turnOff();
      this.isOn = false;
    } else {
      this.device.turnOn();
      this.isOn = true;
    }
  }
}

const bulb = new LightBulb();
const switchButton = new Switch(bulb);
switchButton.operate();  // 전구를 켭니다.

 

Switch 클래스는 Switchable 인터페이스에 의존하고 있음

Switchable 인터페이스를 구현한 클래스(예: LightBulb)를 주입받아 사용하므로, Switch 클래스는 구체적인 구현이 아니라 추상적인 인터페이스에 의존함. 이렇게 하면 코드의 확장성과 유연성이 향상

솔리드 원칙을 따르면 코드의 유지보수성, 재사용성, 테스트 용이성 등이 향상되어 좀 더 견고하고 효율적인 소프트웨어를 개발할 수 있음

'CS > 프로그래밍' 카테고리의 다른 글

디자인 패턴 - 싱글톤 패턴  (3) 2024.02.28
[CS Study] 디자인 패턴  (0) 2024.02.04
고차 컴포넌트 (HOC, High Order Component)  (0) 2024.01.20
CSS vs CSS-in-CSS vs CSS-in-JS  (1) 2024.01.13
CSS in JS  (1) 2024.01.13