키보드 위의 거미줄

코드를 작성하다 보면 때로 이상한 감각에 빠져들 때가 있습니다. 키보드 위를 춤추는 손끝이 만들어내는 클래스들이 서로 얽히고설키며, 마치 거미줄처럼 복잡하게 엮여가는 모습을 보게 되죠. 한 줄을 고치면 다른 곳이 망가지고, 새로운 기능을 추가하려면 십여 개의 파일을 건드려야 하는… 그런 숨막히는 순간들 말입니다.

하지만 어느 날, 코드들이 서로 직접 손을 잡고 있지 않으면서도 아름답게 소통하는 방법이 있다는 걸 발견하게 됩니다. 마치 오케스트라의 연주자들이 지휘자의 손짓 하나로 완벽한 하모니를 만들어내듯이요.

게임 속에서 발견한 이야기

몇 년 전, 저는 RPG 게임을 개발하고 있었습니다. 플레이어가 적을 처치하면 경험치를 획득하고, UI를 업데이트하고, 사운드를 재생하고, 퀘스트 진행도를 확인해야 했죠. 처음에는 단순하게 생각했습니다. 적 클래스에서 이 모든 것들을 직접 호출하면 되잖아요?

public class Enemy : MonoBehaviour
{
    public void Die()
    {
        // 적 사망 처리
        gameObject.SetActive(false);
        
        // 모든 것을 직접 처리
        PlayerController.Instance.AddExperience(experienceReward);
        UIManager.Instance.UpdateHealthBar();
        AudioManager.Instance.PlaySound("enemy_death");
        QuestManager.Instance.CheckKillQuest(enemyType);
        EffectManager.Instance.PlayDeathEffect(transform.position);
    }
}

하지만 시간이 지나면서 문제가 보이기 시작했습니다. 새로운 시스템이 생길 때마다 Enemy 클래스를 수정해야 했고, 테스트하기도 어려웠으며, 각 매니저들이 서로 강하게 결합되어 있어서 유연성이 떨어졌죠. 마치 모든 사람이 한 사람에게만 의존해서 소통하는 조직처럼 비효율적이었습니다.

조용한 깨달음

그때 깨달은 것이 있습니다. 좋은 코드 아키텍처는 현실의 좋은 소통과 닮아있다는 것이요.

생각해보세요. 일상생활에서 우리는 모든 사람과 직접 대화하지 않습니다. 대신 “알림”이라는 방식을 사용하죠. 누군가 생일파티를 연다면, 모든 친구에게 일일이 전화하는 대신 단체 채팅방에 공지를 올리거나 이벤트를 만듭니다. 관심 있는 사람은 참여하고, 그렇지 않은 사람은 무시하면 되죠.

코드에서도 마찬가지입니다. 컴포넌트들이 서로 직접 알 필요 없이, “무언가 일어났다”는 소식만 전달하면 되는 경우가 많습니다. 이것이 바로 delegates와 events가 해결하려는 문제입니다.

메신저 패턴이라는 이름

이런 패턴을 저는 “메신저 패턴(Messenger Pattern)”이라고 부르고 싶습니다. 컴포넌트들이 직접 대화하지 않고, 중간에 메신저(delegates/events)를 통해 소식을 전달하는 방식이거든요.

Delegate는 메서드를 가리키는 포인터입니다. 여러 메서드를 하나의 delegate에 연결할 수 있어서, 마치 여러 사람의 전화번호를 저장한 연락처 그룹 같은 역할을 합니다.

Event는 delegate를 기반으로 한 특별한 형태로, 외부에서 함부로 조작할 수 없도록 보호된 알림 시스템입니다. 클래스 외부에서는 구독(+=)과 구독 해제(-=)만 할 수 있죠.

핵심은 “발신자는 수신자가 누구인지 몰라도 된다”는 것입니다. 그저 “이런 일이 일어났어”라고 알리기만 하면, 관심 있는 모든 이들이 알아서 반응합니다.

다섯 번의 작은 걸음

첫 번째: 이벤트 기반으로 생각하기

먼저 당신의 게임에서 일어나는 일들을 “무엇이 일어났다”의 관점에서 바라보세요. “적이 죽었다”, “플레이어가 아이템을 획득했다”, “레벨이 클리어되었다” 같은 식으로요.

public class Enemy : MonoBehaviour
{
    public static event System.Action<Enemy> OnEnemyDied;
    
    public void Die()
    {
        // 적 사망 처리
        gameObject.SetActive(false);
        
        // 이벤트 발생 - 누가 듣는지는 몰라도 됨
        OnEnemyDied?.Invoke(this);
    }
}

두 번째: 구독자 만들기

각 관심사별로 별도의 핸들러를 만드세요. 이들은 서로를 몰라도 됩니다.

public class ExperienceManager : MonoBehaviour
{
    void Start()
    {
        Enemy.OnEnemyDied += HandleEnemyDeath;
    }
    
    void OnDestroy()
    {
        Enemy.OnEnemyDied -= HandleEnemyDeath;
    }
    
    private void HandleEnemyDeath(Enemy enemy)
    {
        PlayerController.Instance.AddExperience(enemy.ExperienceReward);
    }
}

public class AudioManager : MonoBehaviour
{
    void Start()
    {
        Enemy.OnEnemyDied += HandleEnemyDeath;
    }
    
    void OnDestroy()
    {
        Enemy.OnEnemyDied -= HandleEnemyDeath;
    }
    
    private void HandleEnemyDeath(Enemy enemy)
    {
        PlaySound("enemy_death");
    }
}

public class QuestSystem : MonoBehaviour
{
    void Start()
    {
        Enemy.OnEnemyDied += HandleEnemyDeath;
    }
    
    void OnDestroy()
    {
        Enemy.OnEnemyDied -= HandleEnemyDeath;
    }
    
    private void HandleEnemyDeath(Enemy enemy)
    {
        CheckKillQuest(enemy.EnemyType);
    }
}

세 번째: 연결하기

유니티에서는 각 매니저들이 Start()에서 이벤트를 구독하고, OnDestroy()에서 구독을 해제합니다. 메모리 누수를 방지하기 위해 구독 해제는 필수입니다.

public class GameManager : MonoBehaviour
{
    void Start()
    {
        // 모든 시스템들이 자동으로 이벤트를 구독
        // 별도의 연결 코드가 필요 없음!
    }
}

네 번째: 안전하게 만들기

이벤트를 발생시킬 때는 항상 null 체크를 하고, 유니티의 라이프사이클을 고려하세요.

public class Player : MonoBehaviour
{
    public static event System.Action<int> OnHealthChanged;
    
    private int health = 100;
    
    public void TakeDamage(int damage)
    {
        health -= damage;
        
        // 안전한 이벤트 호출
        OnHealthChanged?.Invoke(health);
        
        if (health <= 0)
        {
            Die();
        }
    }
    
    void OnDestroy()
    {
        // 오브젝트가 파괴될 때 이벤트 초기화
        OnHealthChanged = null;
    }
}

다섯 번째: 테스트 친화적으로 만들기

이벤트 기반 설계는 테스트하기도 쉽습니다. 각 시스템을 독립적으로 테스트할 수 있거든요.

[Test]
public void EnemyDeath_ShouldTriggerExperienceGain()
{
    // Arrange
    var enemy = CreateTestEnemy();
    var initialExp = PlayerController.Instance.Experience;
    
    // Act
    enemy.Die();
    
    // Assert
    Assert.AreEqual(initialExp + enemy.ExperienceReward, 
                   PlayerController.Instance.Experience);
}

변화하는 시대 속에서

요즘 같은 시대에 이런 접근법이 더욱 중요해졌습니다. 마이크로서비스 아키텍처, 클라우드 네이티브 개발, 그리고 끊임없이 변화하는 비즈니스 요구사항들… 모든 것이 빠르게 변하고 있죠.

코드도 마찬가지입니다. 오늘 완벽했던 설계가 내일은 걸림돌이 될 수 있어요. 그렇기에 변화에 유연하게 대응할 수 있는 구조가 필요합니다. Delegates와 events는 이런 변화의 물결 속에서도 흔들리지 않는 든든한 기둥 역할을 해줍니다.

특히 현대의 분산 시스템에서는 이벤트 기반 아키텍처가 표준이 되어가고 있어요. AWS EventBridge, Azure Event Grid, Apache Kafka… 이 모든 것들이 같은 철학을 기반으로 합니다. 컴포넌트들이 서로 직접 연결되지 않고, 이벤트를 통해 느슨하게 결합되는 방식 말이죠.

당신이 지금 배우고 있는 것은 단순히 C#의 문법이 아닙니다. 현대 소프트웨어 아키텍처의 핵심 원리이자, 앞으로 수년간 유효할 보편적 패턴입니다.

작은 용기에서 시작되는 변화

코드를 작성하다 보면 때로 우리는 모든 것을 통제하고 싶어합니다. 각 컴포넌트가 무엇을 하는지, 언제 어떤 순서로 실행되는지 세세하게 관리하려고 하죠. 하지만 진정으로 아름다운 코드는 통제에서 나오는 것이 아니라 신뢰에서 나옵니다.

Delegates와 events를 사용한다는 것은 결국 “믿음”의 행위입니다. 내가 발생시킨 이벤트를 누군가 적절히 처리해줄 것이라는 믿음, 시스템의 각 부분이 자신의 역할을 충실히 해낼 것이라는 믿음 말입니다.

이런 믿음이 쌓여서 만들어지는 코드는 마치 잘 훈련된 오케스트라처럼 조화롭게 동작합니다. 각자의 파트를 연주하면서도 전체의 하모니를 만들어내죠.

오늘 당신의 코드에서도 이런 아름다운 대화가 시작되길 바랍니다. 컴포넌트들이 서로 손을 맞잡는 대신, 공간을 두고 서로를 존중하며 소통하는… 그런 여유롭고 우아한 아키텍처를 만들어보세요.

그 첫걸음은 아주 작을 수 있습니다. 하나의 직접 호출을 이벤트로 바꿔보는 것부터 시작해도 좋아요. 변화는 언제나 작은 용기에서 시작되니까요.

제목 영역
이미지
본문 텍스트 영역