개념을 보기 전에 실제로 사용되는 코드를 보고 Func가 어떤 역할을 할 지 유추해 보면 좋을 것 같습니다.
그래서 C#으로 만든 게임 서버에서 사용하는 코드를 가져와 봤습니다.
public class Listener
{
Socket _listenSocket;
// Func를 사용한 부분
Func<Session> _sessionFactory;
// Listen 소켓을 초기 세팅하는 함수
// 클라이언트의 접속을 받기 위한 세팅
public void Init(IPEndPoint endPoint, Func<Session> sessionFactory, int register = 10, int backlog = 100)
{
// ... Logic 생략 ...
}
// 비동기로 소켓을 Accept 처리하는 함수
// 비동기 처리가 완료되면 OnAcceptCompleted 함수가 호출됩니다.
private void RegisterAccept(SocketAsyncEventArgs args)
{
// ... Logic 생략 ...
}
// Accpet 결과가 나오면 Error인지 Success인지 확인 후 그에 맞는 로직 수행
// 성공 => 세션 Invoke
// 실패 => 실패 Log 콘솔에 출력
private void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
if (args.SocketError == SocketError.Success)
{
Session session = _sessionFactory.Invoke();
session.Start(args.AcceptSocket);
session.OnConnected(args.AcceptSocket.RemoteEndPoint);
}
else
Console.WriteLine(args.SocketError.ToString());
RegisterAccept(args);
}
}
게임 서버가 TCP 소켓 통신을 사용해서 서버가 클라이언트의 연결을 받도록 구현되어 있습니다.
이 때 Listener가 클라이언트의 접속을 처리하고 세션을 할당해 줍니다.
위 코드에서 Func가 사용된 부분은 클라이언트에게 세션을 할당해 주는 부분입니다.
OnAcceptCompleted
메서드를 보면 세션을 할당해주는 로직이 있습니다.
_sessionFactory
에 미리 구독한 세션 생성 로직을 Invoke
해서 생성해 줍니다.
여기서 우리가 추론해 볼 수 있는 것은 Func를 사용해서 어떠한 로직을 구독하고,
해당 로직이 필요할 때 Func를 실행 시키고 나온 반환 값을 받을 수 있다는 것입니다.
_listener.Init(endPoint, () => { return SessionManager.Instance.Generate(); });
위 코드는 SessionManager를 사용해서 세션을 생성해 return 하는 코드를 Func에 구독해 놓은 것입니다.
Func는 사용하는 입장에서 구독되어 있는 내부 로직이 추상화 되어 있습니다.
그래서 내부 로직을 자세히 몰라도 원하는 반환 값을 받아서 쉽게 사용할 수 있습니다.
delegate는 대리자, 위임이라는 뜻을 가지고 있습니다.
C++에서 함수 포인터와 같은 개념이라고 합니다.
주로 이벤트와 관련된 처리에 사용되며,
C#에서 델리게이트는 메서드의 포인터를 저장할 뿐 내부에 코드를 기술하지 않습니다.
이런 Delegate는 메서드를 다른 메서드로 전달할 수 있도록 하기 위해 만들어 졌습니다.
숫자 혹은 객체를 메서드의 라마티러로써 전달할 수 있듯이, 메서드 또한 파라미터로서 전달할 수 있습니다.
class baekTest
{
delegate string MyDelegate(string s);
static void Main(string[] args)
{
MyDelegate myDelegate = new MyDelegate(ConsoleOut);
string result = myDelegate.Invoke("Invoke");
Console.WriteLine(result);
}
private static string ConsoleOut(string s)
{
Console.WriteLine("Console 출력 함수");
return s;
}
}
코드를 실행 시 위와 같이 콘솔창에 출력됩니다.
Delegate(대리자)를 사용해서 함수를 대신 호출해줄 수 있습니다.
그리고 리턴 값도 동일하게 받을 수 있습니다.
여기서 Delegate 사용 시 지켜야 할 것은 같은 리턴과 파라미터가 같은 형식이 같아야 한다는 것입니다.
델리게이트 객체를 Invoke() 메서드를 써서 호출할 수 있습니다.
만약 메서드에 입력 파라미터가 있으면 위 예시 코드와 같이 Invoke() 안에 추가해야 합니다.
그리고 아래와 같이 Invoke() 메서드를 생략해도 됩니다.
string result = myDelegate("Invoke");
서론이 너무 길었는데 이제 Action과 Func에 대해 알아보려고 합니다.
Action과 Func에 대해 알기 위해서는 Delegate를 알고 있는게 이해가 더 쉽기 때문에 먼저 다뤄봤습니다.
가장 큰 차이점은
Func는 값을 반환하는 메서드를 나타내는 델리게이트이고,
Action은 값을 반환하지 않는 메서드를 나타내는 델리게이트입니다.
Func<int, string>
는 int
를 입력으로 받아 string
을 반환하는 메서드를 나타냅니다.
반대로 Action<int, string>
은 int
와 string
을 입력으로 받고, 아무런 값을 반환하지 않는 메서드를 나타냅니다.
위 예시를 보면 알 수 있듯이 Action과 Func의 공통점은 제네릭 형식이라는 것입니다.
이렇게 제네릭 형식으로 미리 정의되어 있는 매개변수와 반환 타입을 간결하게 표현할 수 있습니다.
MSDocs에 각각 정의되어 있는 내용을 보면
Action<T>
대리자 : 매개 변수가 하나이고 값을 반환하지 않는 메서드를 캡슐화합니다.Func<T, TResult>
대리자 : 매개 변수가 하나이고 TResult 매개 변수에 지정된 형식의 값을 반환하는 메서드를 캡슐화합니다.여기서 메서들 캡슐화 한다는 것에 의미는 대리자가 대신 호출을 해주기 때문에 메서드의 로직은 숨겨져 있습니다.
그래서 메서드는 캡슐화가 되어 있다고 하는 것입니다.
Func와 Action은 이벤트 처리, 비동기 프로세스 처리 등을 위해 유연하고 재사용 가능한 코드를 작성하는데 필수적인 요소입니다.
그리고 코드를 좀 더 간결하게 만들어주기도 합니다.
private void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
{
if (args.SocketError == SocketError.Success)
{
Session session = _sessionFactory.Invoke();
session.Start(args.AcceptSocket);
session.OnConnected(args.AcceptSocket.RemoteEndPoint);
}
else
Console.WriteLine(args.SocketError.ToString());
RegisterAccept(args);
}
위 예제 코드에서 아래의 코드를 보면 Func의 장점을 알 수 있습니다.
Session session = _sessionFactory.Invoke();
위와 같이 Session에 대한 세팅을 Func를 사용해 캡슐화된 메서드를 쉽게 호출 할 수 있습니다.
그 결과 좀 더 직관적이고 깔끔한 코드가 완성되게 됩니다.
강의를 들으면서 설명을 들어도 이해가 되지 않던 부분을 글로 정리하면서 이해가 되었습니다.
확실히 적는 것이 시간은 오래 걸려도 이해에 도움이 되고 기억에 오래 남는 것 같습니다.
Func와 Action은 잘 사용하면 상당히 유용하고 많이 사용할 것 같은 것이라고 생각했습니다.
파생되는 개념도 같이 공부를 하게 되니 Invoke부터 Delegate까지 꽤 많은 것들을 공부하게 되었습니다.
그리고 예제 코드로 가져온 게임 서버 코드와 연결 시켜서 공부하니까 실용적인 부분까지 알게 되었습니다.