오늘은 델리게이트에 대해서 알아보겠습니다.
델리게이트 뜻은 대리자로 대신 해주는 사람으로 해석할 수 있습니다.
메서드를 참조하고 호출할 수 있는 참조 타입입니다.
변수에 데이터를 저장하듯이 메서드를 저장하여 나중에 호출할 수 있습니다.
델리게이트의 구조를 알아보기 전에 앞에서 언급했던 구조체, 열거형, 클래스를 다시 살펴보면 이렇게 정의할 수 있습니다.
public enum Type { Normal, Fire, Water } // 열거형 정의
public struct GameData // 구조체 정의
{
public int level;
}
public class Player { } // 클래스 정의
static void Main(string[] args)
{
Type type; // 열거형 호출
GameData gameData; // 구조체 호출
Player player; // 클래스 호출
}
델리게이트는 아래와 같은 형식을 가지고 있습니다.
public delegate 반환형 델리게이트이름(매개변수들);
델리게이트는 메서드를 참조하기 위한 타입이기 때문에 클래스나 구조체처럼 { } 안에 내용을 작성하지 않습니다.
public delegate float DelegateMethod1(float x, float y);
public delegate void DelegateMethod2(string str);
위 코드는 각각 다음과 같은 메서드를 참조할 수 있습니다.
using System;
internal class Program
{
// float 매개변수 2개를 받고 float 값을 반환하는 메서드
public static float Plus(float left, float right)
{
return left + right;
}
// string 매개변수 1개를 받고 반환값이 없는 메서드
public static void Message(string message)
{
Console.WriteLine(message);
}
public delegate float DelegateMethod1(float x, float y);
public delegate void DelegateMethod2(string str);
static void Main(string[] args)
{
// Plus 메서드를 delegate1에 참조시킨다.
DelegateMethod1 delegate1 = Plus;
// Message 메서드를 delegate2에 참조시킨다.
DelegateMethod2 delegate2 = Message;
float result = delegate1(10f, 20f);
Console.WriteLine(result);
delegate2("델리게이트 호출");
}
}
출력 결과는 다음과 같습니다.
30
델리게이트 호출
여기서 중요한 점은 델리게이트에 등록할 메서드의 형태가 델리게이트의 형태와 같아야 한다는 것입니다.
public delegate float DelegateMethod1(float x, float y);
이 델리게이트는 다음과 같은 형태의 메서드만 참조할 수 있습니다.
float 메서드이름(float 값1, float 값2)
즉, 반환형과 매개변수 구성이 일치해야 합니다.
델리게이트에 메서드를 등록할 때는 함수 뒤에 ()를 붙이지 않습니다.
DelegateMethod1 delegate1 = Plus;
여기서 Plus는 함수를 실행하는 것이 아니라 함수 자체를 전달하는 것입니다.
반대로 아래처럼 작성하면 함수 호출이 됩니다.
Plus(10f, 20f);
즉, 차이는 다음과 같습니다.
Plus(10f, 20f); // 함수 호출
DelegateMethod1 delegate1 = Plus; // 함수 참조
만약 아래처럼 작성하면 잘못된 코드가 됩니다.
DelegateMethod1 delegate1 = Plus(10f, 20f);
Plus(10f, 20f)는 실행 결과인 float 값을 반환합니다.
하지만 델리게이트에는 결과값이 아니라 메서드 자체를 저장해야 하므로 함수 이름만 전달해야 합니다.
델리게이트는 Invoke()를 통해 호출할 수 있습니다.
delegate1.Invoke(20f, 10f);
하지만 C#에서는 아래처럼 더 간단하게 호출할 수도 있습니다.
delegate1(20f, 10f);
두 코드는 같은 의미입니다.
delegate1.Invoke(20f, 10f);
delegate1(20f, 10f);
Invoke는 호출한다는 의미이며, 델리게이트가 참조하고 있는 메서드를 실행합니다.
델리게이트는 참조 타입이기 때문에 아래처럼 객체를 생성하듯 작성할 수도 있습니다.
DelegateMethod1 delegate1 = new DelegateMethod1(Plus);
하지만 최신 C#에서는 보통 아래처럼 간단하게 작성합니다.
DelegateMethod1 delegate1 = Plus;
두 코드는 같은 의미입니다.
직접 델리게이트를 선언해서 사용할 수도 있지만, 실제 개발에서는 C#에서 미리 제공하는 Action, Func를 더 자주 사용합니다.
Action<string> action = Message;
Func<float, float, float> func = Plus;
Action과 Func도 내부적으로는 델리게이트입니다.
Action은 반환값이 없는 메서드를 참조할 때 사용합니다.Func는 반환값이 있는 메서드를 참조할 때 사용합니다.즉, 직접 델리게이트를 선언하는 방식은 다음과 같고
public delegate void DelegateMethod2(string str);
DelegateMethod2 delegate2 = Message;
Action을 사용하면 아래처럼 더 간단하게 작성할 수 있습니다.
Action<string> action = Message;
앞에서 살펴본 델리게이트는 함수를 전달하기 위한 기술이고,
콜백은 전달받은 함수를 나중에 호출하는 프로그래밍 패턴입니다.
쉽게 말해 함수를 직접 호출하는 것이 아니라,
"나중에 특정 상황이 발생하면 이 함수를 실행해줘"
라고 등록해두는 방식입니다.
예를 들어 맛집 웨이팅을 한다고 가정해봅시다.
보통은 손님이 계속 식당에 찾아가서
"자리 났나요?"
라고 물어보지 않습니다.
대신 연락처를 남겨두고,
"자리가 나면 연락해주세요."
라고 요청합니다.
그리고 실제로 자리가 나면 식당에서 손님에게 연락을 줍니다.
이처럼 특정 시점에 역으로 호출받는 방식을 콜백(Callback)이라고 합니다.
게임을 개발할 때 버튼마다 별도의 클래스를 만드는 방법도 있습니다.
public class Button
{
public virtual void Click() { }
}
public class SaveButton : Button
{
public override void Click()
{
Console.WriteLine("저장");
}
}
public class LoadButton : Button
{
public override void Click()
{
Console.WriteLine("불러오기");
}
}
하지만 버튼 종류가 많아질수록 클래스 수도 계속 증가하게 됩니다.
SaveButton
LoadButton
JumpButton
ExitButton
OptionButton
...
즉, 버튼 하나를 추가할 때마다 클래스를 새로 만들어야 하므로 유지보수가 어려워집니다.
델리게이트를 사용하면 버튼이 어떤 기능을 실행할지 나중에 결정할 수 있습니다.
using System;
public class File
{
public void Save()
{
Console.WriteLine("저장");
}
public void Load()
{
Console.WriteLine("불러오기");
}
public void Jump()
{
Console.WriteLine("점프");
}
}
public class Button
{
// 버튼 클릭 시 호출될 함수를 저장하는 델리게이트
public delegate void ClickListener();
public ClickListener clickListener;
public void Click()
{
// 등록된 함수가 있을 때만 호출
clickListener?.Invoke();
}
}
public class Program
{
public static void Main()
{
File file = new File();
Button saveButton = new Button();
saveButton.clickListener = file.Save;
Button loadButton = new Button();
loadButton.clickListener = file.Load;
Button jumpButton = new Button();
jumpButton.clickListener = file.Jump;
saveButton.Click();
loadButton.Click();
jumpButton.Click();
}
}
실행 결과
저장
불러오기
점프
중요한 점은 버튼이 Save(), Load(), Jump()를 직접 호출하지 않는다는 것입니다.
saveButton.clickListener = file.Save;
여기서 버튼은 단순히 함수를 전달받아 저장만 합니다.
그리고 나중에 사용자가 버튼을 클릭했을 때
clickListener?.Invoke();
를 통해 저장해둔 함수를 호출합니다.
실제 Unity의 버튼 이벤트도 비슷한 방식으로 동작합니다.
button.onClick.AddListener(OpenPanel);
사용자가 버튼을 클릭하기 전까지는 OpenPanel()이 실행되지 않습니다.
버튼이 클릭되는 순간 Unity가 등록된 함수를 호출해주는데, 이것 역시 콜백의 대표적인 예시입니다.
앞에서 직접 델리게이트를 선언하는 방법을 살펴보았습니다.
public delegate int Calculate(int x, int y);
하지만 실제 개발에서는 이런 델리게이트를 직접 선언하는 경우보다 C#에서 기본적으로 제공하는 일반화 델리게이트를 사용하는 경우가 더 많습니다.
대표적으로 다음 세 가지가 있습니다.
| 종류 | 반환값 | 매개변수 |
|---|---|---|
| Action | void | 0개 이상 |
| Func | 있음 | 0개 이상 |
| Predicate | bool | 1개 |
Func는 반환값이 있는 메서드를 저장하기 위한 델리게이트입니다.
int Plus(int left, int right)
{
return left + right;
}
int Minus(int left, int right)
{
return left - right;
}
void Main()
{
// Func<매개변수1, 매개변수2, ..., 반환형>
Func<int, int, int> func;
func = Plus;
Console.WriteLine(func(10, 20));
func = Minus;
Console.WriteLine(func(10, 20));
}
출력 결과
30
-10
Func<int, int, int>를 해석하면 다음과 같습니다.
매개변수 int
매개변수 int
반환값 int
즉, 아래와 같은 형태의 메서드를 저장할 수 있습니다.
int Method(int x, int y)
Action은 반환값이 없는(void) 메서드를 저장하기 위한 델리게이트입니다.
실제 Unity 개발에서 가장 많이 사용하는 델리게이트 중 하나입니다.
void Message(string message)
{
Console.WriteLine(message);
}
void Main()
{
// Action<매개변수1, 매개변수2, ...>
Action<string> action;
action = Message;
action("Action 호출");
}
출력 결과
Action 호출
Action<string>은 다음과 같이 해석할 수 있습니다.
매개변수 string
반환값 없음(void)
즉, 아래와 같은 형태의 메서드를 저장할 수 있습니다.
void Method(string value)
Unity에서는 이벤트와 함께 자주 사용됩니다.
public event Action OnGameStart;
public event Action<int> OnHPChanged;
Predicate는 반환값이 bool이고 매개변수가 하나인 델리게이트입니다.
주로 조건 검사에 사용됩니다.
bool IsSentence(string str)
{
return str.Contains(' ');
}
void Main()
{
Predicate<string> predicate;
predicate = IsSentence;
Console.WriteLine(predicate("Hello World"));
Console.WriteLine(predicate("Hello"));
}
출력 결과
True
False
Predicate<string>은 다음과 같이 해석할 수 있습니다.
매개변수 string
반환값 bool
즉, 아래와 같은 형태의 메서드를 저장할 수 있습니다.
bool Method(string value)
앞에서 사용한 델리게이트는 아래와 같이 직접 선언해야 했습니다.
public delegate void MessageDelegate(string message);
그리고 사용할 때도
MessageDelegate delegate1 = Message;
처럼 사용했습니다.
하지만 일반화 델리게이트를 사용하면 별도의 선언 없이 바로 사용할 수 있습니다.
Action<string> action = Message;
즉, Action, Func, Predicate는 우리가 직접 델리게이트를 만들지 않아도 되도록 C#에서 미리 제공하는 델리게이트라고 생각하면 됩니다.
실제 개발에서는 직접 delegate를 선언하는 경우보다 Action, Func, Predicate를 사용하는 경우가 훨씬 많습니다.
델리게이트 체인은 말로 설명하는 것보다는 코드를 보면서 이해하는 것이 더 쉽습니다.
델리게이트는 += 를 통해 여러 메서드를 연결할 수 있습니다.
이렇게 연결된 메서드들은 델리게이트가 호출될 때 등록된 순서대로 실행됩니다.
-= 를 사용하면 등록된 메서드를 제거할 수 있으며,
= 를 사용해 새 메서드를 대입하면 기존에 연결되어 있던 메서드 목록은 모두 사라집니다. (⚠️ 기존에 등록된 것이 모두 사라지기 때문에 주의 필요)
모든 메서드가 제거되면 델리게이트 변수는 null이 될 수 있으므로,
호출하기 전에 null 체크를 해주는 것이 안전합니다.
using System;
internal class Program
{
public static void Main()
{
Action action = null;
action += Func1;
action += Func2;
action += Func3;
action += Func3; // 같은 메서드도 여러 번 등록 가능
action -= Func1; // Func1 제거
Console.WriteLine("첫 번째 호출");
if (action != null)
{
action();
}
Console.WriteLine();
action = Func4; // 기존에 등록된 Func2, Func3, Func3는 모두 사라짐
Console.WriteLine("두 번째 호출");
action();
Console.WriteLine();
action -= Func4; // 마지막 메서드 제거 → action은 null 상태가 됨
Console.WriteLine("세 번째 호출");
if (action != null)
{
action();
}
else
{
Console.WriteLine("등록된 메서드가 없습니다.");
}
}
public static void Func1()
{
Console.WriteLine("Func1");
}
public static void Func2()
{
Console.WriteLine("Func2");
}
public static void Func3()
{
Console.WriteLine("Func3");
}
public static void Func4()
{
Console.WriteLine("Func4");
}
}
첫 번째 호출
Func2
Func3 // 중복 호출 가능
Func3
두 번째 호출
Func4 // 기존에 등록된 Func2, Func3, Func3는 모두 사라짐
세 번째 호출
등록된 메서드가 없습니다. // Null에서는 호출 안하도록 체크 필요함
여기서 null 체크의 경우 밑의 예시를 들었지만 실무에서는 Invoke로 많이 사용합니다! 두 코드는 의미가 같습니다
Invoke는 그냥 호출한다는 의미입니다. action()과 동일합니다.
if (action != null)
{
action(); // 이렇게 잘 안 씀...
}
action?.Invoke(); // 실제 개발에서는 이렇게 사용함
델리게이트는 여러 메서드를 참조하고 호출할 수 있는 강력한 기능이지만, 외부에서 의도하지 않은 조작이 가능하다는 문제가 있습니다.
예를 들어 아래와 같이 일반 델리게이트를 선언했다고 가정해보겠습니다.
public Action OnDied;
그러면 외부 클래스에서 다음과 같은 작업이 가능합니다.
player.OnDied = null; // 기존에 등록된 메서드 제거
player.OnDied(); // 외부에서 임의로 호출
이 경우 Player 클래스 내부에서만 발생해야 하는 죽음 이벤트를 외부에서 마음대로 호출하거나 덮어쓸 수 있게 됩니다.
이러한 문제를 방지하기 위해 C#에서는 event 키워드를 제공합니다.
public event Action OnDied;
이벤트로 선언하면 외부에서는 다음 작업만 가능합니다.
player.OnDied += Sound;
player.OnDied -= Sound;
즉, 이벤트를 구독하거나 해제할 수는 있지만 임의로 호출하거나 덮어쓸 수는 없습니다.
따라서 이벤트는 다음과 같은 특징을 가집니다.
+=)할 수 있다.-=)할 수 있다.쉽게 말해 event는 델리게이트를 더욱 안전하게 사용하기 위해 일부 기능을 제한한 기능이라고 생각하면 됩니다.
event로 선언하면 외부에서는+=,-=를 통한 구독과 해제만 가능합니다.
즉, 외부 클래스가 이벤트를 직접 호출하거나null로 덮어쓰는 일을 막을 수 있습니다.
using System;
internal class Program
{
public class Player
{
private int hp = 100;
// 체력이 변경되었을 때 구독자에게 hp 값을 전달하는 이벤트
public event Action<int> OnHPChanged;
// 플레이어가 죽었을 때 구독자에게 알려주는 이벤트
public event Action OnDied;
public void Setup(int hp)
{
this.hp = hp;
// 이벤트가 null이 아닐 때만 호출
// OnHPChanged(hp)와 같은 의미지만, Invoke를 쓰면 "호출" 의미가 더 명확하다.
OnHPChanged?.Invoke(this.hp);
}
public void Die()
{
Console.WriteLine("You Die");
// 플레이어가 죽었다는 사실을 구독자들에게 알림
OnDied?.Invoke();
}
}
public class HPBar
{
public void SetHPBar(int hp)
{
Console.WriteLine("체력바 수치를 {0}로 설정합니다.", hp);
}
}
public class Monster
{
public void Stop()
{
Console.WriteLine("몬스터가 더 이상 쫓아가지 않습니다.");
}
}
public class Sound
{
public void DieSound()
{
Console.WriteLine("죽는 효과음");
}
}
static void Main()
{
Player player = new Player();
Monster monster1 = new Monster();
Monster monster2 = new Monster();
Sound sound = new Sound();
HPBar hpBar = new HPBar();
// 플레이어가 죽었을 때 실행할 메서드들을 이벤트에 등록
player.OnDied += monster1.Stop;
player.OnDied += monster2.Stop;
player.OnDied += sound.DieSound;
// 체력이 변경되었을 때 실행할 메서드를 이벤트에 등록
player.OnHPChanged += hpBar.SetHPBar;
player.Setup(100);
player.Setup(30);
player.Die();
// 이벤트 구독 해제
// Unity에서는 OnDisable 또는 OnDestroy에서 자주 처리한다.
player.OnDied -= monster1.Stop;
player.OnDied -= monster2.Stop;
player.OnDied -= sound.DieSound;
player.OnHPChanged -= hpBar.SetHPBar;
}
}
앞에서 살펴본 델리게이트는 일반적으로 미리 정의된 메서드를 등록하여 사용했습니다.
Action<string> action = Message;
하지만 어떤 기능은 한 번만 사용하고 다시 사용할 일이 없는 경우도 있습니다.
이럴 때는 굳이 별도의 메서드를 만들지 않고, 델리게이트에 직접 함수를 작성할 수 있습니다.
이를 무명 메서드(Anonymous Method) 라고 합니다.
일반적인 방법은 먼저 함수를 만들고 델리게이트에 등록하는 방식입니다.
void Message(string text)
{
Console.WriteLine(text);
}
Action<string> action = Message;
action("Hello");
하지만 한 번만 사용할 함수라면 아래처럼 델리게이트에 직접 작성할 수 있습니다.
Action<string> action = delegate(string text)
{
Console.WriteLine(text);
};
action("Hello");
출력 결과
Hello
여기서 delegate 키워드를 이용해 이름이 없는 메서드를 생성했기 때문에 무명 메서드라고 부릅니다.
무명 메서드는 편리하지만 문법이 다소 길다는 단점이 있습니다.
그래서 C#에서는 무명 메서드를 더욱 간결하게 표현할 수 있는 람다식(Lambda Expression)을 제공합니다.
위의 무명 메서드는 아래와 같이 변경할 수 있습니다.
Action<string> action = (string text) =>
{
Console.WriteLine(text);
};
매개변수의 타입을 추론할 수 있는 경우에는 타입도 생략 가능합니다.
Action<string> action = (text) =>
{
Console.WriteLine(text);
};
한 줄만 실행하는 경우에는 중괄호도 생략할 수 있습니다.
Action<string> action = text => Console.WriteLine(text);
모두 동일한 의미입니다.
다음과 같이 메서드를 따로 만드는 방법도 가능합니다.
void PrintMessage()
{
Console.WriteLine("출력");
}
button.Click += PrintMessage;
하지만 한 번만 사용할 기능이라면 굳이 메서드를 만들 필요가 없습니다.
button.Click += () =>
{
Console.WriteLine("출력");
};
즉,
재사용 예정
→ 일반 메서드
한 번만 사용
→ 람다식
으로 생각하면 됩니다.
람다식은 Unity에서도 매우 자주 사용됩니다.
예를 들어 버튼 이벤트를 연결할 때 다음과 같이 사용할 수 있습니다.
button.onClick.AddListener(() =>
{
Debug.Log("버튼 클릭");
});
또는 특정 값을 함께 전달할 수도 있습니다.
button.onClick.AddListener(() =>
{
OpenPopup(1);
});
만약 람다식을 사용하지 않는다면 별도의 메서드를 만들어야 합니다.
private void OpenPopupButton()
{
OpenPopup(1);
}
button.onClick.AddListener(OpenPopupButton);
즉, 간단한 동작을 한 번만 사용할 경우 람다식을 사용하면 코드를 더 간결하게 작성할 수 있습니다.
delegate(string text)
{
Console.WriteLine(text);
}
(string text) =>
{
Console.WriteLine(text);
}
text => Console.WriteLine(text);
위 세 코드는 모두 같은 의미입니다.
Button.onClick, Action, event와 함께 자주 사용됩니다.