이론/디자인패턴

컴포넌트 패턴

tsyang 2021. 12. 25. 00:45

컴포넌트 패턴

컴포넌트 패턴은 한 개체가 여러 분야의 코드를 커플링 없이 다룰 수 있게 해준다.

 


문제

1

매 업데이트마다 인풋을 받아서 위치를 계산하고 바뀐 위치로 스프라이트를 그리는 Player 클래스가 있다고 하자.

 

void Player::update()
{
	//인풋으로 플레이어의 속도를 결정함.

	//속도를 이용해 위치를 바꿈

	//옮겨진 위치로 플레이어의 스프라이트를 그림.
}

Player에는 앞으로 여러 기능들이 더 추가 될탠데 그때마다 update가 수정된다. 딱 봐도 코드가 더러워 질 것이라는 예감이 든다. 심지어 각 분야의 작업자들은 다른 분야의 코드를 보게된다.


2

이번에는 GameObject를 상속하는 Zone, Decoration, Prop 오브젝트가 있다고 해보자. 

  • Zone은 충돌하지만 렌더링 되진 않는다. (ex. 다음 스테이지 이동 영역)
  • Decoration은 렌더링만 한다. (ex. 땅바닥의 풀)
  • Prop은 충돌도 하고 렌더링도 된다. (ex. 나무상자)

 

단순하게 생각해서, Zone과 Decoration이 GameObject를 상속하고 각각 충돌과 렌더링을 담당하는 메서드를 정의했다고 하자. 그렇다면 Prop은 무엇을 상속하고 무슨 메서드를 정의해야 할까?

 

상속을 하자니 죽음의 다이아몬드가 그려진다. 상속을 하지 않으면 충돌과 렌더링 코드를 중복으로 구현해야 한다. 

 

 


컴포넌트로 나누기

[문제1]의 경우 Player에 InputComponent, PhysicsComponent, GraphicsComponent를 추가한다. 다른 기능이 추가된다면 새로운 컴포넌트를 만들어 추가한다. Player 클래스의 update에서는 각각 컴포넌트의 update메서드를 호출해주는게 전부이다. 이렇게 되면 이제 물리 프로그래머가 그래픽이나 인풋을 담당하는 코드를 볼 필요가 없다. 

 

[문제2]의 경우 더 이상 Zone, Decoration, Prop 오브젝트가 필요하지 않다. GameObject 클래스와, GraphicsComponent, PhysicsComponent 두 가지 컴포넌트만 있으면 된다. 이렇게 되면 중복도 상속도 존재하지 않는다. 

 

이처럼 분야별로 코드를 격리하기 위해 각각 코드를 컴포넌트 클래스에 둔다. 개체 클래스는 단순히 컴포넌트의 컨테이너이다.

 


디자인 결정

컴포넌트 패턴을 구현할 때 가장 중요한 것은 어떤 컴포넌트 집합이 필요한지를 정하는 것이다. 또한 개체 클래스가 어떻게 컴포넌트를 얻는지, 컴포넌트끼리 어떻게 통신해야 하는지를 정해야 한다.

 

 

컴포넌트를 얻는 방식

 

1. 객체가 필요한 컴포넌트를 직접 생성

 

이 경우, 객체가 항상 필요한 컴포넌트를 가지고 있음을 보장할 수 있다. 그렇지만 앞서 Zone,Decoration,Prop의 예제처럼 컴포넌트 재조합만으로 새로운 객체를 만들지 못한다. 즉, 유연성을 잃어버린다.

 

 

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

 

외부에서 객체에 컴포넌트를 제공해줄 수 있다. 이런 경우 객체가 훨씬 유연해진다. 예를 들어, 앞선 Player의 예에서 InputComponent로 자동전투를 수행하는 Component를 넘겨 준다면 쉽게 Player 객체가 자동전투를 수행할 수 있게 만들 수 있다. 또한 외부에서 보다 추상화된 컴포넌트를 전달함으로써 컴포넌트의 자료형을 캡슐화하기 좋다.

 


 

컴포넌트간 통신하는 방법

 

이상적으로는 컴포넌트들 끼리 완전히 격리되는게 좋지만, 실제로는 서로간의 통신이 필요한 경우가 많다. 컴포넌트들 끼리의 통신은 다음의 방법들을 고려해볼 수 있다.

 

 

1. 컨테이너 객체의 데이터를 변경

 

Player 객체를 예로 들면, Player 객체가 속도나 위치를 가지고 Player의 컴포넌트들이 이 값들을 수정하는 경우가 된다.

  • 컴포넌트끼리 서로 디커플링 상태를 유지한다는 장점이 있다.
  • 그러나 일부 컴포넌트만 사용하는 데이터들도 전부 객체에 넣어야 하며 컴포넌트 조합에 따라 사용하지 않는 데이터도 객체에 들어가 메모리 낭비를 유발한다.
  • 또한 update메서드 내부의 컴포넌트 실행 순서에 의존하게 된다. 이렇게 되면 컴포넌트가 증가할수록 버그가 생길 확률이 높아진다.

 

 

2. 컴포넌트가 서로 참조

class GraphicsComponent
{
public:
	GraphicsComponent(PhysicsComponent * physics) : physics_(physics) {}

	void Update()
	{
		//physics_ 를 이용하여 draw
	}
private:
	PhysicsComponent physics_;
};

위 코드처럼 직접 컴포넌트를 레퍼런스로 전달할 수 있다.

  • 간단하고 빠르다.
  • 컴포넌트끼리 강하게 결합된다. 어떻게 보면 컴포넌트끼리 디커플링 하는 패턴의 목적에 반대된다. 그러나 통신하는 컴포넌트끼리만 결합이 되기 때문에 그나마 낫다.

 

 

3. 메시지를 전달

 

조금 복잡한데, 컨테이너 객체에 메시징 시스템을 만든 뒤, 각 컴포넌트들이 서로에게 정보를 broadcast 하는 방식이다. 중재자 패턴인데 컨테이너가 중재자에 해당한다고 보면 된다.

class Component
{
public:
	virtual void receive(Data& data) = 0;
};

각 컴포넌트는 Component 클래스를 상속한다. 컴포넌트가 컨테이너에 메시지를 보내면, 컨테이너는 자신이 가진 모든 컴포넌트에 이를 broadcast한다. (이때 피드백 루프에 빠지지 않도록 주의한다.) 

  • 컴포넌트들은 디커플링 된다. 메시지 값과 커플링 될 뿐이다.
  • 컨테이너 객체는 메시지만 전달해주면 되므로 단순한다.

 

 

4. 정리

 

  1. 객체의 데이터를 공유하는 방식은 위치나 크기같이 모든 객체에 당연히 있을 거라고 생각하는 기본적인 정보를 공유하기에 좋다.
  2. 컴포넌트가 서로를 참조하는 방식은 물리&충돌, 애니메이션&렌더링 처럼 가깝게 연관된 컴포넌트에 사용한다면 작업하기가 편해질 수 있다.
  3. 메시지를 전달하는 방식'사소한' 통신에 쓰기 좋다. 예를 들어, 물리 컴포넌트에서 객체가 충돌했다는 메시지를 전파하면 오디오가 이를 받아 소리를 낼 수 있다.

 

 

위 세 가지 방법은 상호 베타적인 방법이 아니며 그때 그때 필요한 방식을 추가하면 된다.


주의사항

  • 컴포넌트 패턴을 적용하면 오히려 코드가 복잡해질 수 있다. 한 무리의 객체를 생성하고 초기화하고 알맞게 묶어줘야 하나의 '객체'라는 개념을 만들 수 있기 때문이다.
  • 컴포넌트끼리 통신하기가 어렵고, 컴포넌트들을 메모리 어디에 둘지 제어하는 것도 어렵다.
  • 무슨 일이든 객체에서 컴포넌트를 거쳐야 한다. 이런 식으로 포인터를 따라가는 것은 시간이 약간 걸린다.

 

따라서 컴포넌트 패턴을 적용하기 전에 아직 있지도 않은 문제에 대한 해결책을 제시하려 오버하는 것이 아닌지 주의해야한다.

 


참고 : 로버트 나이스트롬, 게임 프로그래밍 패턴, 한빛미디어, [256~279p] 

'이론 > 디자인패턴' 카테고리의 다른 글

서비스 중개자  (0) 2022.01.16
이벤트 큐 패턴  (1) 2022.01.03
디자인 패턴 - 행동패턴  (0) 2021.12.12
디자인 패턴 - 구조  (1) 2021.12.03
디자인 패턴 - 생성 패턴  (0) 2021.11.27