본문 바로가기
까망 동네/디자인 패턴

객체 지향 프로그래밍 5대 원칙 [SOLID]

by 까망 하르방 2023. 11. 19.
반응형

객체지향 프로그래밍에는 SOLID 원칙이 있다.

유연하고 확장성이 있는 코드 재사용에 목적을 둔다.

 

• SRP (Single Responsibility Principle, 단일책임)

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

• LSP (Liskov's Substitution Principle, 리스코프 치원 원칙)

• ISP (Interface Segregation Principle, 인터페이스 분리 원칙)

• DIP (Dependency Inversion Principle, 의존성 역전 법칙)

 

📌 디자인 패턴(Design Pattern)이란?

 

💻 디자인 패턴(Design Pattern)이란?

👨‍💻 디자인 패턴(Design Pattern)이란? • SW 개발 방법 중에서도 구조적인 문제 해결에 목적을 둔다. • 알고리즘과 같이 특정 문제를 해결하는 Logic 형태보다는 특정 상황에 적용할 수 있는 방

zoosso.tistory.com


 

SRP (Single Responsibility Principle, 단일책임)

• 각 클래스는 하나의 책임만 가져야 한다.

    여러 객체가 책임을 잘 분배해야 기능 확장시 코드 수정을 최소화할 수 있다.

• 클래스는 그 책임(기능)을 캡슐화해야 한다.

응집도를 높이고 결합도를 낮추어야 한다.

 

[예시 코드]

#include <iostream>
#include <string>
#include <vector>

using namespace std;

struct Movie
{
    string title;
    int price;
    int audience;

    Movie(string title, int price) : title(title), price(price)
    {
        audience = 0;
    }

    void discount()
    {
        price -= (price * 0.1);
        printf("price = %d\n", price);
    }

    void watch()
    {
        audience++;
        printf("누적 관객수 = %d\n", audience);
    }
};

int main()
{
    Movie m("Joker", 10000);
    m.discount(); // 9000
    m.watch(); // 1
    m.watch(); // 2
}

 

Movie 클래스에서 할인 기능과 누적 관객수 통계를 모두 처리하고 있다.

하나의 클래스에서 두 가지 책임을 가지고 있어서 SRP를 위반한 예시이다.

누적 관객수 관리를 다른 클래스 Manager에서 맡겨보자

 

 

[예시 코드]

#include <iostream>
#include <string>
#include <vector>

using namespace std;

struct Movie
{
    string title;
    int price;

    Movie(string title, int price) : title(title), price(price) {}
    void discount()
    {
        price -= (price * 0.1);
        printf("price = %d\n", price);
    }
};

struct Manager
{
    int audience;
    Manager()
    {
        audience = 0;
    }
    void watch()
    {
        audience++;
        printf("누적 관객수 = %d\n", audience);
    }
};

int main()
{
    Movie m("Joker", 10000);
    m.discount(); // 9000

    Manager mgr;
    mgr.watch(); // 1
    mgr.watch(); // 2
}

 

복잡한 기능을 구현하다보면 단일 책임만을 가질 수는 없지만

해당 기능을 어떤 Class가 담당할지 디자인 설계가 필요하다.


 

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

• 기존 코드 변경을 하지 않으면서 새로운 기능 추가할 수 있어야 한다.

    변경될 부분과 그렇지 않은 부분을 엄격히 구분해야 한다.

• 확장에는 개방되어 있고  변경에는 폐쇄되어야 한다.

• 추상화(Abstraction)와 다형성(Polymorphism)이 해당

 

[예시 코드]

#include <iostream>
#include <vector>

class Shape {
public:
    virtual double area() const = 0;
};

class Circle : public Shape {
public:
    Circle(double radius) : radius(radius) {}
    double area() const override {
        return 3.14 * radius * radius;
    }
private:
    double radius;
};

class Rectangle : public Shape {
public:
    Rectangle(double width, double height) : width(width), height(height) {}
    double area() const override {
        return width * height;
    }
private:
    double width;
    double height;
};

int main() {
    std::vector<Shape*> shapes;
    shapes.push_back(new Circle(5.0)); // 78.5
    shapes.push_back(new Rectangle(4.0, 6.0)); // 24
    
    for (const Shape* shape : shapes) {
        std::cout << "면적: " << shape->area() << std::endl;
    }

    return 0;
}

 

OCP를 준수한 예제로

새로운 도형을 추가해도 기존 코드를 수정하지 않고

새로운 서브클래스만 추가하여 기능을 확장할 수 있다.


 

LSP (Liskov's Substitution Principle, 리스코프 치원 원칙

• 하위 클래스(subclass)는 상위 클래스(superclass)를 대체할 수 있어야 한다

• 상위 클래스를 사용해도 하위 클래스로 문제없이 동작해야 한다.

 

[예시 코드]

#include <iostream>

class Shape {
public:
    virtual void draw() const {
        std::cout << "Drawing a shape." << std::endl;
    }
};

class Rectangle : public Shape {
public:
    void draw() const override {
        std::cout << "Drawing a rectangle." << std::endl;
    }


    void resize(int width, int height) {
        std::cout << "Resizing the rectangle." << std::endl;
    }
};

void drawShape(const Shape& shape) {
    shape.draw();
}

int main() {
    Shape shape;
    Rectangle rectangle;


    drawShape(shape);      // "Drawing a shape."
    drawShape(rectangle);  // "Drawing a rectangle."


    return 0;
}

 

기본 클래스(Shape)와 파생 클래스(Rectangle)가 존재한다.

drawShape() 메서드에서 어떤 인자를 주느냐에 따라

해당 클래스에 맞게 정상 동작한다.

 

즉, 파생 클래스(Rectangle)가 상위 클래스를 대체할 수 있다.

만약에 draw 메서드를 override 하지 하지 않았다면 LSP를 위반하게 된다.

 

 

✔️ [Is-a 관계] 일관성이 있느냐 !

LSP 원칙 위반 예시


 

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

• 자신이 사용하지 않는 메서드에 의존해서 안된다.

 → 원하는 기능만이 필요하며 이용하지 않는 기능에 대해서 영향을 받지 않는다.

 많은 메서드를 포함한 하나의 인터페이스보다 구체적인 인터페이스 여러개가 좋다.

 → 작은 단위의 인터페이스로 분리 (당장 필요한 단위)

 → 범용 인터페이스 보다 특화된 인터페이스 사용!

 

[예시 코드]

#include <iostream>

class Worker {
public:
    virtual void work() = 0;
    virtual void eat() = 0;
};

class Engineer : public Worker {
public:
    void work() override {
        std::cout << "Engineer work." << std::endl;
    }
    void eat() override {
        std::cout << "Engineer eat." << std::endl;
    }
};

class Chef : public Worker {
public:
    void work() override {
        std::cout << "Chef cook." << std::endl;
    }
    void eat() override {
        std::cout << "Chef eat." << std::endl;
    }
};

int main() {
    Engineer eng;
    Chef chef;

    eng.work();
    eng.eat();

    chef.work();
    chef.eat();

    return 0;
}

 

해당 코드에서 Worker 인터페이스 두 개의 메서드가 있다. (SRP 준수 X)

Engineer와 Chef 클래스에

각각 필요한 메서드만 구현되었다고 했을 때, ISP를 준수하였다.

 

Q) SRP와 무슨 차이가 있을까?

"필요한 기능만 구현한다" 점에서 유사하다.

여러 클래스로 적절히 분리하다보면 SRP를 만족하지만

ISP 만족한다고 SRP를 만족한다고 볼 수 없다.


DIP (Dependency Inversion Principle, 의존성 역전 법칙)

• 고수준 모듈은 저수준 모듈에 의존해서는 안된다.

 → 구체적인 클래스보다 인터페이스나 추상 클래스 의존

• 모듈 간 의존성을 느슨하게 만드는 것이 목표

• 의존 관계를 맺을 때 자주 변화하는 것보다

    변화하기 어려운 것에 의존하라는 것

 

[예시 코드]

#include <iostream>

class Switchable {
public:
    virtual void turnOn() = 0;
    virtual void turnOff() = 0;
};

class Light : public Switchable {
public:
    void turnOn() override {
        std::cout << "Light on." << std::endl;
    }
    void turnOff() override {
        std::cout << "Light off." << std::endl;
    }
};

class Fan : public Switchable {
public:
    void turnOn() override {
        std::cout << "Fan on." << std::endl;
    }
    void turnOff() override {
        std::cout << "Fan off." << std::endl;
    }
};

class Switch {
public:
    Switch(Switchable* device) : device(device) {}


    void operate() {
        if (isOn) {
            device->turnOff();
            isOn = false;
        }
        else {
            device->turnOn();
            isOn = true;
        }
    }

private:
    Switchable* device;
    bool isOn = false;
};

int main() {
    Light light;
    Fan fan;

    Switch lightSwitch(&light);
    Switch fanSwitch(&fan);

    lightSwitch.operate();
    fanSwitch.operate();

    return 0;
}

 

Switch Switchable인터페이스로 Light Fan과 상호작용하고 있다.

즉, [Switch]  [Switchable (Light, Fan)형태

 

고수준 모듈(Switch)이 저수준 모듈(Light, Fan)에 의존하지 않고

추상화(Switchable)에 의존하고 있다.

반응형

댓글