CS/Algorithm

📌 SOLID 원칙 – 객체지향 프로그래밍의 핵심 설계 원칙

nkm 2025. 2. 2. 20:43
728x90
반응형

📌 SOLID 원칙 – 객체지향 프로그래밍의 핵심 설계 원칙

SOLID 원칙은 유지보수성과 확장성을 높이기 위해 객체지향 프로그래밍(OOP)에서 필수적으로 고려해야 하는 5가지 설계 원칙이다.
이를 준수하면 유연하고 확장 가능하며, 결합도가 낮은 코드를 작성할 수 있다.


1. SOLID 원칙 개요

원칙 설명 해결하는 문제

Single Responsibility Principle (SRP) 하나의 클래스는 하나의 책임(기능)만 가져야 한다. 클래스가 너무 많은 역할을 할 경우 유지보수 어려움
Open/Closed Principle (OCP) 기존 코드를 수정하지 않고 기능을 확장할 수 있어야 한다. 새로운 기능 추가 시 기존 코드 변경으로 인한 버그 발생
Liskov Substitution Principle (LSP) 자식 클래스는 부모 클래스를 대체할 수 있어야 한다. 다형성을 유지하지 않으면 코드 예측이 어려움
Interface Segregation Principle (ISP) 클라이언트가 사용하지 않는 메서드에 의존하지 않아야 한다. 불필요한 의존성으로 인해 클래스가 복잡해짐
Dependency Inversion Principle (DIP) 상위 모듈이 하위 모듈에 직접 의존하지 않고, 추상화(인터페이스)에 의존해야 한다. 변경이 어려운 코드 구조로 인해 확장성이 낮음

2. SRP (단일 책임 원칙)

하나의 클래스는 하나의 책임(기능)만 가져야 한다.

  • 즉, 하나의 클래스가 하나의 역할만 수행해야 유지보수와 확장이 쉬워진다.
  • 여러 개의 기능을 한 클래스에서 수행하면 변경이 어려워지고, 재사용성이 떨어진다.

잘못된 예제 (SRP 위반)

class UserService {
  saveToDatabase() { /* DB 저장 로직 */ }
  sendEmail() { /* 이메일 발송 로직 */ }
}
  • UserService 클래스가 DB 저장과 이메일 전송이라는 두 개의 책임을 가짐 → 유지보수 어려움.

올바른 예제 (SRP 적용)

class UserRepository {
  saveToDatabase() { /* DB 저장 */ }
}

class EmailService {
  sendEmail() { /* 이메일 발송 */ }
}
  • UserRepository → 데이터 저장 역할
  • EmailService → 이메일 전송 역할
  • 각 클래스가 하나의 책임만 가지므로 유지보수 용이!

3. OCP (개방-폐쇄 원칙)

기존 코드를 수정하지 않고 기능을 확장할 수 있어야 한다.

  • 새로운 기능을 추가할 때 기존 코드 변경 없이 확장 가능하도록 설계해야 한다.

잘못된 예제 (OCP 위반)

class PaymentService {
  processPayment(type: string, amount: number) {
    if (type === "credit") {
      console.log(`💳 ${amount}원 신용카드 결제`);
    } else if (type === "paypal") {
      console.log(`💻 ${amount}원 PayPal 결제`);
    }
  }
}
  • 새로운 결제 방식 추가 시, 기존 코드 수정이 필요 → 유지보수 어려움.

올바른 예제 (OCP 적용 – 확장 가능)

interface PaymentStrategy {
  pay(amount: number): void;
}

class CreditCardPayment implements PaymentStrategy {
  pay(amount: number): void {
    console.log(`💳 ${amount}원 신용카드 결제`);
  }
}

class PaypalPayment implements PaymentStrategy {
  pay(amount: number): void {
    console.log(`💻 ${amount}원 PayPal 결제`);
  }
}

class Order {
  constructor(private payment: PaymentStrategy) {}

  checkout(amount: number) {
    this.payment.pay(amount);
  }
}
  • 새로운 결제 방식이 필요하면 새로운 클래스(BitcoinPayment 등)만 추가하면 됨.
  • 기존 코드(Order 클래스)는 변경되지 않음! (OCP 적용)

4. LSP (리스코프 치환 원칙)

자식 클래스는 부모 클래스를 대체할 수 있어야 한다.

  • 즉, 부모 클래스의 객체를 자식 클래스로 교체해도 코드가 정상적으로 작동해야 한다.
  • 만약 자식 클래스가 부모 클래스의 기대 동작을 깨트린다면, LSP 위반!

잘못된 예제 (LSP 위반)

class Rectangle {
  constructor(protected width: number, protected height: number) {}

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

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

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

class Square extends Rectangle {
  setWidth(width: number) {
    this.width = width;
    this.height = width; // 정사각형이므로 height도 변경됨!
  }

  setHeight(height: number) {
    this.width = height;
    this.height = height;
  }
}
  • Rectangle의 setWidth()와 setHeight()는 독립적으로 동작해야 하지만, Square에서는 하나를 바꾸면 다른 값도 변경됨 → LSP 위반.

올바른 예제 (LSP 적용)

interface Shape {
  getArea(): number;
}

class Rectangle implements Shape {
  constructor(protected width: number, protected height: number) {}

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

class Square implements Shape {
  constructor(private side: number) {}

  getArea(): number {
    return this.side * this.side;
  }
}
  • Rectangle과 Square를 공통 인터페이스(Shape)로 추상화하여 문제 해결.

5. ISP (인터페이스 분리 원칙)

"클라이언트가 사용하지 않는 메서드에 의존하지 않아야 한다."

  • 즉, 인터페이스를 최소한으로 분리하여 필요한 기능만 구현하도록 설계.

잘못된 예제 (ISP 위반)

interface Worker {
  work(): void;
  eat(): void;
}

class HumanWorker implements Worker {
  work(): void {
    console.log("사람이 일하고 있습니다.");
  }

  eat(): void {
    console.log("사람이 점심을 먹습니다.");
  }
}

class RobotWorker implements Worker {
  work(): void {
    console.log("로봇이 일하고 있습니다.");
  }

  eat(): void {
    throw new Error("로봇은 음식을 먹을 수 없습니다!"); // 🚨 ISP 위반
  }
}
  • RobotWorker는 eat() 메서드가 필요 없음! (ISP 위반)

올바른 예제 (ISP 적용)

interface Workable {
  work(): void;
}

interface Eatable {
  eat(): void;
}

class HumanWorker implements Workable, Eatable {
  work(): void {
    console.log("사람이 일하고 있습니다.");
  }

  eat(): void {
    console.log("사람이 점심을 먹습니다.");
  }
}

class RobotWorker implements Workable {
  work(): void {
    console.log("로봇이 일하고 있습니다.");
  }
}
  • 인터페이스를 분리하여 각 클래스가 필요한 기능만 구현!

6. DIP (의존성 역전 원칙)

"상위 모듈이 하위 모듈에 직접 의존하지 않고, 추상화(인터페이스)에 의존해야 한다."

  • 즉, 클래스가 직접 객체를 생성하지 않고, 외부에서 주입받도록 설계해야 한다.

올바른 예제 (DIP 적용)

interface Database {
  connect(): void;
}

class MySQLDatabase implements Database {
  connect(): void {
    console.log("MySQL 연결 완료!");
  }
}

class UserService {
  constructor(private db: Database) {}

  getUser() {
    this.db.connect();
    console.log("사용자 정보 가져오기");
  }
}

const userService = new UserService(new MySQLDatabase());
userService.getUser();
  • UserService가 MySQLDatabase에 직접 의존하지 않고, 추상화(Database 인터페이스)에 의존함.

🚀 결론

SOLID 원칙을 적용하면 유지보수성과 확장성이 뛰어난 코드를 작성할 수 있다!
각 원칙을 TypeScript로 구현하여 실전에서 활용 가능!

🔥 다음 단계 → 디자인 패턴 자세히 설명! 🚀

728x90
반응형