본문 바로가기

유니티 스파르타 캠프 4주차

25-04-30 유니티 람다식의 내용 정리

유니티에서 다른 부분은 어느정도 이해하고 넘어갈 수 있다. 대부분의 함수가 직접 실행해보면서 확인할 수 있고 그러다보면 어느샌가 이해가 되기 마련이다. 하지만 필자는 델리게이트, 람다식에서 막히기 시작했다. 이들은 특이하게도 함수를 직접 사용하고 솔직히 없어도 큰 불편함을 느끼지 못할 때가 생긴다. 하지만 있으면 굉장히 편리하다. 오늘 막 이해했기에 이 글을 읽는 이들이 한번에 이해가 쉽게끔 설명하고자 노력하겠다.

1. 람다식이란?

유래라던가 필자는 자세히 모른다. 그렇기에 코드의 구조와 내용만을 설명하겠다.

(int x, int y) => x + y

private int gold;
int Gold { get => gold; } {set => gold = value;}

아마 람다식을 공부하다가 검색을 하는 중이신 분들에게는 저런 내용을 굉장히 많이 보셨을 것이다. 그럼 =>는 무슨 뜻을 가지는가? 그 내용은 아래에 예제를 보면 확인할 수 있다. 익명 메서드라고 불리는 람다식은 그냥 어렵게 생각하지 않고 그냥 이름이 없으니 또 사용할 수 없고 본래 메서드 처럼 값을 받아 =>는 return으로 번역해서 보면 된다. 이렇게까지는 다들 이해했을 것이다. 그럼?

int Add(int x, int y) { return x + y; }

private int gold;
int Gold {get return gold; } {set return (gold = value);}

다음 예제는 내가 람다식이 헷갈린 이유이자 이것이 해결되면 델리게이트에 대한 이해도 쉬워질 것이다. 

inventory.SetStatChangedCallback((ItemAtk, ItemHp, ItemDef) =>
    {
        MaxHp += ItemHp;
        MaxAtk += ItemAtk;
        MaxDef += ItemDef;
    });

강의에서 나왔던 한 람다식에 대한 이야기다. 이 때문에 =>이 return 이라는 것을 아는데 시간이 걸렸고 이걸 서술하는 사람도 몇 없다. (내가 못 찾았던 건가..?) 저기서 => 이후에 나오는 값 대입이 보이는데 자세히 보면 람다식의 매개변수와 함께 뒷 내용이 묶여있는 것을 볼 수 있다. 그럼 좀 더 보기 쉽게 내용을 고쳐보겠다.

inventory.SetStatChangedCallback((ItemAtk, ItemHp, ItemDef)
	return
    {
        MaxHp += ItemHp;
        MaxAtk += ItemAtk;
        MaxDef += ItemDef;
    });

이렇게 보면 이해가 되는 사람도 있을 것으로 보인다. 그냥 아까와 똑같다. 람다식으로 준 값을 안에다가 넣는다는 식이다. return에 들어간 내용은 메서드와 굉장히 유사하니 그냥 저 문구는 하나의 메서드에 들어가는 내용을 람다로 구현했다고 생각하면 편하다. 짧은 내용의 메서드의 경우 람다식을 사용하면 굉장히 직관적인 느낌이 들어 사용하기 좋고 => 이 return보다는 짧으니 적응만 하면 더 쓰기 좋고 가독성 좋은 코드를 만드는데 도움이 될 것이다.

그리고 델리게이트의 이야기가 나왔으니 추가 설명을 이어가보겠다. 

Action<float, float, float> statChangedCallback;

public void SetStatChangedCallback(Action<float, float, float> callback)
{
    statChangedCallback = callback; // 이게 델리게이트를 저장하는 부분
}

/// 위 아래는 서로 다른 클래스

inventory.SetStatChangedCallback((ItemAtk, ItemHp, ItemDef) =>
{
    MaxHp += ItemHp;
    MaxAtk += ItemAtk;
    MaxDef += ItemDef;
});

아까 예문을 긁어온 것이다. 델리게이트의 경우 같은 클래스가 아닌 타 클래스에서도 주고 받을 수 있다. 물론 inventory.SetStatChangedCallback에서 눈치채신 분들도 계시겠지만 결국 선언해야 값을 넣어준다는 사실은 똑같다. 다른 점이 있다면 메서드를 저장하여 활용할 수 있다는 것이다. 값을 한 번 제대로 지정해놓으면 필요한 메서드를 델리게이트를 통해 여러번 구현하기가 편하다는 것이다. 델리게이트도 예문을 바꾸면 다중 구독이 가능하다.

Action<float, float, float> statChangedCallback;

public void SetStatChangedCallback(Action<float, float, float> callback)
{
    statChangedCallback += callback; // 이게 델리게이트를 저장하는 부분
}

//다른 내용

public class Inventory
{
    // 이벤트 선언: EventHandler<T>는 표준 델리게이트 타입
    public event EventHandler<StatChangeEventArgs> OnStatChanged;
}

 inventory.OnStatChanged += HandleStatChange;

델리게이트를 사용한 두 예문을 비교하면 하나는 =을 하나는 +=을 사용하였다. 정말 간단하게 내용을 바꿀 수 있는 대신에 Delegate 설계 철학이 무너지기에 가능하면 event를 사용하는 편이 좋다는 것이다. 이러한 다중구독을 활용하면 Item을 장착했을 시, 플레이어에게 스텟올려주기, 장착여부 판단하기 등의 소요 값을 하나의 event에 저장하여 다룰 수 있다는 것이다. 사실 한번씩 다 적을 거라면 굳이 event에 줘야하는가? 당연히 의문이 들만하다. 허나 이점이라고 한다면 event의 값은 코드 중간에 넣는 것이 아닌 초기에 한 번 넣어주고 event를 코드 중간에 넣는다면 한눈에 기능을 확인할 수 있다. 각 클래스의 값을 하나씩 불러와서 다른 메서드에 값을 매번 불러와서 사용하느니 초기에 event 함수를 가져와 그곳에 다 넣는 것이 훨씬 편할 것이다.

항목  delegate 필드 (Action 등)  event
외부에서 = 가능? ✅ 가능 (callback = null 등) ❌ 불가능
외부에서 Invoke() 가능? ✅ 가능 ❌ 불가능
다중 구독 (+=) 가능? ❌ 기본적으로 안됨 (직접 구현해야 함) ✅ 자동 지원
외부에서 전체 덮어쓰기 위험 ✅ 있음 ❌ 없음
안전성 / 캡슐화 낮음 높음
설계 철학 유연한 코드 중심 안정성 중심의 이벤트 시스템