오늘은 좋은 객체지향 코드를 작성하기 위한 설계 원칙에 대해 깊이 있게 학습했다. 코드 품질을 측정하는 지표인 응집도와 결합도의 개념을 이해하고, "응집도는 높게, 결합도는 낮게" 유지하는 것의 중요성을 배웠다. 🚀 더 나아가, 유지보수와 확장이 용이한 소프트웨어를 만들기 위한 5가지 핵심 원칙인 SOLID 원칙을 구체적인 코드 예제와 함께 익혔다. 이 원칙들을 통해 더 유연하고 견고한 프로그램을 설계하는 방법을 배웠다.
피자배달 클래스에 경로확인, 고객대응, 시간측정 기능만 있는 경우)피자배달 클래스에 웹사이트디자인, 회사마케팅 기능이 섞여 있는 경우)자동차 클래스가 구체적인 디젤엔진이 아닌, 엔진이라는 추상 인터페이스에 의존하는 경우)자동차 클래스가 디젤엔진 클래스를 직접 멤버로 가지는 경우, 전기엔진으로 바꾸려면 자동차 클래스 코드를 수정해야 함)유지보수와 확장이 용이한 설계를 위한 5가지 원칙의 앞 글자를 딴 것이다.
S - 단일 책임 원칙 (SRP, Single Responsibility Principle)
Student 클래스는 학생 정보만 관리하고, 성적 계산은 GradeCalculator, 출력은 StudentPrinter 클래스가 각각 맡도록 분리한다.O - 개방-폐쇄 원칙 (OCP, Open-Closed Principle)
ShapeManager가 if-else로 도형 종류를 판별하는 대신, Shape 인터페이스를 받고 shape.draw()를 호출하도록 설계하면, 새로운 도형(Triangle)이 추가되어도 ShapeManager 코드는 수정할 필요가 없다.L - 리스코프 치환 원칙 (LSP, Liskov Substitution Principle)
정사각형은 직사각형의 하위 타입처럼 보이지만, setWidth()와 setHeight()의 동작 방식이 다르다. (정사각형은 너비를 바꾸면 높이도 바뀌어야 함) 이는 부모(직사각형)의 행동 규약을 어기는 것이므로 LSP를 위반한다. 따라서 상속 관계가 아닌 별도의 클래스로 설계하거나, 공통의 Shape 인터페이스를 상속받도록 해야 한다.I - 인터페이스 분리 원칙 (ISP, Interface Segregation Principle)
복합기 인터페이스에 print(), scan(), fax()가 모두 있다면, 프린터 기능만 필요한 클래스도 scan(), fax()를 억지로 구현해야 한다. IPrinter, IScanner, IFax로 인터페이스를 분리하는 것이 더 낫다.D - 의존 역전 원칙 (DIP, Dependency Inversion Principle)
Computer 클래스가 구체적인 Keyboard, Monitor 클래스에 직접 의존하는 대신, IInputDevice, IOutputDevice라는 인터페이스에 의존하도록 설계한다. 이렇게 하면 키보드 대신 마우스를, 모니터 대신 프린터를 쉽게 교체할 수 있다.#include <iostream>
#include <string>
// 구체적인 Engine 클래스
class Engine {
public:
void start() { std::cout << "Engine started" << std::endl; }
};
// Car가 구체적인 Engine 클래스에 직접 의존 (높은 결합도)
class Car {
private:
Engine engine; // Engine 객체를 멤버로 직접 소유
public:
void startCar() {
engine.start();
std::cout << "Car started" << std::endl;
}
};
int main() {
Car myCar;
myCar.startCar();
// 만약 ElectricEngine을 사용하려면 Car 클래스를 수정해야 함
return 0;
}
#include <iostream>
#include <memory> // std::unique_ptr
// 추상화 계층: Engine 인터페이스 (순수 가상 함수)
class IEngine {
public:
virtual void start() = 0; // 엔진은 시동 기능이 있어야 함
virtual ~IEngine() = default; // 가상 소멸자
};
// 저수준 모듈: 구체적인 엔진 구현
class DieselEngine : public IEngine {
public:
void start() override { std::cout << "Diesel Engine started" << std::endl; }
};
class ElectricEngine : public IEngine {
public:
void start() override { std::cout << "Electric Engine started silently" << std::endl; }
};
// 고수준 모듈: Car는 추상 인터페이스인 IEngine에만 의존 (낮은 결합도)
class Car {
private:
std::unique_ptr<IEngine> engine; // 구체적인 엔진이 아닌 인터페이스에 의존
public:
// 외부에서 생성된 엔진 객체를 주입받음 (의존성 주입)
Car(std::unique_ptr<IEngine> eng) : engine(std::move(eng)) {}
void startCar() {
engine->start(); // 다형성을 통해 실제 엔진의 start()가 호출됨
std::cout << "Car started" << std::endl;
}
};
int main() {
// DieselEngine을 사용하는 자동차
auto diesel = std::make_unique<DieselEngine>();
Car dieselCar(std::move(diesel));
dieselCar.startCar();
std::cout << "---" << std::endl;
// ElectricEngine을 사용하는 자동차
// Car 클래스 코드 수정 없이 새로운 엔진을 유연하게 교체 가능
auto electric = std::make_unique<ElectricEngine>();
Car electricCar(std::move(electric));
electricCar.startCar();
return 0;
}
main 함수에서 구체 클래스를 직접 생성해서 넘겨주는 게 무슨 의미가 있나 싶었다. 하지만 핵심은 Car 같은 고수준 모듈이 구체 클래스의 존재 자체를 모르게 만드는 것이었고, 이를 통해 Car의 재사용성과 확장성이 극대화된다는 것을 이해했다.| 개념 | 설명 | 비고 |
|---|---|---|
| 응집도 (Cohesion) | 클래스 내부 요소들이 얼마나 밀접하게 관련 있는가? | 높을수록(High) 좋다. (하나의 책임) |
| 결합도 (Coupling) | 클래스 간의 의존성이 얼마나 강한가? | 낮을수록(Low) 좋다. (느슨한 연결) |
| SRP (단일 책임) | 클래스는 단 하나의 책임만 가져야 한다. | 클래스를 변경할 이유는 하나뿐. |
| OCP (개방-폐쇄) | 확장에 열려있고, 수정에 닫혀있어야 한다. | 기존 코드 수정 없이 기능 추가. |
| LSP (리스코프 치환) | 자식 클래스는 부모 클래스와 대체 가능해야 한다. | 부모의 행동 규약을 지켜야 함. |
| ISP (인터페이스 분리) | 자신이 쓰지 않는 메서드에 의존하지 않아야 한다. | 거대한 인터페이스를 잘게 분리. |
| DIP (의존 역전) | 구체적인 구현이 아닌, 추상화(인터페이스)에 의존해야 한다. | 결합도를 낮추는 핵심 원칙. |