Observer

Observer 패턴이란?

옵저버Observer 패턴은 객체 간의 일대다one-to-many 관계를 정의하여, 한 객체의 상태 변화가 있을 때 관련된 다른 객체들에게 자동으로 통보되는 방식을 제공합니다. 이 패턴은 특히 이벤트 기반 시스템에서 자주 사용되며, 객체들 간의 결합도를 낮추고, 확장성을 높이는 데 기여합니다.

상태 변화와 통보의 필요성

애플리케이션 개발 시, 하나의 객체가 상태를 변경하면 이와 관련된 다른 객체들도 그 변화를 알아야 하는 경우가 자주 발생합니다. 예를 들어, 도서관 관리 시스템에서 새로운 책이 추가되거나 기존 책이 대여될 때, 관련된 여러 사용자 인터페이스UI 요소들이 그 변화를 실시간으로 반영해야 할 수 있습니다. 옵저버 패턴은 이러한 상황에서 매우 유용합니다. 이 패턴을 사용하면 주체(Subject) 객체가 상태 변화를 감지할 때마다 옵저버Observer 객체들에게 자동으로 통보할 수 있습니다. 이를 통해 주체와 옵저버 간의 결합도를 낮추고, 시스템이 더 유연하게 변화에 대응할 수 있도록 만듭니다.

Observer 패턴 구조

D2 Diagram

  • Client → Subject
    • 클라이언트는 Attach 또는 Detach를 호출하여 옵저버를 등록하거나 제거합니다.
    • 옵저버는 Observer 인터페이스를 구현한 ConcreteObserver 객체입니다.
  • ConcreteObserver → Subject
    • ConcreteObserverSubscribe 메서드를 호출하여 자신을 Subject에 등록합니다.
  • Subject → ConcreteObserver
    • Subject는 상태가 변경될 때 Notify를 호출하여 등록된 모든 옵저버에게 변경 사항을 알립니다.
  • ConcreteObserver
    • 각 옵저버는 상태 변경에 따라 고유한 동작을 수행합니다(예: 로그 기록, UI 업데이트 등).

옵저버 패턴의 구성 요소

옵저버 패턴은 주로 다음과 같은 구성 요소로 이루어져 있습니다:

주체Subject

상태를 가지고 있으며, 그 상태가 변경될 때 옵저버들에게 통보하는 역할을 합니다.

옵저버Observer

주체의 상태 변화를 감지하고, 그에 따라 행동을 취하는 객체들입니다. 옵저버들은 주체에 등록되어 있으며, 주체가 상태를 변경할 때 통보를 받습니다.

구체적인 주체Concrete Subject

주체의 구체적인 구현체로, 상태를 관리하고 옵저버들에게 상태 변화를 통보하는 역할을 합니다.

구체적인 옵저버Concrete Observer

옵저버의 구체적인 구현체로, 주체의 상태 변화에 따라 행동을 취하는 역할을 합니다.

Observer 패턴 적용

잘못된 상태 전달

도서관 관리 시스템에서 새로운 책이 추가될 때, 이를 실시간으로 UI에 반영하는 기능을 구현해 보겠습니다. 주체 객체(Book 클래스)와 옵저버 객체(UserInterface 클래스)가 직접적으로 결합된 잘못된 코드와 이를 사용하는 예시입니다.

// 도서 클래스
public class Book
{
    public string Title { get; private set; }
    public string Availability { get; private set; }
    private UserInterface _ui;  // 구체적인 UI 클래스에 의존
    public Book(string title, UserInterface ui)
    {
        Title = title;
        _ui = ui;
    }
    public void SetAvailability(string availability)
    {
        Availability = availability;
        _ui.Update(availability);  // UI 업데이트를 직접 호출
    }
}
// 사용자 인터페이스 클래스
public class UserInterface
{
    private string _userName;
    public UserInterface(string userName)
    {
        _userName = userName;
    }
    public void Update(string availability)
    {
        Console.WriteLine($"{_userName}에게 알림: 도서 상태가 '{availability}'로 변경되었습니다.");
    }
}
public class Program
{
    public static void Main(string[] args)
    {
        // 사용자 인터페이스 인스턴스 생성
        UserInterface ui = new UserInterface("Alice");
        // 도서 객체 생성 시 UI 객체를 전달
        Book book = new Book("C# Programming", ui);
        // 도서 상태 변경
        book.SetAvailability("Available");
        // 도서 상태가 변경될 때 UI 업데이트
        // 출력: Alice에게 알림: 도서 상태가 'Available'로 변경되었습니다.
    }
}

높은 결합도: Book 클래스가 UserInterface 클래스에 직접 의존

Book 클래스는 상태 변경 시 직접 UserInterface 클래스의 Update 메서드를 호출합니다. 이는 Book 클래스가 구체적인 UserInterface 클래스에 강하게 결합되어 있음을 의미합니다. 이로 인해, 다른 종류의 UI(예: 모바일 UI, 웹 UI 등)를 추가하거나 변경할 때 Book 클래스를 수정해야 합니다.

확장성 부족: 새로운 UI 요소 추가 어려움

만약 새로운 UI 요소를 추가하려면, Book 클래스에 해당 UI 요소를 위한 코드를 추가해야 합니다. 예를 들어, 또 다른 사용자 인터페이스를 추가해야 한다면, Book 클래스는 이를 위한 새로운 필드를 추가하고, SetAvailability 메서드를 수정해야 합니다. 이는 코드의 유지보수를 어렵게 하고, 확장성을 제한합니다.

단일 책임 원칙 위반: Book 클래스의 책임 과다

Book 클래스는 도서의 상태를 관리하는 역할 외에도, UI를 업데이트하는 책임을 가지게 됩니다. 이로 인해 Book 클래스가 지나치게 복잡해지고, 단일 책임 원칙(SRP)을 위반하게 됩니다. 이로 인해 코드의 재사용성도 저하됩니다.

재사용성 감소

Book 클래스는 특정 UI 클래스에 의존하기 때문에, 다른 용도로 이 클래스를 재사용하는 것이 어렵습니다. 예를 들어, 콘솔 UI가 아닌 웹 UI를 사용하는 다른 프로젝트에서 이 Book 클래스를 사용하려고 할 때, Book 클래스는 해당 프로젝트에 맞게 수정되어야 합니다.

Observer 패턴의 필요성

옵저버 패턴을 사용하면, 도서 상태 변경 시 UI 요소들이 자동으로 업데이트되도록 할 수 있어 이러한 문제를 해결할 수 있습니다.

// 주체 인터페이스
public interface ISubject
{
    void RegisterObserver(IObserver observer);
    void UnregisterObserver(IObserver observer);
    void NotifyObservers();
}
// 옵저버 인터페이스
public interface IObserver
{
    void Update(string availability);
}
// 구체적인 주체 클래스
public class Book : ISubject
{
    private readonly List<IObserver> _observers;
    private string _availability;
    public Book(string title)
    {
        _observers = new List<IObserver>();
        Title = title;
    }
    public string Title { get; private set; }
    public string Availability
    {
        get => _availability;
        set
        {
            _availability = value;
            NotifyObservers();
        }
    }
    public void RegisterObserver(IObserver observer)
    {
        _observers.Add(observer);
    }
    public void UnregisterObserver(IObserver observer)
    {
        _observers.Remove(observer);
    }
    public void NotifyObservers()
    {
        foreach (var observer in _observers)
        {
            observer.Update(Availability);
        }
    }
}
// 구체적인 옵저버 클래스
public class UserInterface : IObserver
{
    private readonly string _userName;
    public UserInterface(string userName)
    {
        _userName = userName;
    }
    public void Update(string availability)
    {
        Console.WriteLine($"{_userName}에게 알림: 도서 상태가 '{availability}'로 변경되었습니다.");
    }
}
  • Book 클래스: ISubject 인터페이스를 구현하여, 도서의 상태가 변경될 때 등록된 옵저버들에게 통보합니다.
  • UserInterface 클래스: IObserver 인터페이스를 구현하여, 도서의 상태 변화에 따라 사용자 인터페이스를 업데이트합니다.

Observer 패턴의 장단점

장점

결합도 감소

옵저버 패턴은 주체와 옵저버 간의 결합도를 낮추어, 주체가 구체적인 옵저버 클래스에 의존하지 않도록 합니다. 이를 통해 시스템의 유연성과 확장성이 향상됩니다.

실시간 업데이트

주체의 상태가 변경될 때마다 옵저버들에게 즉시 통보할 수 있어, 실시간 업데이트가 필요할 때 매우 유용합니다.

단점

복잡성 증가

여러 옵저버가 주체에 등록되고 통보를 받는 구조는 시스템의 복잡성을 증가시킬 수 있습니다.

메모리 누수 가능성

주체가 옵저버를 해지하지 않으면, 메모리 누수가 발생할 수 있습니다. 이를 방지하기 위해 옵저버를 적절히 관리해야 합니다.

개선 방안

적절한 사용 시점 선택

옵저버 패턴을 꼭 필요할 때만 사용하고, 단순한 요구사항에는 과도하게 적용하지 않도록 주의합니다. 예를 들어, 이벤트를 수신해야 하는 객체가 많지 않다면, 단순한 콜백이나 이벤트 핸들러를 사용하는 것이 더 간단할 수 있습니다.

중앙 집중식 관리

옵저버를 중앙에서 관리하는 매니저 클래스를 도입하여, 등록 및 해지 작업을 한 곳에서 수행하도록 설계하면 코드의 복잡성을 줄일 수 있습니다.

약한 참조Weak Reference 사용

.NET에서는 WeakReference 클래스를 사용하여 옵저버를 약한 참조로 보관할 수 있습니다. 이를 통해 옵저버 객체가 가비지 컬렉션에 의해 수거될 수 있도록 하여 메모리 누수를 방지할 수 있습니다.

적절한 옵저버 해지

옵저버가 더 이상 필요하지 않을 때는 주체로부터 옵저버를 명시적으로 해지하는 것이 중요합니다. 주체 클래스에서 UnregisterObserver 메서드를 잘 활용해 옵저버를 적시에 제거해야 합니다.

이벤트 기반 옵저버 해지

주체가 해지 조건을 모니터링하고, 특정 이벤트 발생 시 옵저버를 자동으로 해지하는 메커니즘을 도입할 수 있습니다.

맺음말

옵저버 패턴은 객체 간의 일대다 관계를 효과적으로 관리하고, 상태 변화를 실시간으로 반영해야 하는 시스템에서 매우 유용한 설계 패턴입니다. 도서관 관리 시스템과 같은 애플리케이션에서 상태 변화에 따른 UI 업데이트와 같은 기능을 구현할 때, 이 패턴을 사용하면 코드의 유지보수성과 확장성을 높일 수 있습니다. 다만, 복잡성이 증가할 수 있으므로 필요한 상황에서 신중하게 적용하는 것이 중요합니다.