본문 바로가기

유니티 개발 정보/개념

유한 상태 기계(Finite State Machines, FSM) #2 (2/3)

원문보기
1편보기 3편보기


(이어서 계속)
* 전편에 약간 누락된 부분이 있어 전편 마지막부분에 추가하였습니다. (13. 12. 2)

델리게이트

그럼 이제 델리게이트를 한번 써보자. .NET에는 2개의 일반적인 델리게이트 타입(Func, Action)이 내장되어 있고, 
스스로도 정의할 수 있는데, 첫번째 함수를 예로 들어 보자.
public delegate void UnityTriggerFunctionDelegate(Collider collider);
이제 트리거 함수를 가리키는 변수를 정의할 수 있다:
UnityTriggerFunctionDelegate _triggerFunc;
그리고 다음과 같이 델리게이트에 함수를 할당할 것이다.

1
2
3
4
5
6
7
8
9
void Start()
{
    _triggerFunc = OnTriggerEnter;
}
 
void OnTriggerEnter(Collider collider)
{
     //Do something
}

우리는 다음과 같이 호출할 수 있다:
_triggerFunc(someColliderYouFound);
OnTriggerEnter(somColiderYouFound)를 호출하는 것과 동일하다. 
만약 나중에 _triggerFunc가 OnTriggerExit를 가리킨다면 _triggerFunc(somColliderYouFound)는 OnTriggerExit(someColliderYouFound)와 동일한 함수를 호출할 것이다.

델리게이트를 하나 만들려고 할 때 마다 매번 델리게이트를 정의하는것 대신, .NET에서 정의한 일반적인(generic) Action, Func 델리게이트 타입을 사용할 수 있다.
(같은 매개변수를 사용하는 함수를 참조하는 여러개의 델리게이트를 가지고 있다면, 매우 혼란스러울 것이다.)

이 일반적인 클래스를 사용함으로써, _triggerFunc에 대한 정의를 재작성할 수 있고, 
UnityTriggerFunctionDelegate또한 더 이상 선언할 필요가 없게 된다.
Action<Collider> _triggerFunc;
Actions는 리턴타입이 void형태라 반환값이 없으며, 매개변수 인자로 일반적인 매개변수를 가지는 델리게이트를 가진다. 그래서 리턴값이 void형태이고 string, int형을 매개변수로 가지는 델리게이트는 다음과 같이 사용할 수 있다.
Action<string, int> variable;
두번째로 내장된 일반적인 델리게이트 타입은 Func인데, Action과 가장 큰 차이는, 마지막 일반적인 매개변수가 함수의 리턴타입을 나타낸다는 것이다. 그래서 return값으로 int를 반환하고 string을 매개변수로 가지는 델리게이트는 다음과 같이 사용할 수 있다.
Func<string, int> variable;
요즘에는 일반적으로 꼭 델리게이트를 선언할 필요가 없다. 위에서 본 것 처럼 일반적인 델리게이트 사용에 익숙해진다면, 나중에 훨씬 더 쉽게 내용을 이해할 수 있을 것이다.

리플렉션

리플렉션은 많이 무시당하고, 실행중에 코드의 메타데이터를 읽는 함수라는 오해를 받아왔다. 메타데이터란 무엇인가? 이는 모든 필드, 속성, 메소드, 매개변수, 리턴 값, 심지어는 private 한정자가 붙은 것들까지 찾을 수 있게 해준다. 그리고, 이것 뿐만 아니라, 동적으로 호출할 수도 있고, 값도 변경할 수 있다.

리플렉션은 종종 약간 좋지 않은 평을 듣는다. 한 예를 든다면, 리플렉션은 캡슐화를 무너뜨린다. 리플렉션을 사용함으로써, 클래스의 모든 숨겨진 그리고 private 맴버들에 접근할 수 있게 된다. 그렇다고 하더라도 이들을 찾는데 느릴 것이고, 동적으로 이들을 실행하는데 매우 느릴 것이다.

유니티에서 SendMessage는 리플렉션을 사용한다.
그렇기 때문에 델리게이트를 이용해서 이벤트를 실행시키는 것보다 85~150배 정도 느리다!
(역자 주: SendMessage의 대한 새로운 사실을 알게 되었는데, 우리가 생각하는 것보다 그렇게 느리지 않다는 사실입니다. SendMessage 내부 작동원리는 해당 링크의 발표자료에서 44페이지를 참고하시기 바랍니다.)
올바르게 사용된 리플렉션은 엄청난 자산이다. 이번 섹션에서는 리플렉션을 사용하여 FSM를 구현하는데 사용되는 함수에 대한 엄청나게 빠른 델리게이트를 만들어 볼 것이다.

우리의 첫번째 리플렉션

그래서 이 리플렉션을 어떻게 해야하나? .GetType()은 .NET안에 있는 모든 단일 객체에 대한 모든 것을 나타낸다. 이는 Type을 찾게 해주고, Type은 함수등 여러가지를 찾을 수 있게 해준다.
1
2
3
4
5
6
7
8
void CallMethod(object target, string name)
{
       var mtd = target.GetType().GetMethod(name);
       if(mtd != null)
       {
          mtd.Invoke(target, null);
       }
}

위에 나온 방법은, 전달한 객체를 가지고, 그 객체의 Type을 얻어 낸 다음, 함수에 전달된 name이라는 이름을 가지는 public 함수를 가지는 것이다 - 이는 GetMethod를 호출함으로써 가능하다. 이 함수는 실제 코드에 대한 많은 정보를 제공하는 MethodInfo 클래스를 반환한다. 만약 name이라는 이름의 public 함수를 가지고 있다면, mtb는 null이 아닌 값을 가지게 되고, 파라미터를 가지지 않는 발견된 함수를 Invoke를 통해 호출한다.
Invoke의 첫번째 매개변수는 발견된 함수가 존재하는 인스턴스 객체를 의미하고, 두번째는 함수에 전달될 매개변수를 나타낸다. 여기서는 파라미터가 없는 함수이기때문에, null을 입력하였다.

object[]에 매개변수를 넣어 두번째 매개변수에 전달함으로써 Invoke에 매개변수를 전달한다.
위 예제에 나온 함수를 다음과 같이 사용할 수 있다:
CallMethod(yourScript, "SomeMethod");
위 코드는 정말 느린 리플렉션의 전형적인 예제이다. Invoke의 성능은 매우 끔찍하고 매번 함수를 찾는 것 또한 빠르지 않다.
기본적으로, GetMethod로 public 인스턴스 함수를 검색할 수 있는데, 마찬가지로 GetMethod를 사용하여 private 함수와 static 함수 또한 찾을 수 있다. - 많은 옵션을 명시할 수 있는 Binding Flags 매개변수를 가진 버전이 존재한다.

name으로 필드를 얻는 것은 GetField를 통해 가능하고, 속성(property)은 GetProperty를 통해 얻을 수 있다.

이 튜토리얼의 목적은 리플렉션의 모든 것을 가르치는 것이 아니다. 빠르고, 규칙 기반의  FSM을 만들기에 충분할 정도면 된다. 우리에게 필요한 것은 클래스에서 함수에 접근하는 것이다.


리플렉트된 함수를 델리게이트로 바꾸기

좋다, 이미 이해했을지도 모르지만, 요약을 한번 해보자.

  • 델리게이트는 매우 빠르지만, 수동으로 함수를 할당하는 것 보다 나은 방법이 필요하다.
  • 리플렉션은 매우느리지만, string을 사용하여 클래스에 존재하는 함수를 발견할 수 있게 해준다.
델리게이트를 변경하고 싶을때, 원하는 함수를 찾고 최대한 빠르게 함수를 호출하기 위해서, 리플렉트된 MethodInfo 인스턴스를 델리게이트로 바꾸는게 좋지 않을까?

델리게이트 클래스는 CreateDelegate라는 static 함수를 제공하는데, 이는 리플렉트된 정보를 매우 성능이 좋은 델리게이트로 바꿔준다.

먼저 이에 앞서, 우리가 만들 델리게이트에 대해 얘기를 잠깐 하고 싶다. - 여기에는 당신이 실수를 할 수 있는 감지하기 힘든 부분이 있다. 

Action은 매개변수와 리턴값을 가지지 않는 델리게이트이다. 여기 Action에 할당될 수 있는 2개의 함수를 가진 클래스가 존재한다.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public class ExampleClass
{
 
       public string somethingToLog;
 
       public void Log()
       {
            Debug.Log(somethingToLog);
       }
 
       public static void LogCurrentTime()
       {
            Debug.Log(Time.time.ToString());
       }
 
}

두 함수 모두 Action에는 적합한 함수이지만, 이들 중 하나는 클래스의 인스턴스 변수(somethingToLog)에 접근하고 있고, 나머지 하나는 그렇지 않다. 
어떻게 Action은 이 클래스의 인스턴스 변수를 어떻게 알 수 있을까? 그 이유는 바로 클로저(Closures) 때문이다.

클래스의 인스턴스 함수를 호출할 때, 숨겨진 매개변수가 함수에 전달된다. 함수 내부에 있을때, 숨겨진 매개변수의 값은 this가 된다. 물론 인스턴스 메소드를 작성할때, 따로 this.somethingToLog라고 작성할 필요는 없지만, 해도 상관은 없다. 작성하지 않았다면, 컴파일러가 알아서 this를 입력해주기 때문이다.

그래서, someObject.SomeMethod()를 호출할 때, 내부적으로는 컴파일러가 실질적으로 다음과 같이 처리한다: 
someObject.SomeMethod(someObject).
기본적으로, 클래스 인스턴스는 우리가 만들 인스턴스 함수에 관한 델리게이트와 바인딩되어야 되고, static 함수에 대해서는 그러지 않아도 된다. 델리게이트를 만든 후에, 실제로 실행이 되기전까지는 우리나 컴파일러나 이것이 바인드된 델리게이트인지 아닌지 알 수 없을 것이다.

지금부터는 델리게이트를 C/C++의 함수 포인터와 똑같다는 생각을 버려야 한다. 델리게이트는 값을 바인드하는 능력이 있기 때문이다.
그래서, 다음과 같이 코드를 작성할 수 있다:
Action someAction = instanceOfExampleClass.Log;
위 예제 코드를 사용할 때, Log 함수가 someAction에 저장되기전에 instanceOfExampleClass는 Log 함수에 대한 델리게이트에 연결될 것이다. 그리고 이후부터는, 항상 특정 인스턴스에 대한 Log함수를 호출할 수 있게 된다. 델리게이트에 instanceOfExampleClass에 대한 숨겨진 참조가 있기 때문이다.

물론 다른 클래스 인스턴스의 함수를 호출하는 someAction()를 만들기 위해서 
someAction = someOtherClassEntirely.SomeMethod()라고 작성할 수도 있다.
만약 델리게이트에 static 함수를 설정할 때는 인스턴스에 대한 참조는 필요하지 않으며, 그래도 작동하는데는 문제가 되지 않는다.

이전 예제에서처럼, 델리게이트를 할당할 때, 우리는 항상 Closed 델리게이트를 만들고 있다. - 이는 인스턴스 혹은 타입 참조(static인 경우)에 둘러 쌓여져 있다는 것을 의미한다.

클로저에 대한 다른 예를 한번 살펴보자. 우리는 지금 델리게이트의 가장 강력한 기능을 이해하기 직전에 와있고, FSM 프레임워크를 위해 Closed 델리게이트에 대해 이해가 필요하기도 하지만, 지금 어설프게 알고 있는 상태로 내버려두는 것은 옳지 않다고 생각하기 때문이다.

실제로 Open 델리게이트라고 불리는 것이 있는데, 이는 델리게이트를 실행할 때 this 포인터를 전달하는 것이 가능하다. 기본적으로는 숨겨진 this 파라미터를 코드에서 볼 수 있게 한다는 의미이다. 이것 또한 여러 상황에서 유용한 경우가 존재하는데, 여기에 대한 주제는 다음에 다루도록 하겠다.

(다음 글에서 계속)