본문 바로가기

차장님의 이야기

유니티의 CBD(Component-Based Development) 구현 해보기

게임 개발을 할 때, 또는 무언가를 개발 할 때 OOP 기반으로 개발을 주로 하는데

OOP의 단점이 상속을 계속 하게 되면 내용이 비대 해지고 관리가 힘들어 지며, 거대한 코드가 되고 만다.

 

그래서 상속(수직적) 구성이 아닌, 컴포넌트(수평적) 기반으로 새로운 패턴이 나왔다.

 

상속을 하는것이 아닌, "컴포넌트" 단위로 개발을 하는것이고, 새로운 기능을 추가 할 때 마다 "컴포넌트"를 만들어서

추가 하면 된다.

 

실제로 동작하는 방식이나, 코드를 보면 엄청 쉽게 이해가 된다.

 

코드 작성은 C++로 하였고, 공부용으로 사용 했다.

 

#include <iostream>
#include <string>
#include <list>

class component;
class object;
class userComponent;
class adminComponent;
class manageComponent;

제일 먼저 사용하는 헤더, 그리고 class를 정방 선언을 해주었다.

class component {
public:
    object* attachParent = nullptr;

    virtual void print() {
    	std::cout << "component\n";
    }
};

먼저 component-base 라고 하였으니 component를 만들어 준다.

component는 혼자서 동작을 하지 못하므로 어디에 붙는지 부모를 만들어 주고

테스트 하기 위하여 print 함수를 만들었다.

class object {
private:
    std::list<component*> components;

public:
	// componet를 추가 함.
    void addComponent(component* component) {
        component->attachParent = this;
        components.push_back(component);
    }

    // componet를 가져옴. 
    // 해당 componet를 추가 하지 않았으면 nullptr를 반환함.
    template<typename T>
    T* getComponent() {
        for (auto iter : components) {
            T* item = dynamic_cast<T*>(iter);
            if (item != nullptr) {
                return item;
            }
        }
        return nullptr;
    }
};

그 다음에는 component를 관리하는 객체이다.

해당 객체에서 component를 관리하고 지속적인 실행을 행해준다.

 

addComponent 코드에서 component*를 받은 후 attachParent를 자기 자신으로 가르킨다.

그 후 components에 추가 한다.

 

해당 알고리즘에서는 removeComponent를 구현 하지 않았다. 추가가 가능하다면 삭제 또한 구현하기 쉬울 것이다.

중요한건 addComponent와 getComponent이기 때문이다.

class userComponent : public component {
public:
    virtual void print() {
        printf("userComponent\n");
    }

    void userFunc() {
        printf("only have user\n");
    }
};

class adminComponent : public component {
public:
    virtual void print() {
        printf("adminComponent\n");
    }

    void adminFunc() {
        printf("only have admin\n");
    }
};

component 클래스를 상속 받는 userComponent와 adminComponent를 만들었다.

 

userComponent에 만 구현이 되어 있는 userFunc, 마찬가지로 adminComponent에만 구현이 되어 있는 adminFunc가 있다.

int main() {
    object mainObject;

    component* user = new userComponent();
    component* admin = new adminComponent();

    mainObject.addComponent(user);
    mainObject.addComponent(admin);


    mainObject.getComponent<userComponent>()->userFunc();
    mainObject.getComponent<adminComponent>()->adminFunc();

    return 0;
}

그리고 main에서 각각의 컴포넌트를 만들고 mainObject에 컴포넌트를 추가 해 준 뒤,

getComponent함수를 호출 하여 정상적으로 동작하는지 확인한다.

 

only have user
only have admin

결과 값은 당연하게 위 처럼 나온다.

 

자 이제, 유니티에서 사용이 되는 component안에서 다른 component를 호출을 한다면 어떻게 해야하는가?

class manageComponent : public component {
public:
    virtual void print() {
        printf("manageComponent\n");
    }

    void manageFunc() {
        attachParent->getComponent<adminComponent>()->print();
        attachParent->getComponent<userComponent>()->print();
    }
};

manageComponent를 구현하여 adminComponent와 userComponent를 호출 한 뒤 print 함수를 호출 하였다.

 

구현된 방식을 보면 attachParent를 사용하여 mainObject를 가져오고, 해당 object에 있는 admin, userComponent를 가져온다.

 

가져온 뒤 print함수를 호출 하는 부분이다.

 

즉 코드의 흐름이

mainObject ->

getComponent로 manageComponent를 가져옴 ->

mainObject를 가져 옴 ->

getComponent로 adminComponent를 가져옴 ->

print 함수를 호출 함

mainObject를 가져오는 것이 중요하며, 포인터 연산을 이용한다.

 

컴포넌트 기반 개발에 대해서 이해를 하였으며, 그 외에 다양한 패턴이 있다.

 

스마트폰에 특화가 된 VIPER 패턴, 아이폰에 특화 된 RIBs 패턴, 최근 MS에서 밀고 있는 MvvM 패턴 등등 여러가지 있다.