본문 바로가기
반응형
Programming/Design Pattern

디자인패턴 - 옵저버 패턴(Observer Pattern)

by JAMINS 2016. 1. 25.

정의

디자인 패턴 중 옵저버 패턴(Observer Pattern)을 알아보자. 객체지향 설계를 하다보면 객체들 사이에서 다양한 처리를 할 경우가 많다. 예를 들어 한 객체의 상태가 바뀔 경우 다른 객체들에게 변경됐다고 알려주는 경우를 들 수 있다. 상태를 가지고 있는 주체 객체와 상태의 변경을 알아야 하는 관찰 객체(Observer Object)가 존재하며 이들의 관계는 1:1이 될 수도 있고 1:N이 될 수가 있다. 서로의 정보를 넘기고 받는 과정에서 정보의 단위가 클 수록, 객체들의 규모다 클 수록, 각 객체들의 관계가 복잡할 수록 점점 구현하기 어려워지고 복잡성이 매우 증가할 것이다. 이러한 기능을 할 수 있도록 가이드라인을 제시해 주는 것이 바로 옵저버 패턴이다.

이 패턴은 과연 개발을 하면서 밀접하게 사용되는가 궁금해졌다. Head First Design Pattern 서적을 참고하면서 나름대로 정리해보겠다. 도대체 어디에 쓰이는 패턴이며 어떻게 구현하는가? 일단, 위에서 언급한 주체 객체, 관찰 객체라는 의미부터 파악해보자. 이러한 관계들을 우리는 일상생활에서 흔히 발견할 수 있다. 잡지를 발행하는 잡지사와 그 잡지를 매달 구독하는 독자들과의 관계, 이와 비슷하게 우유를 제공하는 우유배달업체와 고객들, 신문사와 정기구독자들. 어떤 인터넷 까페에 속한 회원과 매달 공지사항을 날려주는 운영진들과의 관계......무수히 많은 케이스가 있다. 어떤 정보를 알고자 혹은 얻고자 하는 고객들은 Publishing 해주는 존재와의 관계를 맺고(계약 등) 꾸준히 정보를 얻어간다. 이렇게 지속되다가 더 이상 정보를 원하지 않을 경우 관계를 끊을 수 있다(계약 해지). 객체와의 관계를 맺고 끊으며 객체의 상태가 변경되면 그 정보를 Observer(Subscribe)에게 알려주는 방법을 알아보자.

예시

위의 수많은 예들 중, 뉴스를 예로 들어보자. 매일매일 새로운 뉴스를 제공하는 뉴스머신(?)이 있다고 가정하자. 이 머신은 새로운 뉴스를 취합하여 정보를 가지고 있다. 그리고 이 뉴스머신에서 나온 뉴스를 구독하고 싶어하는 Observer를 정의해보자. 이들은 뉴스머신이 새로운 뉴스를 생성할 때마다 이 소식을 바로 받을 수 있다. 그렇다면 일단 NewsMachine 이라는 객체와 Observer라는 객체가 필요하겠지?? 이들을 클래스로 만들면 되는것인가?

객체지향설계를 하기전에 저번에 배웠던 스트레티지 패턴을 다시 상기시켜보자. (스트레티지 패턴 : http://flowarc.tistory.com/entry/1-Strategy-Pattern) 인터페이스를 이용하여 객체간의 느슨한 결합(Loose Coupling)을 권장했다. 즉 상속을 통한 구현이 아닌 구성(Composition)을 이용한 것이다. 구성이란 A에 B가 있다 라는 것을 의미하며 한 객체에 객체를 포함시키는 것이 아닌 인터페이스를 포함하는 방식으로 많이 사용된다.

우리가 사용하려는 옵저버 패턴에서도 이러한 방식처럼 인터페이스를 활용한다. 여기서는 크게 두 가지의 역할을 한다. 하나는 Publisher의 역할, 또 하나는 Observer의 역할이다. 일단 이들의 인터페이스를 정의한 후 이를 implement한 클래스들을 이용한다. Publisher라는 인터페이스는 Observer들을 관리하는 Method를 가지고 있을 것이다. 구독을 원하는 Observer를 받아 등록시키고(add), 명단에서 제외하고(delete), 등록된 Observer들에게 정보를 알려주는(notifyObserver) 이렇게 3가지의 Method를 정의할 수 있다. Observer 인터페이스는 정보를 업데이트 해주는 Update method를 가지고 있을 것이다. 이를 토대로 클래스 다이어그램을 그린다면 아래와 같이 나온다.

Publisher를 구현한 NewsMachine 클래스는 정보를 제공하는 Publisher가 되며 Observer객체들을 가지고 있다. 그리고 Observer 인터페이스를 구현한 AnnualSubscriber(매년 정기구독자), EventSubscriber(이벤트 고객) 클래스는 NewsMachine이 새로운 뉴스를 notifyObserver() 를 호출하면서 알려줄 때마다 Update가 호출된다. 이를 토대로 JAVA로 코딩해보자.

구현

1. Observer 인터페이스 정의

public interface Observer {
    public void update(String title, String news);
}

2. Publisher 인터페이스 정의

public interface Publisher {
    public void add(Observer observer);
    public void delete(Observer observer);
    public void notifyObserver();
}

3. NewsMachine 클래스

public class NewsMachine implements Publisher {
    private ArrayList<Observer> observers;
    private String title;
    private String news;

    public NewsMachine() {
        observers = new ArrayList<>();
    }

    @Override
    public void add(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void delete(Observer observer) {
        int index = observers.indexOf(observer);
        observers.remove(index);
    }

    @Override
    public void notifyObserver() {
        for(Observer observer : observers) {
            observer.update(title, news);
        }
    }

    public void setNewsInfo(String title, String news) {
        this.title = title;
        this.news = news;
        notifyObserver();
    }

    public String getTitle() {
        return title;
    }

    public String getNews() {
        return news;
    }
}

4. AnnualSubscriber 클래스

public class AnnualSubscriber implements Observer {
    private String newsString;
    private Publisher publisher;

    public AnnualSubscriber(Publisher publisher) {
        this.publisher = publisher;
        publisher.add(this);
    }

    @Override
    public void update(String title, String news) {
        this.newsString = title + " \n -------- \n " + news;
        display();
    }

    private void display() {
        System.out.println("\n\n오늘의 뉴스\n============================\n\n" + newsString);
    }
}

5. EventSubscriber 클래스

public class EventSubscriber implements Observer {
    private String newsString;
    private Publisher publisher;

    public EventSubscriber(Publisher publisher) {
        this.publisher = publisher;
        publisher.add(this);
    }

    @Override
    public void update(String title, String news) {
        newsString = title + "\n------------------------------------\n" + news;
        display();
    }

    public void display() {
        System.out.println("\n\n=== 이벤트 유저 ===");
        System.out.println("\n\n" + newsString);
    }
}

위의 클래스 다이어그램을 구현했다. 이제 Main에서 한번 활용해보자.

6. main함수

public class MainClass {
    public static void main(String[] args) {
        NewsMachine newsMachine = new NewsMachine();
        AnnualSubscriber as = new AnnualSubscriber(newsMachine);
        EventSubscriber es = new EventSubscriber(newsMachine);

        newsMachine.setNewsInfo("오늘 한파", "전국 영하 18도 입니다.");
        newsMachine.setNewsInfo("벛꽃 축제합니다", "다같이 벚꽃보러~");
    }
}

뉴스머신을 선언하고 한파와 벚꽃 축제라는 2가지의 뉴스가 업데이트 했다. 과연 어떻게 출력 될 것이가?

탈퇴 기능 추가

위와 같이 2가지의 뉴스가 각각 AnnualSubscribe와 EventSubscribe 객체에 잘 전달이 됐다. 이 두 객체가 생성되는 순간 Publisher가 가지고 있는 Observer 목록에 등록이 된 것이다. 자, 여기서 AnnualSubscribe와 EventSubscribe 객체에서 꼭 Publisher 객체를 멤버변수로 선언할 필요가 있을까 궁금증이 생긴다. 생성자에서 add 역할만 하는데 왜 필요할까? 바로 Observer 목록에서 탈퇴할 때 이 Publisher 레퍼런스를 이용하면 편하기 때문이다. 위의 소스에서 EventSubscribe 객체인 es 인스턴스를 탈퇴시킨다면,

public class MainClass {
    public static void main(String[] args) {
        NewsMachine newsMachine = new NewsMachine();
        AnnualSubscriber as = new AnnualSubscriber(newsMachine);
        EventSubscriber es = new EventSubscriber(newsMachine);

        newsMachine.setNewsInfo("오늘 한파", "전국 영하 18도 입니다.");

        // es인스턴스 Observer 목록에서 탈퇴
        newsMachine.delete(es);
        newsMachine.setNewsInfo("벛꽃 축제합니다", "다같이 벚꽃보러~");
    }
}

위와 같이 newsMachine의 delete 를 이용하여 탈퇴해야 한다. 아니 내가 탈퇴하겠다는데!!!!! newsMachine을 이용하는 것은 뭔가 기분이 영 나쁘다. 내 스스로 탈퇴하도록 만들어보자.

EventSubscriber 클래스

public class EventSubscriber implements Observer {
    private String newsString;
    private Publisher publisher;

    public EventSubscriber(Publisher publisher) {
        this.publisher = publisher;
        publisher.add(this);
    }

    @Override
    public void update(String title, String news) {
        newsString = title + "\n------------------------------------\n" + news;
        display();
    }

    // publisher를 이용하여 탈퇴
    public void withdraw() {
        publisher.delete(this);
    }

    public void display() {
        System.out.println("\n\n=== 이벤트 유저 ===");
        System.out.println("\n\n" + newsString);
    }
}

이렇게 withdraw 메서드를 호출하면 바로 탈퇴할 수 있다.

main 함수

public class MainClass {
    public static void main(String[] args) {
        NewsMachine newsMachine = new NewsMachine();
        AnnualSubscriber as = new AnnualSubscriber(newsMachine);
        EventSubscriber es = new EventSubscriber(newsMachine);

        newsMachine.setNewsInfo("오늘 한파", "전국 영하 18도 입니다.");

        // es인스턴스 Observer 목록에서 탈퇴
        es.withdraw();
//        newsMachine.delete(es);

        newsMachine.setNewsInfo("벛꽃 축제합니다", "다같이 벚꽃보러~");
    }
}

어디서 활용될까

위와같이 1번째 소식 이후 탈퇴한 결과 2번째 뉴스부터는 EventSubscriber 객체는 제외되었다. Publisher에서 정보나 상태가 변경될 때마다 Observer에게 보내는 방식을 우리가 흔히 알고 있는 푸시(Push)라고 부른다. 반면에 Observer에서 정보가 필요할 때마다 Publisher에게 요청하는 방식을 풀(Pull)이라고 한다. 지금까지 예를 들어가며 살펴본건 푸시 방식이었다. 물론 옵저버 패턴으로 풀 방식을 구현할 수 있다. 이는 추후에 다음 포스팅에서 살펴보자. JAVA에서는 이 옵저버 패턴을 적용한 것들을 기본적으로 제공해준다.

바로 Observable 클래스와 Observer 인터페이스다. 이를 이용하면 직접 인터페이스를 구현하지 않아도 쉽게 옵저버 패턴을 적용할 수 있다. 하지만 장점이 있다면 단점도 있듯이 여기에는 하나의 문제점이 있다. 바로 Observable이 인터페이스가 아닌 클래스로 구현되어있다는 점이다. 결국 상속을 받을 수 밖에 없는데 다른 클래스에서 반드시 상속을 받아야하는 경우에는 사용할 수 없다.(JAVA는 다중상속을 지원하지 않기 때문) 때문에 적절히 상황을 봐서 사용해야 할 것이다. 이 옵저버 패턴이 쓰이는 곳이 또 어디일까? JAVA의 Swing이나 Android의 View나 Button 등의 위젯의 각종 이벤트를 받을 때 쓰인다. Android 에서 흔히 사용되는 버튼을 살펴보자.

버튼은 항상 클릭이라는 이벤트가 있으며 이 이벤트는 OnClickListener 라는 인터페이스로 구성되어있다. 즉 버튼이라는 객체가 Publisher가 되고 OnClickListener가 Observer가 된다고 볼 수 있다. 버튼에서 상태가 변경(클릭 될 경우)된다면 OnClickListener로 알려준다.

Button button = (Button) findViewById(xx);
button.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(...){
    // Action
  }
});

위와 같이 버튼에 OnClickListener라는 Observer를 등록하는 경우가 대표적인 옵저버 패턴을 적용한 것이다. 이를 응용하여 커스텀 리스너를 선언할 수도 있다. 이제 옵저버 패턴을 배웠으니 Android에서 한번 사용해 보자. Drawer에서 버튼을 클릭할 경우 Activity로 어떠한 동작을 해야할 경우 커스텀 리스너를 인터페이스로 정의하고 활용하면 될 것이다.

정리

  • 옵저버 패턴은 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들에게 연락이 가고 자동으로 정보가 갱신되는 1:N 의 관계를 정의한다.
  • 연결은 인터페이스를 이용하여 느슨한 결합성을 유지한다. 주체, 옵저버 인터페이스를 적용한다.
  • 옵저버 패턴은 푸시 방식과 풀 방식으로 언제든지 구현할 수 있다.
  • JAVA에서 기본으로 Observable 클래스와 Observer 인터페이스를 제공한다.
  • Swing, Android 등에서 UI관련된 곳에서 이 옵저버 패턴이 많이 사용된다. (물론 이보다 더 많다)

댓글