1
2
3
4
5
6
7
한 객체가 소지한 여러 기능이 서로 커플링 없이 작동할 수 있게 구현하는 패턴
아래의 상황에 놓여있다면 사용을 고려해볼만한 패턴
- 한 클래스에서 여러 분야를 건드리고 있어서 이들을 서로 디커플링 하고 싶다
- 클래스가 거대해져서 작업하기가 힘들다
- 여러 다른 기능을 공유하는 다양한 객체를 정의하고 싶다.
단, 상속으로는 딱 원하는 부분만 골라서 재사용할 수가 없다.
1. 서론
우리가 게임을 만들 때 만드는 객체에는 Animation, Sound, Input System 등 다양한 기능들이 포함되어 있는 경우가 많다.
이 때 하나의 객체 클래스에 기능을 모두 넣는다면 코드가 길어지고 기능들이 서로 묶이게 되어
하나를 수정하려면 코드를 전부 확인해야 하는 문제가 생기게 된다.
이러면 코드를 수정할수록 수정 결과보다 버그가 더 많이 발생할 것임을 알 수 있다.
그리고….. 몇 백 몇 천 줄이 되는 코드를 분석하는 건 정말 지옥과도 같다
이 때 우리가 사용할 수 있는 방법으로 컴포넌트 패턴이 있다.
이 패턴으로 위 문제를 어떻게 해결할 수 있는지 알아보자!
1
2
참고로
게임 엔진인 유니티(UNITY)가 컴포넌트 패턴을 이용하고 있다.
2. 컴포넌트 패턴을 쓰지 않는다면..?
우리는 마리오 게임을 만들 것이다.
마리오 안에 아래와 같은 코드가 있다고 가정하자
아주 극단적인 예시이니 참고만 하자
1
2
3
4
if(IsOnGround() && IsRunAnimation())
{
PlaySound(ESoundType.Run);
}
여기서 IsOnGround() 를 수정하려면 우리는 IsOnGround() IsRunAnimation() PlaySound(ESoundType.Run) 3개의 내용을 전부 알아야 한다.
그리고 IsOnGround() 를 다른 객체에 사용하려면 마리오 안에 있는 코드를 복사해서 가져와야 하는데 묶인 게 많다 보니 가져오기 쉽지 않을 뿐더러 매번 객체 상황에 맞추어 코드를 바꿔줘야 하니
재사용성도 떨어지게 된다.
3. 컴포넌트 패턴
3.1 컴포넌트 패턴 적용
1
유니티를 기반으로 설명하고 있습니다.
간단한 예시를 통해 확인해보자
왼쪽, 오른쪽으로만 이동하는 객체가 있고 왼쪽으로 갈 때는 파란색으로 오른쪽으로 갈 때는 빨간색으로 변한다.
여기서 큐브가 가지고 있는 기능은 아래 2개로 볼 수 있다.
- 왼쪽, 오른쪽으로 이동하는 이동 시스템
- 방향에 따라 컬러가 변하는 렌더링 시스템
컴포넌트 패턴을 쓰지 않는다면 코드는 아래와 같을 것이다.
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
public class Mario : MonoBehaviour
{
float _speed = 1;
Renderer _thisRenderer;
private void Start()
{
_thisRenderer = this.GetComponent<Renderer>();
}
private void Update()
{
float input = Input.GetAxis("Horizontal");
this.transform.position =
new Vector3(this.transform.position.x + input * _speed * Time.deltaTime, 0);
// 오른쪽으로 이동
if(input > 0f)
{
_thisRenderer.material.color = Color.red;
}
// 왼쪽으로 이동
else if(input < 0f)
{
_thisRenderer.material.color = Color.blue;
}
// 멈춘 상태
else
{
_thisRenderer.material.color = Color.white;
}
}
}
이동하는 기능과 색이 변하는 렌더링 기능이 같이 있는 것을 볼 수 있다.
간단한 예시로 코드를 구현했음으로 딱히 문제가 될 게 없다고 보일 수 있다. 하지만 코드가 길어지고 기능이 하나 둘 추가 된다면 발생할 문제가 많다는 것을 예상할 수 있을 것이다.
이걸 MovementComponent 와 RenderingComponent 2 개의 컴포넌트로 분리해서 컴포넌트 패턴을 적용해보자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Mario : MonoBehaviour
{
MovementComponent _movementComponent;
RenderingComponent _renderingComponent;
Renderer _thisRenderer;
private void Start()
{
_movementComponent = new MovementComponent();
_renderingComponent = new RenderingComponent();
_thisRenderer = this.GetComponent<Renderer>();
}
private void Update()
{
// 공용으로 써야하는 변수
float input = Input.GetAxis("Horizontal");
_movementComponent.Update(input, this.transform);
_renderingComponent.Update(input, _thisRenderer);
}
}
1
2
3
4
5
6
7
8
9
public class MovementComponent
{
float _speed = 1f;
public void Update(float input, Transform player)
{
player.position = new Vector3(player.position.x + input * _speed * Time.deltaTime, 0);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class RenderingComponent
{
public void Update(float input, Renderer renderer)
{
if (input > 0f)
{
renderer.material.color = Color.red;
}
else if (input < 0f)
{
renderer.material.color = Color.blue;
}
else
{
renderer.material.color = Color.white;
}
}
}
기능 별로 코드를 분리하면서 기존 하나의 클래스에 모든 코드가 있었을 때보다 코드 길이가 짧아졌고 기능 별 클래스가 생기면서 관련 코드를 한 눈에 보기 쉬워졌다.
그 결과,
기능을 수정할 때 그 기능의 컴포넌트 클래스만 보면 되기에 유지 보수가 좋아졌음을 알 수 있고,
다른 객체에서 해당 기능이 필요할 때 객체에 해당 기능의 컴포넌트만 넣어주면 바로 기능을 사용할 수 있기에 재사용성 또한 좋아졌음을 볼 수 있다.