한 개체가 여러 분야를 서로 커플링 없이 다룰 수 있게 한다.

 

유니티 게임 오브젝트에 여러 컴포넌트를 붙였던 경험을 떠올려 쉽게 이해할 수 있었다.

 

멀티스레드 환경에서는 게임 코드를 분야별로 스레드의 방법을 사용한다. AI, 사운드, 렌더링을 각자 스레드에서 실행시킨다. 분야별로 디커플링 해야 교착상태 같은 동시성 버그를 피할 수 있다.

 

서로 통신이 필요한 컴포넌트가 있다면 필요한 컴포넌트 끼리의 결합을 사용할 수 있다.

 

14.3 패턴

여러 분야를 다루는 하나의 개체가 있다. 분야별로 격리하기 위해, 각각의 코드를 별도의 컴포넌트 클래스에 둔다. 이제 개체 클래스는 단순히 이들 컴포넌트들의 컨테이너 역할만 한다.

 

14.4 언제 쓸 것인가?

게임 개체를 정의하는 핵심 클래스(GameObject)에서 가장 많이 사용되지만, 다음 조건 중 하나라도 만족한다면 다른 분야에서도 유용하게 쓸 수 있다.

 

1. 한 클래스에서 여러 분야를 건드리고 있어서 이들을 서로 디커플링하고 싶다.

2. 클래스가 거대해져서 작업하기가 어렵다.

3. 여러 다른 기능을 공유하는 다양한 객체를 정의하고 싶다. 단, 상속으로는 딱 원하는 부분만 골라서 재사용할 수가 없다.

 

14.5 주의사항

컴포넌트 패턴을 적용하면 클래스 하나에 코드를 모아놨을 때보다 더 복잡해질 가능성이 높다.

컴포넌트끼리 통신하기 어렵다.

각 컴포넌트의 객체를 생성하고 초기화를 잘 해줘야 한다.

대상 개체의 데이터에 접근하거나 행동을 수행하기 위해서 개체의 컴포넌트 객체를 거쳐야 하기 때문에 성능이 민감한 내부 루프 코드에서 이런 식으로 포인터를 따라가다 보면 성능이 떨어질 수 있다.

 

14.6 예제 코드

class GameObject {
public:
    int velocity;
    int x, y; // 컴포넌트간 통신을 위한 멤버 변수
    
    GameObject(InputComponent* input,
    		PhysicsComponent* physics,
                GraphicsComponent* grpahics): 
                input_(input),
                pyhics_(physics),
                graphics_(graphics) {
                
    }
    
    void update(Wolrd& world, Graphics& graphics) {
    	input_->update(*this);
        physics_->update(*this, world);
        graphics_->update(*this, graphics);
    }
    
private:
    InputComponent* input_;
    PhysicsComponent* physics_;
    GraphicsComponent* graphics_;
};
class PhysicsComponent {
	public:
    	virtual ~PhysicsComponent() {}
        virtual void update(GameObject& obj, World& world) = 0;
};

class BjronPhysicsComponent : public PhysicsComponent {
public:
    virtual void update(GameObject& obj, World& world) {
        
	}
};

 

14.7 디자인 결정

객체는 컴포넌트를 어떻게 얻는가?

1. 객체가 필요한 컴포넌트를 알아서 생성할 때

ㄴ 객체는 항상 필요한 컴포넌트를 가지게 된다.

ㄴ 객체를 변경하기가 어렵다. (컴포넌트 패턴의 강점 중 하나는 컴포넌트 재조합만으로 새로운 종류의 객체를 만들 수 있다는 점인데 어떤 컴포넌트를 사용할지를 하드코딩해놓으면, 이런 유연성을 잃게 된다.)

 

2. 외부 코드에서 컴포넌트를 제공할 때

ㄴ 객체가 훨씬 유연해진다. 컴포넌트 구현 클래스만 변경해도 다르게 동작하는 컴포넌트를 객체에 제공할 수 있다.

ㄴ 객체를 구체 컴포넌트 자료형 (예제의 BjronPhysicsComponent)으로부터 디커플링 할 수 있다. 

 

컴포넌트들끼리는 어떻게 통신할 것인가? (중복 가능)

1. 컨테이너 객체의 상태를 변경(공유)하는 방식 (멤버 변수를 공유 정보로써 사용할 수 있다.)

ㄴ 컴포넌트들이 공유하는 정보를 컨테이너 객체에 전부 넣어야 한다.

ㄴ 컴포넌트끼리 암시적으로 통신하다 보니 컴포넌트 실행 순서에 의존하게 된다.

 

2. 컴포넌트가 서로 참조하는 방식

ㄴ 컴포넌트 구체 클래스의 생성자로 참조할 컴포넌트 객체를 인수로 전달한다.

ㄴ 참조하는 컴포넌트들 끼리 강하게 결합된다.

 

3. 메세지를 전달하는 방식

ㄴ 컴포넌트가 컨테이너에 메세지를 보내면, 컨테이너는 자기에게 있는 모든 컴포넌트에 이를 전파한다.

ㄴ 메시징은 호출하고 나서 신경 안 써도 되는 사소한 통신에 쓰기 좋다. 예를 들어 물리 컴포넌트에서 객체가 무엇인가와 충돌했다고 전파하면 오디오 컴포넌트가 이를 받아서 소리를 내는 식이다.

ㄴ 상태 공유 방식에서처럼 상위 컨테이너 객체를 통해서 통신하기 때문에, 컴포넌트들은 메세지 값과 커플링되지만 하위 컴포넌트들끼리는 디커플링 상태를 유지한다. 

class Component {
public:
    virtual ~Component() {}
    virtual void receive(int message) = 0;
};

class ContainerObject { 
public:
	void send(int message) {
    	for (int i=0; i < MAX_COMPONENTS; i++){
        	if (components_[i] != NULL){
            	components_[i]->receive(message);
            }
        }
    }
private:
	static const int MAX_COMPONENTS = 10;
    Component* components_[MAX_COMPONENTS];
};

 

+ Recent posts