로그인게임하기
Unity C# 강좌

[Unity C# 5] 함수를 돌려주는 함수, Func 와 클로저

2026.10.14

Unity C# 강좌 · 5편

함수를 변수에 담을 수 있다면, 함수가 함수를 돌려줄 수도 있습니다. 계산 방법을 통째로 바꿔 끼우고 싶을 때 쓰는 방법입니다. 이번 편에서는 Func 로 계산 함수를 주고받고, async 람다를 맛보고, 람다가 바깥 값을 붙잡는 클로저를 키 입력으로 확인합니다.

수업 정보

2024년 부산 콘텐츠 아카데미 퍼스널 트레이닝 수업에서 MyLesson Unity 실습 프로젝트의 Strategy_14_Func.cs 로 가르친 내용입니다. 수업은 세 단계로 나뉘고, 단계마다 코드가 조금씩 바뀝니다.

STEP 1

계산 방법을 돌려주는 인터페이스

FuncProvider1.cs수업 자료 1단계3–28줄
public interface FuncProvider
{
    public Func<int, int, int> ProvideCrossMethod();
    public Func<int, int, int> ProvideAddMethod();
}

public class FuncProvider1 : FuncProvider
{
    public Func<int, int, int> ProvideCrossMethod()
    {
        Func<int, int, int> crossCalc = (a, b) =>
        {
            Program.WriteLine("{0} * {1} =", a, b);
            return a * b;
        };

        return crossCalc;
    }

    public Func<int, int, int> ProvideAddMethod()
    {
        Func<int, int, int> addCalc = (a, b) => a + b;

        return addCalc;
    }
}

Func<int, int, int> 는 "int 둘을 받아 int 하나를 돌려주는 함수" 입니다. 마지막 타입 매개변수가 반환 타입입니다. FuncProvider 인터페이스는 그런 함수를 돌려주는 메서드 둘을 약속합니다.

람다는 두 모양으로 썼습니다. 여러 줄이 필요한 곱셈은 중괄호와 return 이 있는 문 람다, 한 줄짜리 덧셈은 식 람다 (a, b) => a + b 입니다.

FuncProvider1.cs수업 자료 1단계56–83줄
public void Run()
{
    FuncProvider ofp = new FuncProvider1();
    crossCalc = ofp.ProvideCrossMethod();
    addCalc = ofp.ProvideAddMethod();

    Program.WriteLine("FuncProvider1");
    Program.WriteLine(crossCalc(2, 5));

    Program.WriteLine();

    Program.WriteLine("(outer) 5 + 5 =");
    Program.WriteLine(addCalc(5, 5));

    Program.WriteLine();

    ofp = new FuncProvider2();
    crossCalc = ofp.ProvideCrossMethod();
    addCalc = ofp.ProvideAddMethod();

    Program.WriteLine("FuncProvider2");
    Program.WriteLine(crossCalc(2, 5));

    Program.WriteLine();

    Program.WriteLine("(outer) (5 + 5)^2 =");
    Program.WriteLine(addCalc(5, 5));
}
실행 결과게임 화면의 출력 창
FuncProvider1
2 * 5 =
10

(outer) 5 + 5 =
10

FuncProvider2
(2 * 5)^2 =
100

(outer) (5 + 5)^2 =
100

Run 은 crossCalc(2, 5) 라고만 부릅니다. 어떤 계산이 일어날지는 어느 FuncProvider 가 함수를 줬느냐에 달렸습니다. 1편의 Strategy 처럼, 이번에는 클래스가 아니라 함수를 갈아 끼웠습니다.

STEP 2

async 람다와 Task

FuncProvider1.cs수업 자료 2단계53–67줄
public class AsyncFuncProvider
{
    public Func<int, int, Task<int>> ProvideCrossTask()
    {
        Func<int, int, Task<int>> crossCalc = async (a, b) =>
        {
            //await Task.Delay(1000);

            Program.WriteLine("{0} * {1} =", a, b);
            return a * b;
        };

        return crossCalc;
    }
}
FuncProvider1.cs수업 자료 2단계78–87줄#if 줄 생략
public void Run()
{
    var afp = new AsyncFuncProvider();
    someTask = afp.ProvideCrossTask();

    Program.WriteLine("AsyncFuncProvider");
    Program.WriteLine(someTask(2, 5).Result);
}

람다 앞에 async 를 붙이면 결과를 바로 주지 않고 Task<int>, 곧 "나중에 int 가 나올 약속" 을 돌려줍니다. .Result 는 그 약속이 끝날 때까지 기다렸다가 값을 꺼냅니다.

이 코드에서 짚을 점

람다 안의 await Task.Delay(1000) 이 주석이라, 지금은 await 가 하나도 없어 그냥 동기로 돌고 컴파일러가 경고(CS1998)를 냅니다. 주석을 풀고 .Result 로 기다리면 Unity 의 메인 스레드가 1초 멈춥니다. 게임에서 기다릴 때는 다음 편 이후에 볼 코루틴이나 await 를 끝까지 이어 쓰는 방법을 씁니다.

STEP 3

클로저, 람다가 바깥 값을 붙잡는다

FuncProvider1.cs수업 자료 3단계60–90줄#if 줄 생략
private float accTime = 0;
private int varA = 0;
private int varB = 0;
private Action addCalcNoParam = null;

public void Run()
{
    FuncProvider ofp = new FuncProvider1();
    crossCalc = ofp.ProvideCrossMethod();

    addCalcNoParam = delegate
    {
        Program.WriteLine("{0} + {1} =\r\n{2}", varA, varB, (varA + varB));
    };
}

public void Update()
{
    accTime += Time.deltaTime;

    if (accTime > 5.0f)
    {
        Program.WriteLine(crossCalc(varA, varB));
        addCalcNoParam();
        accTime = 0;
    }

addCalcNoParam 는 매개변수가 없는 Action 입니다. 그런데 안에서 varA, varB 를 씁니다. 람다나 무명 메서드가 바깥의 변수를 쓰면, 그 변수를 값으로 복사해 두지 않고 변수 자체를 붙잡아 둡니다. 이것을 클로저라고 합니다.

FuncProvider1.cs수업 자료 3단계92–131줄
bool ctrl = Input.GetKey(KeyCode.LeftControl);
bool alt = Input.GetKey(KeyCode.LeftAlt);

if (ctrl || alt)
{
    int temp = 0;
    if (Input.GetKeyUp(KeyCode.Keypad1))
    {
        temp = 1;
    }
    else if (Input.GetKeyUp(KeyCode.Keypad2))
    {
        temp = 2;
    }
    else if (Input.GetKeyUp(KeyCode.Keypad3))
    {
        temp = 3;
    }
    else if (Input.GetKeyUp(KeyCode.Keypad4))
    {
        temp = 4;
    }
    else if (Input.GetKeyUp(KeyCode.Keypad5))
    {
        temp = 5;
    }

    if (temp != 0)
    {
        if (ctrl)
        {
            varA = temp;
            Program.WriteLine("varA에 {0}이 입력되었습니다.", temp);
        }
        else
        {
            varB = temp;
            Program.WriteLine("varB에 {0}이 입력되었습니다.", temp);
        }
    }

실행 중에 왼쪽 Ctrl + 키패드 숫자로 varA 를, 왼쪽 Alt + 키패드 숫자로 varB 를 바꿉니다. 5초마다 불리는 addCalcNoParam 은 Run 에서 만들어진 함수인데도 부를 때의 최신 값으로 계산합니다.

Ctrl+3, Alt+4 를 누른 뒤 5초가 지나면게임 화면의 출력 창
varA에 3이 입력되었습니다.
varB에 4이 입력되었습니다.
3 * 4 =
12
3 + 4 =
7
붙잡는 것은 변수다

이 예에서 varA · varB 는 필드라 람다는 객체(this)를 붙잡습니다. 지역 변수를 쓰면 그 지역 변수를 붙잡습니다. 반복문 안에서 람다를 여러 개 만들 때 모두 같은 변수를 붙잡아 같은 값을 보는 실수가 자주 납니다.

RETRO

돌아보며

이번 편에서 얻은 것

  • Func 의 마지막 타입 매개변수는 반환 타입이다
  • 인터페이스가 함수를 돌려주게 하면 계산 방법을 갈아 끼울 수 있다
  • 람다는 바깥 변수를 붙잡아 두고, 부를 때의 값을 쓴다

아쉬운 점

  • async 람다에 await 가 없어 비동기의 효과를 볼 수 없다
  • .Result 로 기다리는 방식은 실제 게임 코드에서는 피해야 한다

정리

  1. Func 는 값을 돌려주는 델리게이트입니다. Func<int, int, int> 는 int 둘을 받아 int 를 돌려줍니다.
  2. 함수도 반환값이 될 수 있습니다. 계산 방법을 돌려주는 FuncProvider 를 바꾸면 계산 전체가 바뀝니다.
  3. 클로저는 값이 아니라 변수를 붙잡습니다. 그래서 나중에 불러도 그때의 최신 값을 봅니다.

다음 편에서는 함수가 값을 하나씩 나눠 돌려주는 yield return 과 IEnumerable 을 봅니다.

← 개발 노트 목록