Observer Patter - 옵저버 패턴
객체 간의 1:N(One-to-Many) 관계를 정의하여, 하나의 객체 상태가 바뀌면 그 변화를 자동으로 다른 객체들에 알리는 디자인 패턴이다.
Publisher(발행자)가 변경을 통보하면, Subsriber(구독자) 들이 자동으로 반응하는 구조다.
웹 프론트엔드에서도 서버나 전역 상태관리를 구독, 구독 해제 하는 기능을 사용해왔었는데 그것과 매우 유사해 보인다.
여러 객체가 하나의 상태 변화에 동시에 반응해야 할 때 유용하게 사용된다
- 플레이어 체력 감소 → HP UI, 사운드, 로그 시스템 모두 반응
- 게임 종료(GameOver) → 타이머 멈춤, 버튼 활성화, 텍스트 변경
- 데이터 변경 → UI에 즉시 반영
이러한 경우, 발행자가 직접 모든 객체를 참조하고 일일이 갱신하는 것은 코드 결합도를 높이고 유지보수를 어렵게 만든다.
옵저버 패턴을 사용하면, "누가 반응할지는 발행자가 모른다"는 구조로 변경되어 확장성과 유지보수성이 향상될 수 있다.
구조
옵저버 패턴은 크게 세 가지 구성 요소로 이루어진다.
구성 요소 역할
| Subject (또는 Publisher) | 상태 변화를 알리는 주체 |
| Observer (또는 Subscriber) | 변화를 감지해 동작하는 수신자 |
| Notify() | Subject가 Observer들에게 통보하는 메서드 |
예시 구조
public class Player
{
public event Action OnDead; // Observer들이 구독할 이벤트
public void Die()
{
OnDead?.Invoke(); // 사망 시 알림
}
}
public class GameManager
{
private Player _player;
public void Init()
{
_player = new Player();
_player.OnDead += OnPlayerDead; // 구독
}
private void OnPlayerDead()
{
Console.WriteLine("Game Over");
_player = null;
}
}
이 구조에서 Player는 Publisher
GameManager는 Subscriber(Observer) 이다
OnDead 이벤트를 발행하면, 이를 구독한 모든 객체가 자동으로 반응한다.
이 구조를 이제 Unity에서 적용해보자.
GameManager(싱글톤)가 시간 상태를 발행하고
TimerUI가 이를 구독하여 자동으로 갱신하는 구조의 옵저버 패턴을 구성해보자.
public enum GameState
{
Playing,
Pause,
GameOver
}
public class GameManager : Singleton<GameManager>
{
[SerializeField] private GameState _state = GameState.Playing;
public GameState State => _state;
private readonly List<IGameStateObserver> _observers = new List<IGameStateObserver>();
private float _timer;
public float Timer => _timer;
void Update()
{
if (_state == GameState.Playing)
{
_timer += Time.deltaTime;
// 등록된 모든 Observer에 시간 업데이트 알림
foreach (IGameStateObserver obs in _observers)
{
obs.OnGameTimerUpdated(_timer);
}
}
}
// 옵저버 등록 / 해제
public void RegisterObserver(IGameStateObserver obs)
{
if (!_observers.Contains(obs)) _observers.Add(obs);
}
public void UnregisterObserver(IGameStateObserver obs)
{
_observers.Remove(obs);
}
// 상태 변경 알림
private void NotifyObservers()
{
foreach (IGameStateObserver obs in _observers)
obs.OnGameStateChanged(_state);
}
public void GameStart()
{
_state = GameState.Playing;
NotifyObservers();
}
public void GameOver()
{
if (_state == GameState.GameOver) return;
_state = GameState.GameOver;
Debug.Log("Game Over");
NotifyObservers();
}
}
GameState가 Playing 상태라면 타이머가 매 프레임 증가하고, 등록된 모든 옵저버에 OnGameTimerUpdated()를 호출한다.
상태가 변경되면 NotifyObserver()로 전체 통보를 하고, Subscriber는 옵저버를 통해 그 데이터를 사용하면 된다.
Text (TMP)로 타이머를 출력하는 코드와 연결해보자
public interface IGameStateObserver
{
void OnGameStateChanged(GameState state);
void OnGameTimerUpdated(float time);
}
public class TimerUI : MonoBehaviour, IGameStateObserver
{
[SerializeField] private TMP_Text timerText;
void Start()
{
UpdateTimerText(0f);
GameManager.Instance.RegisterObserver(this); // 구독 등록
}
void OnDestroy()
{
if (GameManager.Instance != null)
GameManager.Instance.UnregisterObserver(this); // 해제
}
private void UpdateTimerText(float time)
{
TimeSpan t = TimeSpan.FromSeconds(time);
timerText.text = $"{t.Minutes:D2}:{t.Seconds:D2}";
}
public void OnGameTimerUpdated(float time)
{
UpdateTimerText(time);
}
public void OnGameStateChanged(GameState state)
{
if (state == GameState.GameOver)
UpdateTimerText(GameManager.Instance.Timer);
}
}
이렇게 TimerUI가 없어도 GameManager에서 타이머는 독립적으로 동작하기 때문에
또 다른 곳에서 타이머를 사용할 일이 있으면 쉽게 연결하거나 해제할 수도 있고 확장에도 용이한 형태가 된다.
정리
옵저버 패턴은 다음과 같은 장점이 있다는 것을 알 수 있다.
| 결합도 감소 | GameManager는 UI, 사운드, 로그 등 구체 클래스를 알 필요가 없음 |
| 확장 용이성 | 새 UI나 시스템을 추가해도 기존 코드 수정 없이 구독만 하면 됨 |
| 역할 분리 명확 | GameManager는 “상태 관리”, TimerUI는 “표시”에만 집중 |
| 유지보수성 향상 | 디버깅, 테스트, 리팩토링이 쉬워짐 |
주의
| 구독 해제 누락 | 옵저버가 Destroy되었는데 해제되지 않으면 메모리 누수 발생 | OnDestroy()에서 반드시 UnregisterObserver() 호출 |
| 순환 참조 | Observer가 Subject를 강하게 참조할 경우 GC 해제 불가 | 이벤트 기반 참조 또는 약한 참조(WeakReference) 사용 |
| 디버깅 어려움 | 누가 구독 중인지 추적 어려움 | 등록/해제 시 로그 출력, Inspector 디버그 툴 활용 |
실제로 위 코드는 기존 코드에 적용하는 과정에서 다른 로직이 꼬이는 현상이 있었다.
구독해제 뿐만 아니라 유사한 상황 (파괴, 비활성화, 씬전환 후 초기화 등)에서도 비슷한 문제가 발생할 수 있으니 코드 작성시 충분히 주의하도록 하자.
'Unity' 카테고리의 다른 글
| Unity 프로젝트 이슈 해결 - 범위내 적 타게팅 최적화 (0) | 2025.11.04 |
|---|---|
| Unity - Raycast (레이캐스트) (0) | 2025.10.22 |
| Unity - UGUI : Unity Graphical User Interface (0) | 2025.10.21 |
| Unity - Spawner에 Singleton Pattern 적용하기 (0) | 2025.10.20 |
| Unity - EventSystem (이벤트 시스템) (0) | 2025.10.20 |