인터넷에 좋은 글들이 있지만 저는 제 방식대로 게임만들때에 대비해서 생각하는 편이라 정리해서 올립니다.
일단 이런 규칙들이 생긴 이유에 대해서는 처음에는 몰랐지만 지금은 아주 통감하고 있습니다. 특히 팀프로젝트 할때는 더더욱 그렇습니다.
아래는 http://b-jay.tistory.com/115사이트에서 가져온 도입 이유입니다.
1. 경직성
- 무엇이든 하나를 바꿀 때마다 반드시 다른 것도 바꿔야 하며, 그러고 나면 또 다른 것도 바꿔야 하는 변화의 사슬이 끊이지 않기 때문에 시스템을 변경하기 힘들다.
나 : 팀원에서 묻습니다. 이것좀 바꿔줄 수 있어?
팀원A : 그걸 바꾸면 구조를 싸그리 고쳐야 해요. 안됨.
나 : 아니 어떻게 짰으면 그거 하나 바꿨다고 구조를 다 바꿔?
팀원A : 이미 그렇게 만들었어요 시간 더든다니까요...
나 : 그래 그럼 뭐 어쩔 수 없지...(아니 무슨 프레임워크의 최상위 클래스에 순수가상함수를 넣자는 것도 아니고 뭐만하면 )
2. 부서지기 쉬움
- 시스템에서 한 부분을 변경하면 그것과 전혀 상관없는 다른 부분이 작동을 멈춘다.
팀원B : 형 제가 이 코드에 이거 바꿨는데 형께 작동 안하는데요.
나 : 응 왜?
팀원B : 그 클레스를 만든게 형이라.
나 : (훑어보고)어 미안 이거 이러저러해서 동작이안되네... 고칠께... (아... 내가 왜 이걸 이렇게 짰지?)
3. 부동성
- 시스템을 여러 컴포넌트로 분해해서 다른 시스템에 재사용하기 힘들다.
나 : 이거 기능 빼서 이 클래스로 다시 만들께.
팀원들 : 안되요 그거 바꾸면 구조다 망가짐.
나 : 아니... 그러니까... 왜...
4. 끈끈함(점착성)
- 개발 환경이 배관용 테이프나 풀로 붙인 것처럼 꽉 달라붙은 상태다. 편집 - 컴파일 - 테스트 순환을 한 번 도는 시간이 엄청나게 길다.
나 : 아 이 클래스 너무 헤더가 많은데... 컴파일!
나 : (10분동안 기다리다가 돌아와서) 아직도! 안끝났어!!!!
5. 쓸데없이 복잡함
- 괜히 머리를 굴려서 짠 코드 구조가 굉장히 많다. 이것들은 대개 지금 당장 하나도 필요 없지만 언젠가는 굉장히 유용할지도 모른다고 기대하며 만들었다.
나 : 이걸 이렇게 이렇게 짜면... 애들이 이러이러 써줄꺼야...
2개월후
나 : 아무도 사용하지 않았군 괜히 만든 기능이네...
6. 필요 없는 반복
- 코드를 작성한 프로그래머 이름이 마치 ‘복사’와 ‘붙여넣기’ 같다.
나 : 아니 함수객체나 static 클래스로 함수화 시키면 되지 왤케 같은 함수가 많아.
나 : 전에 형이 만든 기능도 한번밖에 안쓰던데요. 그럴줄 알았죠.
7. 불투명함
- 코드를 만든 의도에 대한 설명을 볼 때 그 설명에 ‘표현이 꼬인다’라는 말이 잘 어울린다.
나 : 이건 이러저러해서 이러저러하기 때문에 이러저러한 기능을 담당하는 클래스야...
모두들 : 어려워...
예제코드
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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | // TEST.cpp : 콘솔 응용 프로그램에 대한 진입점을 정의합니다. // #include "stdafx.h" #include <iostream> using namespace std; class 존재 { private: int 세상속의X축위치; int 세상속의Y축위치; int 세상속의Z축위치; public: virtual void 그려지다() { cout << "나 그려진다!" << endl; } virtual void 타입을말해봐() { cout << "객체다!" << endl; } }; class 생물 : public 존재 { private: TCHAR* 생물의이름; int 공격력; int 방어력; int 수명; public: virtual void 이동() = 0; virtual void 사망() = 0; void 넌뭐니?() { cout << "난 생물이다!!!!!!" << endl; } }; class 괴물 : public 생물 { private: int 생명력; public: virtual void 이동() { cout << "괴물 이동한다." << endl; } virtual void 사망() { cout << "괴물 죽는다." << endl; } void 포효하다(void); void 공격하다(const int 데미지); }; class 사람 : public 생물 { private: int 공격력; int 방어력; public: virtual void 이동() { cout << "사람 이동한다." << endl; } virtual void 사망() { cout << "사람 죽는다." << endl; } virtual void 그려지다() { cout << "사람은 다르게 그려진다" << endl; } void 말하다(void); void 먹다(void); void 공격하다(void) { cout << "사람 이동한다." << endl; } }; int _tmain(int argc, _TCHAR* argv[]) { 사람 철수; 철수.타입을말해봐(); 철수.그려지다(); 철수.넌뭐니(); 철수.이동(); 철수.사망(); cout << endl; 생물* 고질라 = new 괴물; 고질라->이동(); 생물* 울트라맨 = new 사람; 울트라맨->이동(); return 0; } |
1. SRP (단일 책임 원칙)
Single Responsibility Principle
모든 클래스는 단 하나의 책임을 가져야 한다는 것이다. 만들 때나 수정할 때 한가지 이상의 이유가 있어서는 안된다.
클래스를 만들다보면 하나의 클래스에서 너무 많은 역할을 담당할 때가 있다. 특히 게임 오브젝트에 대한 내용이 그렇다.
게임 오브젝트는 상호작용, 렌더링, 게임내데이터등 많은 역할을 맡을 때가 많습니다.
또한 그것들이 또 다시 상호작용한다.
그래서 캐릭터의 렌더링이 이상할때도 게임오브젝트를 보고 충돌이 이상할때도 게임오브젝트를 보게되는 경우가 많습니다.
즉 하나의 클래스에 너무 많은 부담이 가해지는 것이다.
물론 캐릭터에 이상이있다... 라는 점에서는 그것을 담당하는 게임 오브젝트 클래스를 보는게 맞지만.
캐릭터의 렌더링, 캐릭터의 상호작용 등으로 들어가기 시작하면 또다시 복잡해 지기 시작한다.
이럴때는 각 기능들을 다형성을 이용해서 또 다시 클레스로 세분화시키는 작업을 하게된다.
나와 같은 경우에는 게임의 리소스를 관리하는 리소스관리자 클래스를 만들고 또 그 아래 세부적으로 텍스처를 관리하는 텍스처매니저 클래스, 매쉬를 관리하는 매쉬매니저 클래스를 따로 놓거나 아니면 두가지를 애초부터 분리해 놓기도한다.
즉 텍스처 로딩이 이상한데? 라고 하면 텍스처매니저 클래스를 보면 되고 매쉬가 이상하다! 그렇다면 매쉬매니저 클래스를 보면 된다.
2. OCP (개방-폐쇄 원칙)
Open Closed Principle
모든 소프트웨어 구성 요소는 확장에 대해서는 개방되어있지만, 수정에 대해서는 폐쇄되어있다는 원칙이다.
이미 만들어진 클래스를 수정하는 것은 항상 문제가 따른다. 이를 잘 지키기 위해서 위해서 가상함수를 잘 이용해야 한다.
생물 클래스는 이동과 사망을 가지고 있다. 생물들은 마땅히 이동할수 있다고 생각했고 공통되는 기능은 상위 클래스에 정의되어 있거나 혹은 순수가상함수로 꼭 구현해야될 기능으로 정의되어 있다.
즉 생물 클래스는 생물클래스를 통해서 사람이나 괴물 같은 하위 클래스를 확장하는데에는 개방되어 있지만.
반대로 생물 클래스의 이동이나 사망을 수정하거나 할 이유는 없으므로 수정에 대해서는 폐쇠되어 있다.
3. LSP (리스코프 치환 원칙)
Liskov Substitusion Principle
자식 클래스는 언제나 자신의 부모 클래스로 교체될수 있다는 원칙이다.
부모클래스의 자리에 자식클래스가 들어가더라도 잘 작동해야 한다는 것이다.
즉 생물 클래스의 자리에 자식 클래스인 괴물이나 사람이 들어가도 잘 작동해야하고 실제대로 잘 작동한다.
이는 상속의 본질을 의미한다. 이에대해서는 잘 작동 못하게 짜는 것도 굉장히 힘이들지만 기능이 많아지다보면 규칙은 내다버리고 싶은 경우가 다수입니다.
즉 부모의 인터페이스를 자식은 모두 만족할 수 있도록 구현해야 합니다. c++이라면 어떤 부모 클래스형의 포인터가 있을때 그 포인터 형으로 자식 클래스를 넣어 놓습니다.
그 상태에서 어떤 자식 클래스를 넣었을 때는 잘 작동하고 어떤 녀석을 넣었을 때는 작동하지 않는다면 그것은 그 자체로 후일 문제를 일으킬 가능성이 존재합니다.
- LSP는 상속(Inheritance), 다형성(Polymorphism) 과 관련된 원칙.
상속은 코드 재사용 이라는 이유로 과용 될수 있는 기능. 과잉 사용된 상속은 복잡한 계층구조와 커플링을 타이트하게 함으로써 객체지향으로 얻기 위한 유지관리 비용 감소에 악영향을 미치는 요소 중 하나.
라는 의견이 있으니 참고해 주시기 바랍니다.
4. DIP (의존 관계 역전 원칙)
Dependency Inversion Principle
고차원의 모듈은 저차원의 모듈에 의존하면 안된다. 이 두 모듈 모두 추상적인 것에 의존해야 한다.
추상화된것은 반대로 구체적인 것에 의존해서는 안된다. 구체적인 것이 추상적인 것에 의존해야 한다.
상위 클래스는 하위 클래스에 의존해서는 안된다는 법칙이다.
고차원의 클래스는 자신의 하위객체에 어떤것도 의존해서는 안됩니다. DLL을 사용할때 이런일이 많이 발생합니다. DLL을 만들어 놓고 클라이언트에 있는 어떤 객체가 필요하다던가 하는 일이 있는데.
애초에 잘못 짠 것입니다. 추상클래스는 하위의 구현될 가능성이 있는클래스에 의존해서는 안됩니다.
순수가상함수가 있는 클래스는 다른 클래스에 의존해서 작동해서는 안된다는 것입니다.
이는 위의 코드에서도 잘 나와있습니다.
고질라와 울트라맨을 만들어 낼때 둘은 모두 생물이라는 클래스의 포인터 타입으로 동적할당 되었습니다.
여기에서 예를 들자면 꼭 사람으로 할당되어야 하거나 꼭 울트라맨으로 할당되어야 하는 일이 벌어진다면 그것만으로도 객체지향을 해치는 결과가 될 수 있습니다.
또한 이러한 점에 있어서는 함수인자등으로 받을때 하나의 클래스로 대부분이 통용되므로
5. ISP (인터페이스 분리 원칙)
Interface Segregation Principle
클라이언트에서 사용하지 않는 메서드는 사용해선 안된다.
이건 한가지 예를 들어보면 편할 것입니다.
만약 생물 클래스 아래에 사망과 죽음 말고 갑자기 말하다라는 함수가 생성되었습니다.
하지만 괴물은 말하다에 아무것도 할일이 없습니다. 하지만 추상클래스가 어쩔수 없이 구현만 해놓고 안에는 아무것도 넣어놓지 않았습니다.
하지만 이것을 말그대로 어쩔 수 없이 구현한 것입니다.
그런 어쩔수 없는 구현을 하위 클래스로 내려서 어쩔수 없지 않게 만드는 것. 메소드가 필요 없음에도 다른 기능들 때문에 상위의 클래스를 상속받지 않게 상속구조를 정리하거나
메소드를 분리하는 것이 바로 인터페이스 분리 원칙의 핵심입니다.