내일배움캠프

내일배움캠프 14일차: 이벤트

pracumj 2024. 10. 1. 22:20

오늘 공부한 내용📝

오늘은 C#에서 event라는 개념을 배웠다. delegate를 활용할 때 왜 event를 사용해야 하는지 헷갈렸지만, 여러 예제를 보며 delegate와의 차이를 이해할 수 있었다. event는 delegate의 접근을 제어하는 역할을 하며, 외부 클래스에서 직접 delegate를 호출하지 못하도록 막아주는 역할을 한다.

왜 event를 사용해야 할까?🤔

 event는 delegate의 외부 접근을 제한하고, 클래스 내에서만 안전하게 호출할 수 있게 해준기 때문이다. 예를 들어, delegate를 사용한 경우 외부에서 호출을 막지 않으면 예상치 못한 메서드가 실행될 수 있다. 이를 방지하기 위해 event를 사용해 이벤트 구독만 가능하게 하고 호출 권한을 클래스 내부로 한정시키는 것이 좋기 때문이다.

코드 예시

using System;

public delegate void DamageHandler(int currentHealth);

public class Player
{
    // event 선언
    public event DamageHandler OnDamage;

    private int health;

    public Player(int initialHealth)
    {
        health = initialHealth;
    }

    public void TakeDamage(int damage)
    {
        health -= damage;
        Console.WriteLine($"플레이어가 {damage} 만큼의 피해를 입었습니다.");

        // 이벤트 호출 (외부에서는 이 호출을 막을 수 있음)
        OnDamage?.Invoke(health);

        if (health <= 0)
        {
            Console.WriteLine("Player is dead.");
        }
    }
}

public class Program
{
    static void Main(string[] args)
    {
        Player player = new Player(100);
        
        // 이벤트 구독
        player.OnDamage += DisplayHealthChange;

        // 플레이어에게 데미지를 줘서 이벤트 발생
        player.TakeDamage(30);
        player.TakeDamage(50);
        player.TakeDamage(30);
       
        // 이벤트 핸들러 해제
        player.OnDamage -= DisplayHealthChange;
        Console.WriteLine("이벤트 핸들러 해제됨.");

        // 이벤트 해제 후에는 이벤트 핸들러가 호출되지 않음
        player.TakeDamage(30);
    }

    static void DisplayHealthChange(int currentHealth)
    {
        Console.WriteLine($"현재 체력: {currentHealth}");
    }
}
플레이어가 30 만큼의 피해를 입었습니다.
현재 체력: 70

플레이어가 50 만큼의 피해를 입었습니다.
현재 체력: 20

플레이어가 30 만큼의 피해를 입었습니다.
현재 체력: -10
Player is dead.

이벤트 핸들러 해제됨.
플레이어가 30 만큼의 피해를 입었습니다.


위 코드에서는 OnDamage라는 event를 사용해 플레이어의 체력이 변할 때마다 이벤트가 발생하고, 외부에서는 이를 구독하여 체력 변화를 감지할 수 있다. event가 없다면 외부에서 마음대로 OnDamage를 호출할 수 있어 코드의 안정성이 떨어질 수 있다.


null 조건부 연산자(?.)의 사용 이유는?

  • OnDamage?.Invoke(health); 이 부분에서 ?. 연산자는 null 조건부 연산자로, OnDamage 이벤트가 null일 경우 Invoke 메서드를 호출하지 않고 아무 작업도 하지 않는다. 이 방식은 이벤트를 구독한 핸들러가 없을 때 Invoke를 호출하려고 하면 발생할 수 있는 NullReferenceException 오류를 방지해준다.
  • 예를 들어, 위 코드에서 player.OnDamage -= DisplayHealthChange;와 같이 모든 이벤트 구독자를 해제하면 OnDamage는 null이 된다. 만약 이 상태에서 OnDamage.Invoke(health);를 호출하려 하면 NullReferenceException이 발생하여 프로그램이 중단될 수 있다.

null 조건부 연산자의 역할

  1. null 체크를 간편하게 처리: if문을 통해 null 체크를 하지 않고도 ?. 연산자는 해당 객체가 null인지 체크한 후 null이 아니면 메서드나 속성에 접근한다.
    OnDamage?.Invoke(health); // OnDamage가 null이 아닐 때만 Invoke 호출
  2. 안전한 이벤트 호출: 이벤트 구독자가 없을 때 null 상태에서 Invoke를 호출해도 오류가 발생하지 않는다.

 


이벤트 해제의 중요성

이벤트 핸들러를 구독만 하고 해제하지 않는다면 메모리 누수가 발생할 수 있다. 이는 이벤트를 구독한 객체가 여전히 메모리에 남아 있어 가비지 컬렉션에 의해 제대로 해제되지 않기 때문이다. 특히, 이벤트를 구독한 객체가 더 이상 사용되지 않는데도 이벤트 호출 시 계속해서 참조된다면, 메모리와 성능 문제가 발생할 수 있다.
이러한 문제를 방지하기 위해 필요하지 않은 이벤트 핸들러는 적절한 시점에 반드시 해제해야 한다. 예를 들어, UI가 파괴되었거나, 게임 오브젝트가 삭제되었을 때 이벤트 구독을 해제해 줌으로써 객체가 메모리에서 올바르게 해제될 수 있도록 해야 한다.


delegate로 외부에서 호출 시 발생할 수 있는 문제점

    1. 잘못된 이벤트 호출:
      delegate가 외부에서 호출되면, 의도하지 않은 시점이나 조건에서 이벤트가 발생할 수 있다.
    2. 상태 변화의 일관성 손상:
      delegate가 외부에서 임의로 호출되면, 객체의 상태 변화가 일관되지 않게 된다. 예를 들어, 체력이 감소하지 않았음에도 체력 감소 이벤트가 발생하면, 이후 로직이 체력 상태를 잘못된 값으로 처리하게 된다.

코드 예시

using System;

public delegate void DamageHandler(int currentHealth);

public class Player
{
    public DamageHandler OnDamage; // event가 아닌 delegate 사용

    private int health = 100;

    public void TakeDamage(int damage)
    {
        health -= damage;
        Console.WriteLine($"플레이어가 {damage} 만큼의 피해를 입었습니다.");

        OnDamage?.Invoke(health); // 내부에서만 호출되어야 하는 이벤트
    }
}

public class Program
{
    static void Main(string[] args)
    {
        Player player = new Player();

        // 이벤트 구독
        player.OnDamage += DisplayHealthChange;

        // 정상적으로 데미지 입히기
        player.TakeDamage(20);

        // 외부에서 임의로 delegate 호출 - 잘못된 호출로 인해 체력이 비정상적으로 변함
        player.OnDamage?.Invoke(999); // 실제로 체력이 감소한 것이 아닌데 호출됨

        player.TakeDamage(30);
    }

    static void DisplayHealthChange(int currentHealth)
    {
        Console.WriteLine($"현재 체력: {currentHealth}");
    }
}
출력 결과:
플레이어가 20 만큼의 피해를 입었습니다.
현재 체력: 80

플레이어가 30 만큼의 피해를 입었습니다.
현재 체력: 50

현재 체력: 999  // 외부에서 잘못된 값으로 이벤트 호출
플레이어가 30 만큼의 피해를 입었습니다.
현재 체력: 20


문제점 

player.OnDamage?.Invoke(999);와 같이 외부에서 delegate를 호출하면, 플레이어의 체력이 실제로 변하지 않았음에도 이벤트가 호출된다. 이는 체력과 관련된 모든 이벤트 구독자들이 잘못된 데이터를 받게 되어, 게임 로직이 꼬이거나 버그를 발생시킬 수 있다. OnDamage delegate는 플레이어가 실제로 데미지를 입었을 때만 호출되어야 하지만, 외부에서 호출됨으로 인해 현재 체력 값이 다르게 나오는 등 문제가 발생할  수 있다.

 

마무리😺

event는 delegate를 안전하게 사용하기 위한 확장 개념이다. 이벤트를 통해 클래스 내부의 상태 변화를 외부에 알리고, 외부에서는 이를 구독해 필요한 처리를 할 수 있다. 앞으로 다양한 상황에서 event를 활용해 더 안전하고 구조화된 코드를 작성해봐야겠다!