객체지향 설계에서 유지 보수성과 확장성을 높이기 위한 5가지 원칙을 의미합니다.
하나의 클래스는 하나의 기능 또는 책임(관심사)에만 집중해야 한다는 원칙입니다. 클래스가 여러 책임을 가지게 되면, 한 책임의 변경이 다른 책임과 관련된 코드에 영향을 미칠 수 있어 시스템이 복잡해지고 취약해집니다.
<잘못된 예>
#include <iostream> #include <string> class Student { public: void setName(const std::string& name) { this->name = name; } void displayDetails() { std::cout << "Student Name: " << name << std::endl; } void calculateGrade(int score) { if (score >= 90) { std::cout << "Grade: A" << std::endl; } else if (score >= 80) { std::cout << "Grade: B" << std::endl; } else { std::cout << "Grade: C" << std::endl; } } private: std::string name; };
- Student 클래스는 학생이름을 저장,출력,점수계산 하는 3개의 기능이있다
- 각각의 기능을 수정해야할떄마다 Student클래스를 수정해야하며 Student 클래스와 연관된 클래스에도 영향을 미칠수 있어 시스템이 복잡해지고 취약해진다.
<좋은 예>
#include <iostream> #include <string> // 학생 정보 관리 클래스 class Student { public: void setName(const std::string& name) { this->name = name; } std::string getName() const { return name; } private: std::string name; }; // 성적 계산 클래스 class GradeCalculator { public: void calculateGrade(int score) { if (score >= 90) { std::cout << "Grade: A" << std::endl; } else if (score >= 80) { std::cout << "Grade: B" << std::endl; } else { std::cout << "Grade: C" << std::endl; } } }; // 출력 클래스 class StudentPrinter { public: void displayDetails(const Student& student) { std::cout << "Student Name: " << student.getName() << std::endl; } };
- 정보를 관리하는 클래스, 성적을 계산하는 클래스, 출력을 하는 클래스로 나누어져 있어 기능을 변경할때도 다른 클래스에 미치는 영향력이 낮아진다.
확장에는 열려 있어야 하고, 수정에는 닫혀있어야 한다는 개념입니다.
즉, 새로운 기능을 추가할 때 기존의 코드를 수정하지 않고도 기능을 확장할 수 있어야 한다는 원칙입니다. 이는 주로 추상화(인터페이스, 추상 클래스)와 다형성을 통해 달성됩니다.
<잘못된 예>
class ShapeManager { public: void drawShape(int shapeType) { if (shapeType == 1) { // 원 그리기 } else if (shapeType == 2) { // 사각형 그리기 } } };
- 새로운 도형이 추가될때마다 기존 ShapeManager 클래스에서 수정을 해야한다.
<좋은 예>
class Shape { public: virtual void draw() = 0; // 순수 가상 함수 }; class Circle : public Shape { public: void draw() { // 원 그리기 } }; class Square : public Shape { public: void draw() { // 사각형 그리기 } }; class ShapeManager { public: void drawShape(Shape& shape) { shape.draw(); // 다형성 활용 } };
- 인터페이스나 추상클래스를 만들고 상속 받아서 사용하면 새로운 도형이 추가되도 기존 코드 ShapeManager 클래스에서 수정할필요없이 기능이 확장된다.
자식 클래스는 부모 클래스에서 기대되는 행동을 보장해야 합니다.
객체지향 프로그래밍에서 다형성을 활용할 때,
부모 클래스를 사용하는 코드가 자식 클래스로 대체되더라도 정상적으로 동작해야 합니다.
부모 클래스를 사용하는 곳에 자식 클래스를 대신 넣어도, 원래의 의도대로 프로그램이 동작해야 합니다. 상속을 'is-a' 관계에 맞게 올바르게 사용했는지 확인하는 척도이다.
<잘못된 예>
#include <iostream> class Rectangle { public: virtual void setWidth(int w) { width = w; } virtual void setHeight(int h) { height = h; } int getWidth() const { return width; } int getHeight() const { return height; } int getArea() const { return width * height; } private: int width = 0; int height = 0; }; class Square : public Rectangle { public: void setWidth(int w) override { Rectangle::setWidth(w); Rectangle::setHeight(w); // 정사각형은 너비와 높이가 같아야 함 } void setHeight(int h) override { Rectangle::setHeight(h); Rectangle::setWidth(h); // 정사각형은 너비와 높이가 같아야 함 } }; void testRectangle(Rectangle& rect) { rect.setWidth(5); rect.setHeight(10); std::cout << "Expected area: 50, Actual area: " << rect.getArea() << std::endl; } int main() { Rectangle rect; testRectangle(rect); // Expected area: 50 Square square; testRectangle(square); // Expected area: 50, Actual area: 100 (문제 발생) return 0; }
- 정사각형은 직사각형의 한 유형이지만 정사각형은 직사각형이 될수없다.
- 사각형의 가로, 세로 길이를 다르게 호출시 정사각형으로 치환하면 오류를 일으킨다.
<좋은 예>
#include <iostream> class Shape { public: virtual int getArea() const = 0; // 넓이를 계산하는 순수 가상 함수 }; class Rectangle : public Shape { public: void setWidth(int w) { width = w; } void setHeight(int h) { height = h; } int getWidth() const { return width; } int getHeight() const { return height; } int getArea() const override { return width * height; } private: int width = 0; int height = 0; }; class Square : public Shape { public: void setSide(int s) { side = s; } int getSide() const { return side; } int getArea() const override { return side * side; } private: int side = 0; }; void testShape(Shape& shape) { std::cout << "Area: " << shape.getArea() << std::endl; } int main() { Rectangle rect; rect.setWidth(5); rect.setHeight(10); testShape(rect); // Area: 50 Square square; square.setSide(7); testShape(square); // Area: 49 return 0; }
- 모든 도형이 공통 인터페이스인 넓이를 부모 클래스가 가지고 그 도형을 상속 받아서 사용한다.
클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다는 원칙입니다.
즉, 하나의 거대한 인터페이스보다는 역할별로 세분화된 인터페이스를 만들어,
필요한 기능만 구현하도록 설계해야 합니다.
SRP가 클래스의 책임 분리라면, ISP는 인터페이스의 책임 분리입니다.
<잘못된 예>
class Machine { private: public: Machine() {} virtual void print() { // 세부 기능 구현 } virtual void scan() { // 세부 기능 구현 } };
- Machnine이라는 인터페이스를 상속받은 클래스는 출력하는 기능과 스캔하는 기능을 다 구현해야한다.
<좋은 예>
#include <iostream> using namespace std; class Printer { public: virtual void print() = 0; }; class Scanner { public: virtual void scan() = 0; }; class BasicPrinter : public Printer { public: void print() override { cout << "BasicPrinter: 프린트" << endl; } }; class NewPrinter : public Printer, public Scanner { public: void print() override { cout << "NewPrinter: 프린트" << endl; } void scan() override { cout << "NewPrinter: 스캔" << endl; } }; int main() { NewPrinter np; BasicPrinter bp; np.print(); np.scan(); bp.print(); return 0; }
- 프린트하는 인터페이스와, 스캔하는 인터페이스로 2개로 나누어서 원하는 인터페이스를 사용하는 자식 클래스를 만들어서 사용한다.
구체적인 구현(하위 수준 모듈)에 의존하는 것이 아니라, 인터페이스나 추상 클래스 같은 추상화 계층을 두어 결합도를 낮추는 것이 좋은 설계입니다.
<잘못된 예>
#include <string> class Keyboard { public: std::string getInput() { return "입력 데이터"; } }; class Monitor { public: void display(const std::string& data) { // 출력 } }; class Computer { Keyboard keyboard; Monitor monitor; public: void operate() { std::string input = keyboard.getInput(); monitor.display(input); } };
- Computer 클래스는 keyboard,Monitor 클래스에 강하게 결합되어있다.
- 키보드와 모니터가 바뀐다면 컴퓨터클래스도 바뀐 키보드와 모니터에 맞춰서 고쳐야한다.
<좋은 예>
#include <string> class InputDevice { public: virtual std::string getInput() = 0; }; class OutputDevice { public: virtual void display(const std::string& data) = 0; }; class Keyboard : public InputDevice { public: std::string getInput() override { return "키보드 입력 데이터"; } }; class Monitor : public OutputDevice { public: void display(const std::string& data) override { // 화면에 출력 } }; class Computer { private: InputDevice* inputDevice; OutputDevice* outputDevice; public: Computer(InputDevice* input, OutputDevice* output) : inputDevice(input), outputDevice(output) {} void operate() { std::string data = inputDevice->getInput(); outputDevice->display(data); } };
- 키보드는 입력과 관련된 것을 인터페이스로 만들고, 모니터는 출력과 관련된 것을 인터페이스로 만들어서 인터페이스나 추상클래스에만 의존하게 만들어 결합도를 낮출수있다.