개념공부

클로저(Closure)

Cadi 2025. 2. 12. 01:28

저번에도 한 번 나왔던 개념인데 이제서야 정리한다.

 

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에 대해 더욱 자세히 알아보기