C++: 객체 지향 설계(Object Oriented Design)
C++: 객체 지향 설계(Object Oriented Design)
객체 지향 설계(Object Oriented Design, OOD)
- OOP와 연관이 깊은 개념으로, OOP가 프로그램과 코드의 구현방식에 대한 개념과 이론이였다면, 객체 지향 설계(OOD)는 프로그램/시스템의 구조와 설계에 대한 방법론이다. 즉, 보다 거시적인 시각에서의 접근법이다.
- 어떤 객체와 책임이 필요한지.
- 어떻게 상호작용할지.
- 클래스와 인터페이스 구조를 어떤식으로 만들지.
- 남의 코드을 자신이 사용할 일도, 반대로 자신의 코드를 남이 사용할 일도 현업에서 많기에 객체 지향 설계를 바탕으로 설계하고, 작성된 객체지향적 코드를 이해할 수 있는 능력 또한 중요하다.
- 높은 응집도와 낮은 결합도 를 구현한 구조가 객체지향적으로 설계된 구조이다.
응집도(Cohesion)
- 클래스 혹은 모듈의 내부 요소들이 얼마나 목적성에 맞게 집중되어 있는지를 나타내는 지표이다.
- 일반적으로는 응집도가 높은 코드가 더 좋은 코드다.
- 응집도가 낮으면
- 불필요한 기능들이 많이 포함되어있다.
- 추후에 수정에서 변경 사항이 많다.
- 오류 발생시 어떤 지점이 문제인지 찾기 어렵다.
- 클래스가 복잡해지고 재사용성이 떨어진다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
#include <iostream> #include <string> using namespace std; class Utility{ public: static int Increment(int val){ return ++val; } static string ReverseString(string s){ string temp = ""; for(auto i = s.rbegin(); i != s.rend(); i++){ temp.push_back(*i); } return temp; } //감소 함수, 첫 글자 반환 함수도 Utility 안에 들어갈 것. //관련 없는 기능들의 집합 => 응집도 낮음. }; int main(){ cout << Utility::Increment(10) << endl; //11 cout << Utility::ReverseString("abcde") << endl; //edcba }
- 응집도가 높으면
- 필요한 기능들만 객체 단위로 모여있다.
- 추후 수정에서 바꿀 객체만 바뀌기에 변경사항이 적다.
- 오류 발생시 특정한 클래스 단위로 좁혀지기에 디버깅에 용이하다.
- 클래스가 간결하게 유지되고 재사용성이 높다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
#include <iostream> #include <string> using namespace std; class CalcUtil{ public: static int Increment(int val){ return ++val; } //계산 관련된 감소 함수만 CalcUtil에 추가 }; class StrUtil{ public: static string ReverseString(string s){ string temp = ""; for(auto i = s.rbegin(); i != s.rend(); i++){ temp.push_back(*i); } return temp; } //문자열 관련 첫글자 반환 함수만 StrUtil에 추가 }; //관련 높은 기능들이 각각의 클래스에 모임 =>응집도 높음. int main(){ cout << CalcUtil::Increment(10) << endl; //11 cout << StrUtil::ReverseString("abcde") << endl; //edcba }
- 응집도가 낮으면
결합도(Coupling)
- 클래스 혹은 모듈들이 서로 얼마나 의존하는지를 나타내는 지표이다.
- 결합도가 높으면
- 한 클래스가 다른 클래스에 직접적으로 포함/호출된다.
- 변경사항이 자신 뿐 아니라 다른 클래스에도 영향을 준다.
- 수정에 유연하지 못하다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
#include <iostream> #include <string> using namespace std; class WarriorCharacter{ public: void Attack(){ cout << "Warrior is raging!" << endl; } }; class BattleManager{ public: static void BeginAttack(WarriorCharacter Player){ Player.Attack(); //만약 다른 캐릭터가 추가된다면? //다른 캐릭터 클래스 구현 후 Attack 함수 수정 필요. } }; int main(){ BattleManager BM; WarriorCharacter A; WarriorCharacter B; BM.BeginAttack(A); BM.BeginAttack(B); }
- 결합도가 낮으면
- 다른 클래스에 대한 상호작용이 간접적이다.
- 변경사항으로 인해 타 클래스가 받는 영향이 제한적이다.
- 수정에 있어 유연하다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
#include <iostream> #include <string> using namespace std; class Character{ //캐릭터 추상 클래스 public: virtual ~Character() = default; virtual void Attack() = 0; //공격 순수 가상 함수 }; class Warrior: public Character{ //캐릭터를 상속받는 기존 전사 클래스 public: void Attack(){ //공격 구현 cout << "Warrior is raging!" << endl; } }; class Mage: public Character{ //추가된 마법사 클래스 public: void Attack(){ //공격 구현 cout << "Mage is casting Frostbite!" << endl; } }; class BattleManager{ public: static void BeginAttack(Character& Player){ Player.Attack(); //캐릭터를 매개변수로 받아 별도의 수정 불필요. } }; int main(){ Warrior A; Mage B; BattleManager::BeginAttack(A); BattleManager::BeginAttack(B); }
- 결합도가 높으면
- 클래스 혹은 모듈들이 서로 얼마나 의존하는지를 나타내는 지표이다.
- 이런 객체지향적 목적성을 위해 SOLID, 디자인 패턴등 여러 설계 상의 기법들이 사용될 수 있다.
SOLID
- OOD, OOP에 있어 보편적으로 쓰이는 5가지 원칙이다.
- 유지보수성, 확장성, 변경에 대한 유연성을 목표로 하며, 이는 OOD의 목적성과 부합한다.
- 단일 책임 원칙(Single Responsiblilty Principle, SRP)
- 각 클래스는 하나의 역할/책임만을 가진다.
- 응집도와 관련이 높은 원칙이다.
- 응집도의 코드가 Utility 클래스에서 각각 연관된 분류의 Util 클래스들로 분리시킨 것을 단일 책임 원칙의 예시가 될 수 있다. 2. 개방 폐쇄 원칙(Open/Close Principle, OCP)
- 확장에는 열려있고, 수정에는 닫혀있어야 한다.
- 기존 코드의 수정 없이 기능이 확장될 수 있어야 한다.
- 위 결합도 예시 코드가 mage 클래스를 기존 BattleManager코드의 수정 없이 추가할 수 있도록 OCP에 맞춰 수정한 예시가 될 수 있다. 3. 리스코프 치환 원칙(Liskov Substitution Principle, LSP)
- 컴퓨터 과학자 바바라 리스코프(Barbara Liskov)가 제시한 원칙으로, 자식 클래스가 부모 클래스를 완전히 대체할 수 있어야 한다.
- 즉, 부모 클래스에서 정해놓은 규칙(계약)을 지킬 수 없는 자식 클래스가 있어서 프로그램 전체의 작동을 보장하지 못한다면, 그 예외적인 클래스는 자식이 아닌 별도의 클래스로 분리하는게 좋다.
- 아래같은 코드는 부모 클래스(Car)를 기준으로 작성된 외부의 클라이언트 함수(isRaceReady) 가 자식 클래스중 계약의 위반이 있다는 것을 확인하지 못하기에 정상작동(isRaceReady를 통한 true 반환)하지 못한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
#include <iostream> #include <string> #include <vector> using namespace std; class Car{ //차량 클래스 protected: bool isEngineOn; public: Car(){ isEngineOn = false; //시동 상태 변수 } virtual void StartEngine(){ //시동 함수, isEngineOn 을 true로 변경. cout << "engine is on" << endl; isEngineOn = true; } bool GetEngineStatus(){ return isEngineOn; } }; class Camaro: public Car{ //쉐보레 카마로 클래스 virtual void StartEngine(){ //시동 함수 구현 cout << "Chevrolet Camaro is on" << endl; isEngineOn = true; //정상적으로 엔진 시동 } }; class ModelS: public Car{ //테슬라 모델 S 클래스 virtual void StartEngine(){ //시동 함수 구현 cout << "Telsa Model S doesn't have an engine." << endl; //전기차라 엔진 시동 불가 } }; class RaceManager{ //경주 관리 클래스 public: static bool isRaceReady(vector<Car*> cars){ //자동차 준비 확인 함수 for(Car* car: cars){ if(car->GetEngineStatus()) continue; else return false; } return true; } }; int main(){ Car* A = new Camaro(); Car* B = new ModelS(); A->StartEngine(); B->StartEngine(); //ModelS 준비 안됨! => Race 준비 안됨! if(RaceManager::isRaceReady({A, B})) cout << "race is ready!" << endl; else cout << "race is not ready!" << endl; }
- 이 경우 자식 클래스를 분류에 맞게 EnginedCar와 ElectricCar로 분리하고, Car에서 준비의 기준을 Engine으로 잡는게 아닌 활성화 여부 나타내는 변수를 따로 두는 편이 LSP를 만족하기 좋을 것이다. 4. 인터페이스 분리 원칙(Interface Segregation Principle, ISP)
- 클라이언트는 사용하지 않는 인터페이스에 의존해선 안된다.
- 하나의 거대한 인터페이스는 불필요한 기능들을 클라이언트가 포함하도록 만든다.
- 따라서 기능별로 각각 분리된 인터페이스로 세분화해야한다.
- 응집도 예시에서 묘사한 Utility라는 하나의 큰 클래스(엄밀히 말해서는 함수를 사용하기 위한 인터페이스에 가깝다)를 사용하기보단, CalcUtil이나 StrUtil을 필요한 것들만 사용하는것이 ISP에 부합한다고 봐야 할것이다. 5. 의존 역전 원칙(Dependency Inversion Principle, DIP)
- 고수준 모듈은 저수준 모듈에 직접 의존하면 안된다.
- 고수준 모듈은 추상화를 통해 간접적으로 의존해야 한다.
- 위 결합도 코드에서는 기존 BattleManager가 WorriorCharacter에 직접 접근하지만, Character클래스를 통해 간접적으로 접근하도록 하여 DIP를 만족하였다. 객체 지향은 학교 시절 배우긴 했었지만 가장 햇갈리는 개념중 하나였다. 굳이 필요한가?, 왜 굳이 저렇게 프로그래밍 해야하지? 싶은 의문이 많이 들었었고 결국 머리로만 간략히 이해하게 됬었다. 지금 프로그래밍과 언리얼 그래도 더 공부한 후에 다시 보게 되니, 경험이 지금보다도 부족하던 그 시절엔 받아들이기 힘들었을 수도 있겠다는 생각은 든다. 단순한 코딩테스트, 알고리즘 문제 수준이라면 보통은 이런 구조를 사용하지 않는 편이 더 간결하게 해결되는 경우가 많았다. 그렇지만 장기적으로 유지보수성이 중요한, 내지는 큰 규모의 협업을 필요로 하는 프로젝트에서 중요한 개념이 되는 것이고, 혼자 해결해내는 일이 잦았던 그동안의 공부와 코딩 방식에 익숙했기에 그 때는 와닫지 않았던 것이 아닐까 생각한다.
- OOP와 연관이 깊은 개념으로, OOP가 프로그램과 코드의 구현방식에 대한 개념과 이론이였다면, 객체 지향 설계(OOD)는 프로그램/시스템의 구조와 설계에 대한 방법론이다. 즉, 보다 거시적인 시각에서의 접근법이다.
This post is licensed under CC BY 4.0 by the author.