프로그래밍 초보 시절, 단순한 메서드 호출을 넘어서 다양한 동작을 유연하게 처리하고자 했던 경험은 누구나 공감할 수 있다. 이번 글에서는 기존에 알고 있던 델리게이트, 이벤트, 그리고 전략 패턴의 관계를 다시 들여다보고, 그 본질적인 유사성과 차이점을 살펴본다. 단순한 개념 설명을 넘어, 실제로 어떻게 설계가 달라지는지, 그리고 왜 .NET에서는 이를 특별히 강조하는지를 깊이 있게 논의한다.
기본 시나리오: 다국어 인사하기
예를 들어, 한 명의 사용자가 중국어, 영문, 일본어 등 다양한 언어로 인사를 할 수 있다고 가정해보자. 초기에는 다음과 같이 조건문으로 구현할 수 있다:
public void Greet(string name, string language)
{
switch (language)
{
case "zh":
Console.WriteLine("밥 먹었어요? " + name);
break;
case "en":
Console.WriteLine("How are you, " + name);
break;
// 추가 언어...
}
}
하지만 언어가 늘어날수록 switch 문이 길어지고, 유지보수가 어려워진다. 이는 단일 책임 원칙을 위반하며, 확장성과 유연성을 저해한다.
전략 패턴으로 해결하기
이 문제를 해결하기 위해 전략 패턴을 적용해보자. 인터페이스를 정의하고, 각 언어에 맞는 구체 클래스를 만들어 객체로 주입한다:
public interface IGreetingStrategy
{
void Greet(string name);
}
public class ChineseGreeting : IGreetingStrategy
{
public void Greet(string name) => Console.WriteLine("밥 먹었어요? " + name);
}
public class EnglishGreeting : IGreetingStrategy
{
public void Greet(string name) => Console.WriteLine("How are you, " + name);
}
public class GreetingContext
{
private IGreetingStrategy strategy;
public GreetingContext(IGreetingStrategy strategy)
{
this.strategy = strategy;
}
public void SetStrategy(IGreetingStrategy strategy)
{
this.strategy = strategy;
}
public void Execute(string name)
{
strategy.Greet(name);
}
}
이 방식은 클라이언트가 런타임에 어떤 전략을 사용할지 결정할 수 있으며, 새로운 언어를 추가하더라도 기존 코드를 수정할 필요 없이 새 클래스만 만들면 된다.
델리게이트: 함수를 값처럼 다루기
하지만 전략 패턴은 일정한 클래스 구조를 요구한다. 만약 간단한 메서드 하나만 필요하다면, 델리게이트를 활용하면 더 간결하게 해결할 수 있다. 델리게이트는 특정 서명을 가진 메서드를 참조할 수 있는 타입이다.
// 델리게이트 선언
public delegate void GreetingHandler(string name);
// 사용 예시
static void Main()
{
GreetingHandler handler = new GreetingHandler(new Program().ChineseGreeting);
handler("김철수");
// 또는 직접 참조 가능
GreetingHandler english = new Program().EnglishGreeting;
english("Alice");
}
델리게이트는 메서드를 변수처럼 다룰 수 있게 해주며, +=와 -= 연산자를 통해 여러 메서드를 등록하거나 제거할 수 있다. 이는 옵저버 패턴의 핵심 구조와도 매우 유사하다.
이벤트: 델리게이트의 안전한 래핑
그러나 일반 델리게이트는 외부에서 직접 할당이나 호출이 가능하여, 보안과 캡슐화 측면에서 문제가 생길 수 있다. 이를 해결하기 위해 .NET은 event 키워드를 도입했다. 이는 델리게이트를 은폐하고, 오직 +=와 -=만을 허용함으로써, 이벤트 발생자만이 이벤트를 트리거할 수 있도록 한다.
public event GreetingHandler OnGreet;
// 클라이언트는 이렇게만 사용 가능
myButton.OnGreet += HandleGreet;
// 하지만 직접 대입은 불가능
// myButton.OnGreet = someMethod; // 컴파일 에러
또한, 표준 규약에 따라 이벤트는 EventHandler<TEventArgs> 형식을 따르며, 첫 번째 파라미터는 object sender, 두 번째는 EventArgs 상속 객체를 전달한다. 이는 이벤트의 추상화와 재사용성을 높이는 데 기여한다.
핵심 정리: 무엇이 진짜 중요한가?
전략 패턴, 델리게이트, 이벤트는 모두 동작의 분리와 유연한 실행 시점 결정이라는 공통된 목적을 가지고 있다. 전략 패턴은 객체 지향적 접근이고, 델리게이트는 함수형 접근이며, 이벤트는 이러한 접근을 안전하게 래핑한 형태다.
결론적으로, 델리게이트는 단순한 메서드 포인터가 아니라, 행동을 객체처럼 다루는 강력한 도구이며, 이벤트는 그 안정성과 보안성을 보장하는 프레임워크 수준의 설계 패턴이라고 볼 수 있다. 따라서 .NET 개발자는 이들을 이해하고 적절히 활용해야 한다.