클로저(Closure)
저번에도 한 번 나왔던 개념인데 이제서야 정리한다.
1. Closure 기능이란 ?
- 외부 함수의 변수를 내부 함수에서 참조할 수 있는 기능
- 함수가 생성될 때 그 함수가 선언된 환경을 기억하는 것
- C#에서는 익명 함수 또는 로컬 함수가 클로저의 역할을 할 수 있음
2. 동작 방식
using System;
class Program
{
static Func<int> CreateClosure()
{
int count = 0; // 외부 변수
return () =>
{
count++; // 외부 변수를 내부 함수에서 변경
return count;
};
}
static void Main()
{
Func<int> closureFunc = CreateClosure();
Console.WriteLine(closureFunc()); // 1
Console.WriteLine(closureFunc()); // 2
Console.WriteLine(closureFunc()); // 3
}
}
분명 CreateClosure();로 만들어진 closureFunc의 함수 내부에는 int count가 선언되지 않았다.
그런데 여전히 count를 반환하고, 숫자가 점차 증가하는 것을 확인할 수 있다.
이는 closureFunc가 count 값을 기억하고 있기 때문에 발생한다.
count 값을 기억하고 있어 호출때마다 값이 증가한다. 즉, count는 원래 다른 함수들 같으면 CreateClosure() 함수가 종료된 뒤 사라지겠지만, 이를 참조하는 closureFunc이 남아 있어 사라지지 않는다.
using System;
class Program
{
static void Main()
{
var closure1 = CreateClosure();
var closure2 = CreateClosure();
Console.WriteLine(closure1()); // 1
Console.WriteLine(closure1()); // 2
Console.WriteLine(closure2()); // 1 (새로운 클로저는 독립적인 count 가짐)
Console.WriteLine(closure2()); // 2
}
static Func<int> CreateClosure()
{
int count = 0;
return () => ++count;
}
}
이와 같이 다른 객체에서 실행하면 독립적인 외부 변수를 기억하고 있다 반환한다.
3. 유니티에서의 활용 예제
-1) 버튼 클릭 이벤트 핸들러
using UnityEngine;
using UnityEngine.UI;
public class ButtonClosureExample : MonoBehaviour
{
public GameObject buttonPrefab;
public Transform parentPanel;
void Start()
{
for (int i = 0; i < 5; i++)
{
int index = i; // 클로저를 사용해 각 버튼의 인덱스를 고정
GameObject newButton = Instantiate(buttonPrefab, parentPanel);
newButton.GetComponentInChildren<Text>().text = "Button " + index;
newButton.GetComponent<Button>().onClick.AddListener(() =>
{
Debug.Log("Button " + index + " clicked!");
});
}
}
}
버튼을 동적으로 생성하고, 각 버튼이 고유한 인덱스를 유지하게 하려고 한다.
이 때 클로저 기능을 이용하지 않으면 모든 버튼이 마지막 인덱스인 4만을 참조하게 된다.
이처럼 int index = i; 를 선언해 주어야 각 버튼이 고유한 값을 가진다.
void Start()
{
for (int i = 0; i < 5; i++) // i는 0, 1, 2, 3, 4까지 증가
{
GameObject newButton = Instantiate(buttonPrefab, parentPanel);
newButton.GetComponentInChildren<Text>().text = "Button " + i;
// i를 직접 사용
newButton.GetComponent<Button>().onClick.AddListener(() =>
{
Debug.Log("Button " + i + " clicked!");
});
}
}
이렇게 되면 버튼 클릭 이벤트는 나중에 발생하기 때문에 for 루프가 끝난 후 i의 값인 5를 참조하게 된다. (모든 버튼)
-2) 코루틴으로 시간 지연 실행
클로저를 사용해 비동기적으로 실행되는 코루틴에서 변수를 유지할 수 있다.
public class ClosureCoroutine : MonoBehaviour
{
void Start()
{
for (int i = 0; i < 3; i++)
{
int index = i; // 클로저를 활용해 고유한 index 유지
StartCoroutine(DelayedMessage(index, 2f));
}
}
IEnumerator DelayedMessage(int number, float delay)
{
yield return new WaitForSeconds(delay);
Debug.Log($"Message from {number} after {delay} seconds");
}
}
클로저 관련 추가 정리
질문에 대한 답을 정리해보자면,
- 반복문 자체는 종료됨, But 이벤트 리스너(람다 함수)는 살아 있고, 거기서 참조하는 i도 계속 남아 있음
- 버튼 클릭시 변수 i를 참조하려고 함, 그런데 이때 i = 5로 계속 남아있음.
그래서 5가 클릭되었다는 메세지가 모든 버튼에서 나오게 된다.
결국, 싱글톤에서 게임매니저를 구현했던 문제로부터 클로저 개념을 정리했다.
원리는 함수를 포함한 객체가 더 이상 참조되지 않을 때만 가비지 컬렉터(GC)에 의해 제거되는 원리에 의해 클로저가 동작한다.
내일의 목표
GC에 대해 더욱 자세히 알아보기