728x90
반응형

이 글은 Head First에서 나온 Design Patterns 책을 보고 이해한 내용을 나름대로 정리한 내용입니다.

옵저버 패턴(Observer Pattern)은 출판사 와 구독자와 같은 관계를 갖는 패턴이다. 구독자들이 출판사에 구독을 신청하면 출판사는 신문이 출간될 때마다 구독자들에게 신문을 저달한다. 구독자가 구독을 해지하면 더이상 신문을 받지 않는다. 이렇게 옵저버 패턴은 일대다 의존성을 정의하는 패턴이다.

옵저버 패턴에서 일반적으로 출판사 열할을 하는 객체를 Subject 라고 부르고 구독자 객체를 Observer라고 부른다. 모든 Subject는 자신에게 "구독 추가", "구독 해지" 메소드가 있다는 것을 구독자들에게 알려줘야하고, 반드시 알림(신문을 전달하는 행동) 메소드가 필요하다. 이를 추상화 하여 인터페이스로 구현해보자.

public interface ISubject
{
    void AddObserver(IObserver observer);  // 구독자 추가
    void RemoveObserver(IObserver observer); // 구독자 삭제
    void NotifyObserver(); // 구독자들에게 알림
}

이번엔 구독자인 Observer를 보자. 모든 ObserverSubject에게 "나에게는 Update()라는 메소드가 있으니 필요하면 이 메소드를 통해 전달해주세요" 라고 알려줘야한다. 이를 추상화 하여 인터페이스로 만든다.

public interface IObserver
{
    void Update(Object obj); // Subject가 갱신된 내용을 전달하는 메소드
}

* 이 예제에서는 범용적으로 사용하기 위해 Update의 인자로 Object를 넣었는데 구체적인 객체로 받아도 상관 없다.

이제 구체적인 상황을 설정해보자. 나는 WebParser라는 클래스를 Subject로 만들어 3초마다 웹에서 파싱한 내용을 구독자들에게 저달하려고 한다. 시뮬레이션이기 실제 웹파싱을 하진 않고 3초마다 랜덤 String을 만들어 웹에서 긁어온 키워드라고 가정을했다.

public class WebParser : ISubject 
{
    private List<IObserver> _observers = new List<IObserver>();

    public string WebContents { get; set; }

    // 생성자에서 시뮬레이터를 동작시킴
    public WebParser()
    {
        Task.Run(() => SimulateWebParser());
    }

    // 웹 파싱 시뮬레이터 메소드
    private void SimulateWebParser()
    {            
        while(true)
        {
            Thread.Sleep(3000);
            WebContents = RandomStringGenerator();
            NotifyObserver();
        }
    }

    // 랜덤으로 String을 생성해주는 메소드
    private string RandomStringGenerator()
    {
        Random random = new Random();
        const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        return new string(Enumerable.Repeat(chars, 10)
            .Select(s => s[random.Next(s.Length)]).ToArray());
    }

    // Observer가 구독을 신청하는 메소드
    public void AddObserver(IObserver subscriber)
    {
        _observers.Add(subscriber);
    }

    // Observer가 구독을 해지하는 메소드
    public void RemoveObserver(IObserver subscriber)
    {
        if (_observers.Contains(subscriber))
            _observers.Remove(subscriber);
    }

    // 구독중인 Observer들에게 변경을 알려주는 메소드
    public void NotifyObserver()
    {
        foreach(var observer in _observers)
        {
            // Observer의 Update 메소드이용해 갱신해줌.
            observer.Update(this);
        }
    }
}

랜덤 String을 만드는 작업 때문에 코드가 조금 길어졌는데 중요한 부분은 ISubject 인터페이스의 AddObserver(), RemoveObserver(), NotifyObserver()를 어떻게 구현했는지만 보면 된다.

이제 마지막으로 간단한 Observer들을 만들어보자. 나는 WebParser로부터 전달받은 내용을 디스플레이해주는 객체와 이메일로 전달 해주는 두 가지 Observer를 만들었다.

public class DisplayWebContents : IObserver
{
    public DisplayWebContents(WebParser webParser)
    {
        webParser.AddObserver(this);
    }
    public void Update(object obj)
    {
        if (obj is WebParser)
        {
            var webParser = obj as WebParser;
            Console.WriteLine($"Display {webParser.WebContents}"); 
        }
    }
}

public class EmailWebContents : IObserver
{
    public EmailWebContents(WebParser webParser)
    {
        webParser.AddObserver(this);
    }
    public void Update(object obj)
    {
        if (obj is WebParser)
        {
            var webParser = obj as WebParser;
            Console.WriteLine($"Email {webParser.WebContents}");
        }
    }
}

Observer 구상클래스를 보면 알겠지만 구독을 신청하기 위해 Subject의 구상클래스를 반드시 포함하고 있다. 클래스 다이어그램을 보면서 다시 한 번 확인해보자.

이제 마지막으로 Main 함수와 실행 결과를 확인해보자.

class Program
{
    static void Main(string[] args)
    {
        WebParser wp = new WebParser();

        DisplayWebContents display = new DisplayWebContents(wp);
        EmailWebContents email = new EmailWebContents(wp);

        Thread.Sleep(100000);
    }
}

실행결과:

3초에 한번식 랜덤 String이 생성되고 디스플레이와 이메일이 업데이트됨.

이렇게 옵저버 패턴에서는 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다(one-to-many) 의존성을 정의한다.

참고 자료: Head First - Design Pattern

728x90
반응형

+ Recent posts