DevDino

[게임 디자인 패턴] 옵저버 패턴(Observer Pattern) 본문

Unity/디자인 패턴

[게임 디자인 패턴] 옵저버 패턴(Observer Pattern)

데브다이노 2026. 7. 14. 14:49

Observer Pattern

 

게임을 개발하다 보면 특정 이벤트가 발생했을 때 여러 시스템이 동시에 반응해야 하는 상황이 자주 생깁니다.

 

예를 들어, 경주용 오토바이가 장애물과 충돌할 때마다 HUD에서 실드의 현재 체력 값을 업데이트하고 감소하는 지구력을 보여주기 위해 화면 가장자리를 어둡게 하는 전체 화면 셰이더가 보여주는 연출이 있을 수 있습니다. 

 

단순하게 BikeController에서 각 컨트롤러를 참조해서 구현할 수 있지만, 복잡해지면 관리해야 하는 종속성에 대한 호출이 더 많아져 확장하기 쉽지 않습니다. 이러한 의존성 문제를 해결하고, 객체 간의 관계를 느슨하게 만들어주는 옵저버 패턴(Observer Pattern)에 대해 알아보겠습니다. 

 

옵저버 패턴(Observer Pattern)이란? 

옵저버 패턴은 객체의 상태 변화가 있을 때마다 그 객체를 관찰하고 있는 다른 객체들(Observer)에게 자동으로 알림을 보내는 디자인 패턴입니다. 

 

기존에는 주체가 되는 클래스가 다른 클래스들의 함수를 직접 호출했다면, 옵저버 패턴을 적용하면 주체는 상태 변경을 알리고 이를 구독(관찰) 중인 객체들이 알아서 각자의 로직을 수행하게 됩니다. 

 

옵저버 패턴의 장점

- 느슨한 결합(Loose Coupling): 알림을 보내는 쪽과 받는 쪽이 서로의 구체적인 구현을 몰라도 됩니다. 

- 유지보수 및 확장성 증가: 새로운 기능을 추가할 때, 기존 매니저 코드를 수정할 필요 없이 새로운 옵저버 클래스만 추가하여 구독시키면 됩니다(단일 책임 원칙 준수).

- 안정적인 생명주기 관리: 객체가 생성될 때 구독하고, 파괴될 때 구독을 해제하는 방식을 직접 통제하여 메모리 누수와 널 참조(Null Reference) 에러를 방지하기 쉽습니다. 

 

구현 예제

옵저버 패턴을 유니티에 적용하기 위해 크게 3가지 요소가 필요합니다. 

1. IObserver / ISubject(인터페이스): 관찰자와 관찰 대상이 지켜야 할 공통 규칙을 정의합니다. 

2. Subject(관찰 대상 / 주체): 옵저버들의 목록을 관리하고, 상태가 변했을 때 알림을 보냅니다. 

3. Concrete Observer(구체적인 관찰자): IObserver를 상속받아 알림을 받았을 때의 실제 동작을 구현하는 클래스들입니다. 

 

1. 인터페이스 정의하기

가장 먼저 관찰자와 주체가 사용할 인터페이스를 만듭니다.

// 관찰자(구독자) 인터페이스
public interface IObserver
{
    // 주체의 상태가 변경되었을 때 호출되는 메서드입니다.
    // 변경된 데이터나 메시지를 인자로 넘겨받습니다.
    void UpdateState(string message);
}

// 관찰 대상(주체) 인터페이스
public interface ISubject
{
    void RegisterObserver(IObserver observer);   // 구독 등록
    void RemoveObserver(IObserver observer);   // 구독 해제
    void NotifyObservers();                     // 모든 구독자에게 알림 전송
}

 

2. 구체적인 관찰 대상(Subject) 클래스 만들기

상태 데이터를 안전하게 관리하는 핵심 클래스입니다. ISubject를 상속받아 구현하며, 데이터가 변경되면 자신을 관찰하는 모든 이들에게 알림을 전송합니다. 

using System;
using System.Collections.Generic;

public class DataNotifier : ISubject
{
    // 구독자들을 담을 리스트입니다.
    private List<IObserver> _observers = new List<IObserver>();
    
    // 관찰 대상이 내부적으로 관리할 데이터(상태)입니다.
    private string _latestData;

    // 데이터를 갱신하는 핵심 메서드입니다.
    public void SetData(string newData)
    {
        _latestData = newData;
        Debug.Log($"[Subject] 데이터가 변경되었습니다: {_latestData}");
        
        // 데이터가 바뀌었으므로 등록된 구독자들에게 일제히 알림을 보냅니다.
        NotifyObservers();
    }

    public void RegisterObserver(IObserver observer)
    {
        if (!_observers.Contains(observer))
            _observers.Add(observer);
    }

    public void RemoveObserver(IObserver observer)
    {
        if (_observers.Contains(observer))
            _observers.Remove(observer);
    }

    public void NotifyObservers()
    {
        foreach (IObserver observer in _observers)
        {
            // 각 구독자들에게 최신 데이터를 전달하며 업데이트를 요청합니다.
            observer.UpdateState(_latestData);
        }
    }
}

 

3. 구체적인 관찰자(Observer) 클래스들 만들기

알림을 받았을 때 실제로 반응할 클래스들입니다. IObserver 인터페이스를 구현하며, 알림을 처리하는 세부 방식은 각자 다르게 작성할 수 있습니다. 

using UnityEngine;

// 데이터를 받아 화면에 출력하는 역할
public class ScreenDisplay : IObserver
{
    public void UpdateState(string message)
    {
        Debug.Log($"[ScreenDisplay] 화면에 최신 정보 표시: {message}");
    }
}

// 데이터를 받아 시스템 로그에 기록하는 역할
public class Logger : IObserver
{
    public void UpdateState(string message)
    {
        Debug.Log($"[Logger] 시스템 로그 파일에 기록 완료: {message}");
    }
}

 

4. 유니티 적용 예시

using UnityEngine;

public class ObserverTest : MonoBehaviour
{
    void Start()
    {
        // 1. 관찰 대상(주체) 생성
        DataNotifier notifier = new DataNotifier();

        // 2. 관찰자(구독자)들 생성
        ScreenDisplay display = new ScreenDisplay();
        Logger logger = new Logger();

        // 3. 주체에 관찰자들을 등록 (구독 시작)
        notifier.RegisterObserver(display);
        notifier.RegisterObserver(logger);

        // 4. 데이터 변경 (등록된 모든 관찰자에게 알림이 자동으로 발송됨)
        Debug.Log("--- 첫 번째 데이터 업데이트 ---");
        notifier.SetData("보스 몬스터 등장!");
        
        // 5. 특정 관찰자 구독 해제 (더 이상 알림을 받지 않음)
        notifier.RemoveObserver(logger);

        // 6. 데이터 재변경 (남아있는 구독자에게만 알림이 발송됨)
        Debug.Log("--- 두 번째 데이터 업데이트 ---");
        notifier.SetData("보스 몬스터 체력 50% 이하!");
    }
}

 

DataNotifier는 ScreenDisplay나 Logger라는 구체적인 클래스에 대해 아무것도 알지 못합니다. 그저 IObserver 인터페이스를 구현한 객체들의 리스트만 관리할 뿐입니다. 

 

이렇게 설계하면 나중에 새로운 관찰자가 새롭게 추가되더라도, 기존의 주체 코드는 수정할 필요가 없습니다.

이것이 바로 옵저버 패턴이 제공하는 강력한 느슨한 결합과 확장성의 핵심입니다.