프로그래밍

객체 지향 설계 - SOLID 원칙

GONII 2025. 3. 28. 12:44

SOLID 원칙은 객체 지향 프로그래밍(OOP)에서 유지보수성과 확장성을 높이기 위한 5가지 설계 원칙을 의미합니다. SOLID는 각 원칙의 앞 글자를 따서 만든 약어입니다.


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

클래스는 단 하나의 책임만 가져야 합니다.

잘못된 예제

#include <iostream>
#include <fstream>
using namespace std;

class Report {
public:
    void generateReport() { cout << "리포트 생성\n"; }
    void printReport() { cout << "리포트 출력\n"; }
    void saveToFile() { ofstream file("report.txt"); file << "리포트 내용"; file.close(); }
};
  • Report 클래스가 생성, 출력, 저장을 모두 담당 → 단일 책임 원칙 위반.

개선된 예제

#include <iostream>
#include <fstream>
using namespace std;

class Report {
public:
    string generate() { return "리포트 내용"; }
};

class ReportPrinter {
public:
    void print(const string& content) { cout << "출력: " << content << endl; }
};

class ReportSaver {
public:
    void saveToFile(const string& content) {
        ofstream file("report.txt");
        file << content;
        file.close();
        cout << "파일 저장 완료" << endl;
    }
};

int main() {
    Report report;
    ReportPrinter printer;
    ReportSaver saver;

    string content = report.generate();
    printer.print(content);
    saver.saveToFile(content);

    return 0;
}
  • 각각의 클래스가 하나의 역할만 수행하도록 분리.

2️⃣ 개방-폐쇄 원칙 (OCP: Open/Closed Principle)

코드는 확장에는 열려 있고, 수정에는 닫혀 있어야 합니다.

잘못된 예제

class PaymentProcessor {
public:
    void processPayment(string type) {
        if (type == "CreditCard") cout << "신용카드 결제\n";
        else if (type == "PayPal") cout << "페이팔 결제\n";
    }
};
  • 새로운 결제 방식 추가 시 processPayment()를 수정해야 함 → OCP 위반.

개선된 예제

#include <iostream>
using namespace std;

class Payment {
public:
    virtual void pay() = 0; // 추상 메서드
    virtual ~Payment() = default;
};

class CreditCardPayment : public Payment {
public:
    void pay() override { cout << "신용카드 결제\n"; }
};

class PayPalPayment : public Payment {
public:
    void pay() override { cout << "페이팔 결제\n"; }
};

class PaymentProcessor {
public:
    void processPayment(Payment& payment) {
        payment.pay();
    }
};

int main() {
    CreditCardPayment cc;
    PayPalPayment pp;

    PaymentProcessor processor;
    processor.processPayment(cc);
    processor.processPayment(pp);

    return 0;
}
  • 새로운 결제 방식 추가 시 기존 코드 수정 없이 Payment 인터페이스만 구현하면 됨.

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

자식 클래스는 부모 클래스를 완전히 대체할 수 있어야 합니다.

잘못된 예제

class Rectangle {
protected:
    int width, height;
public:
    void setWidth(int w) { width = w; }
    void setHeight(int h) { height = h; }
    int getArea() { return width * height; }
};

class Square : public Rectangle {
public:
    void setWidth(int w) { width = height = w; } // 문제 발생
    void setHeight(int h) { width = height = h; } // 문제 발생
};
  • Rectangle을 기대하고 사용하는 코드에서 Square가 예상과 다르게 동작 → LSP 위반.

개선된 예제

class Shape {
public:
    virtual int getArea() = 0;
    virtual ~Shape() = default;
};

class Rectangle : public Shape {
protected:
    int width, height;
public:
    Rectangle(int w, int h) : width(w), height(h) {}
    int getArea() override { return width * height; }
};

class Square : public Shape {
private:
    int side;
public:
    Square(int s) : side(s) {}
    int getArea() override { return side * side; }
};
  • Rectangle과 Square를 Shape 인터페이스를 통해 분리하여 LSP 준수.

4️⃣ 인터페이스 분리 원칙 (ISP: Interface Segregation Principle)

클라이언트가 사용하지 않는 인터페이스에 의존하지 않아야 합니다.

잘못된 예제

class Worker {
public:
    virtual void work() = 0;
    virtual void eat() = 0; // 문제 발생
};
class Robot : public Worker {
public:
    void work() override { cout << "로봇 작업 중\n"; }
    void eat() override { /* 로봇은 먹지 않음 */ } // 불필요한 메서드 구현
};
  • Robot이 eat() 메서드를 구현해야 하는 문제 발생.

개선된 예제

class Workable {
public:
    virtual void work() = 0;
    virtual ~Workable() = default;
};

class Eatable {
public:
    virtual void eat() = 0;
    virtual ~Eatable() = default;
};

class Human : public Workable, public Eatable {
public:
    void work() override { cout << "사람이 일함\n"; }
    void eat() override { cout << "사람이 식사함\n"; }
};

class Robot : public Workable {
public:
    void work() override { cout << "로봇이 일함\n"; }
};
  • Workable과 Eatable을 분리하여 필요한 인터페이스만 상속하도록 개선.

5️⃣ 의존 역전 원칙 (DIP: Dependency Inversion Principle)

구체적인 구현이 아니라 추상화(인터페이스)에 의존해야 합니다.

잘못된 예제

class Keyboard {};
class Monitor {};

class Computer {
private:
    Keyboard keyboard;
    Monitor monitor;
};
  • Computer가 Keyboard와 Monitor의 구체적인 구현에 직접 의존 → DIP 위반.

개선된 예제

class Keyboard {
public:
    virtual void input() = 0;
    virtual ~Keyboard() = default;
};

class Monitor {
public:
    virtual void display() = 0;
    virtual ~Monitor() = default;
};

class MechanicalKeyboard : public Keyboard {
public:
    void input() override { cout << "기계식 키보드 입력\n"; }
};

class LEDMonitor : public Monitor {
public:
    void display() override { cout << "LED 모니터 표시\n"; }
};

class Computer {
private:
    Keyboard& keyboard;
    Monitor& monitor;
public:
    Computer(Keyboard& k, Monitor& m) : keyboard(k), monitor(m) {}
    void use() {
        keyboard.input();
        monitor.display();
    }
};

int main() {
    MechanicalKeyboard keyboard;
    LEDMonitor monitor;
    Computer pc(keyboard, monitor);
    pc.use();
    return 0;
}
  • Computer가 구체적인 클래스가 아닌 **인터페이스(추상 클래스)**에 의존하도록 변경.

🔥 정리

원칙 개념 개선 방법
SRP 단일 책임 역할을 분리하여 여러 클래스로 나눈다.
OCP 개방-폐쇄 인터페이스(추상 클래스)를 사용하여 확장 가능하도록 한다.
LSP 리스코프 치환 부모 클래스를 완벽하게 대체할 수 있도록 설계한다.
ISP 인터페이스 분리 불필요한 메서드 구현을 피하기 위해 인터페이스를 분리한다.
DIP 의존 역전 구체적인 구현이 아닌 **추상화(인터페이스)**에 의존하도록 한다.

이 원칙을 잘 지키면 유지보수성과 확장성이 뛰어난 C++ 코드를 작성할 수 있습니다! 🚀

반응형

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

시스템콜(System Call) 이란?  (0) 2025.04.04