본문 바로가기

개발자 강화/백엔드

[매일메일] SOLID 원칙이란? (BE.250130)

SOLID 원칙

=객체지향 설계 5원칙, 각 원칙의 앞 글자를 따서 만들어짐

객체지향설계 핵심은 의존성을 잘 관리하는 것임

 

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

클래스가 오직 하나의 목적이나 이유로만 변경되어야 함.

여러 책임이 한 클래스에 있으면 한 기능이 변경될 때 다른 기능도 영향 받음.

Responsibility(책임)은 특정 사용자나 기능 요구사항에 따라 소프트웨어의 변경 요청을 처리하는 역할을 의미함

 

📌SRP를 위반한 코드

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

  saveToDatabase() {
    // DB 저장 로직 (데이터 관리 책임)
    console.log("User saved to database");
  }

  sendEmail() {
    // 이메일 전송 로직 (이메일 관련 책임)
    console.log("Email sent to user");
  }
}

 

📌 SRP를 준수한 코드

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

class UserRepository {
  save(user) {
    console.log(`${user.name} saved to database`);
  }
}

class EmailService {
  sendEmail(user) {
    console.log(`Email sent to ${user.email}`);
  }
}

User 정보 저장 방식이 바뀌어도 EmailService에는 영향이 없음

 

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

확장에는 열려있고, 변경에는 닫혀 있어야 함.

* 확장: 새로운 타입을 추가해 새로운 기능을 추가하는 것

* 폐쇄: 확장이 일어날 때 상위 레벨의 모듈이 영향을 받지 않아야 함

기존 코드 수정 없이 새로운 기능을 추가할 수 있어 버그 발생 가능성을 줄임. 다른 코드와 독립적임.

 

📌 OCP 위반한 코드

class PaymentProcessor {
  process(paymentType) {
    if (paymentType === "creditCard") {
      console.log("Processing credit card payment");
    } else if (paymentType === "paypal") {
      console.log("Processing PayPal payment");
    }
  }
}

 

📌 OCP 준수한 코드

class PaymentProcessor {
  process(paymentMethod) {
    paymentMethod.pay();
  }
}

class CreditCardPayment {
  pay() {
    console.log("Processing credit card payment");
  }
}

class PayPalPayment {
  pay() {
    console.log("Processing PayPal payment");
  }
}

// 확장할 때 기존 코드 수정 없이 새로운 클래스만 추가하면 됨!
class CryptoPayment {
  pay() {
    console.log("Processing cryptocurrency payment");
  }
}

기존 PaymentProcessor 코드를 수정할 필요 없이 새로운 결제 방식을 추가할 수 있음

기능이 확장될 때 기존 코드에 영향 주지 않음

 

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

하위 클래스는 상위 클래스를 대체할 수 있어야 함

상위 타입을 사용하는 코드가 하위 타입으로 교체되더라도 정상적으로 작동해야 한다

하위 클래스가 상위 클래스 기능을 깨지 않도록 보장해 예상치 못한 버그 방지

 

📌 LSP를 위반한 코드

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

  setWidth(width) {
    this.width = width;
  }

  setHeight(height) {
    this.height = height;
  }

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

class Square extends Rectangle {
  setWidth(width) {
    this.width = width;
    this.height = width; // ❌ 정사각형이므로 width와 height가 항상 같아야 함
  }

  setHeight(height) {
    this.height = height;
    this.width = height; // ❌
  }
}

const shape = new Square();
shape.setWidth(10);
console.log(shape.getArea()); // 기대값: 100, 실제값: 100 (OK)

const rect = new Rectangle(10, 5);
rect.setWidth(20);
console.log(rect.getArea()); // 기대값: 100, 실제값: 100 (OK)

Rectangle은 setWidth()와 setHeight()가 독립적으로 작동해야 하지만, Square는 항상 정사각형이 되도록 강제 됨

하위 클래스가 상위 클래스의 계약을 깸

 

📌 LSP를 준수한 코드

class Shape {
  getArea() {
    throw new Error("getArea must be implemented");
  }
}

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

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

class Square extends Shape {
  constructor(side) {
    super();
    this.side = side;
  }

  getArea() {
    return this.side * this.side;
  }
}

Rectangle과 Square는 서로 독립적으로 작동하며 상속 구조 유지

 

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

클라이언트는 사용하지 않는 메서드에 의존하지 않아야 함

사용하지 않는 메서드가 많으면 클래스가 불필요한 의존성을 가지게 됨

변경 시 영향 범위를 최소화해 독립적인 개발 가능

 

📌 ISP 위반한 코드

class Worker {
  work() {
    console.log("Working...");
  }

  eat() {
    console.log("Eating...");
  }
}

class Robot extends Worker {
  eat() {
    throw new Error("Robot can't eat"); // ❌ 불필요한 메서드 포함됨
  }
}

 

📌 ISP 준수한 코드

class Workable {
  work() {
    throw new Error("Method must be implemented");
  }
}

class Eatable {
  eat() {
    throw new Error("Method must be implemented");
  }
}

class HumanWorker extends Workable {
  work() {
    console.log("Working...");
  }
  eat() {
    console.log("Eating...");
  }
}

class RobotWorker extends Workable {
  work() {
    console.log("Working...");
  }
}

불필요한 메서드 제거해 각 클래스가 자신의 역할만 수행하도록 함

 

Dependency Inversion Principle: 의존성 역전 원칙

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

코드는 구체적 구현이 아닌, 인터페이스(추상화된 개념)에 의존해야 한다

하위 모듈이 변경되더라도, 상위 모듈에 영향을 주지 않고 유연하게 확장할 수 있음

 

📌 DIP를 위반한 코드

class Keyboard {
  connect() {
    return "Keyboard connected!";
  }
}

class Computer {
  constructor() {
    this.keyboard = new Keyboard(); // ❌ 직접 의존 (구체적인 클래스 사용)
  }

  useKeyboard() {
    console.log(this.keyboard.connect());
  }
}

const myComputer = new Computer();
myComputer.useKeyboard(); // "Keyboard connected!"

Computer 클래스가 Keyboard 클래스에 직접 의존

Keyboard의 변경이 필요하면 Computer 코드도 수정해야 함

 

📌 DIP를 준수한 코드

// 1️⃣ 추상 인터페이스 생성 (Keyboard의 역할만 정의)
class InputDevice {
  connect() {
    throw new Error("connect() must be implemented");
  }
}

// 2️⃣ Keyboard 클래스는 InputDevice를 상속받아 구현
class Keyboard extends InputDevice {
  connect() {
    return "Keyboard connected!";
  }
}

// 3️⃣ Computer는 Keyboard에 직접 의존하지 않고, InputDevice(추상화)에 의존
class Computer {
  constructor(inputDevice) {
    this.inputDevice = inputDevice; // ✅ 인터페이스(추상화)에 의존
  }

  useInputDevice() {
    console.log(this.inputDevice.connect());
  }
}

// 4️⃣ 다른 입력 장치 추가 (Mouse)
class Mouse extends InputDevice {
  connect() {
    return "Mouse connected!";
  }
}

// ✅ Keyboard 사용
const myComputer = new Computer(new Keyboard());
myComputer.useInputDevice(); // "Keyboard connected!"

// ✅ Mouse 사용
const gamingComputer = new Computer(new Mouse());
gamingComputer.useInputDevice(); // "Mouse connected!"

Computer가 Keyboard에 직접 의존하지 않고 InputDevice라는 인터페이스에 의존함

키보드를 바꾸거나 다른 입력 장치를 추가해도 코드 변경이 필요 없음

 


출처

[1] 매일메일. 250130. SOLID 원칙에 대해서 설명해주세요. 110번. https://maeil-mail.kr 

[2] gpt에게 SOLID에 대해 묻다.