옵저버 패턴

2018, Dec 10    

옵져버 패턴은 신문이나 잡지처럼 데이터(목적)을 데이터가 필요한 쪽(구독자)에게 발행해주는 형식의 디자인을 말한다.

이름은 이와 맞지 않게 Observer(관찰자) 라고 붙여 있는데, 데이터를 필요로 하는 쪽 즉, 구독자가 데이터가 변화할때나 혹은 주기적으로 데이터의 상태를 전달 받음으로서 ‘관찰’하는 형식이 되기 때문이다.

따라서 이 패턴의 출발 과제는 데이터를 조회 하는 client객체와 데이터를 제공하는 객체간의 관계를 어떻게 디자인해야 하는지에 대한 고민이었다는 것을 알 수 있다.

버스 정류장의 곧 도착하는 버스알림 전광판을 예를 들어 보자.

각 버스 정류장에서는 곧 도착할 버스 리스트가 변경 될 때 마다 전광판에 표시를 해줘야 한다. 전국의 버스 위치의 정보를 관리하는’ 버스 위치 시스템’이 있는데, 각 버스 정류장에서는 곧 도착할 버스 리스트를 이 ‘버스 위치 시스템’를 통해서 곧 도착할 버스들의 리스트 정보를 얻어 올 수 있다.

버스 리스트가 언제 갱신 될지 모르므로 각 정류장에서 주기적으로 데이터를 조회한다고 하면, 데이터가 변경되지 않은 상황에서도 불필요하게 데이터를 조회하는 비용이 발생하게 되어 매우 비효율 적일 것이다.

거꾸로 ‘버스 위치 시스템’에서 버스 위치 정보가 변화할때마다 각 정류장으로 데이터를 전송해 준다면, 불필요한 비용을 막을 수 있을 것이다.

요즘 관심이 높아지고 있는 비동기 프로그래밍이나 spring-cloud진영의 유레카 같은 기능도 옵져버 패턴을 기반으로 하고 있다.

아까 옵져버 패턴은 신문이나 잡지와 같은 ‘구독’ 의 형식을 취하고 있다고 했다.

‘신문 구독’의 특징을 생각해 보자.

  • 신문사가 사업을 시작하고 신문을 찍어낸다. (publish)
  • 독자(describer)가 특정 신문사에 구독 신청을 하면 매번 새로운 신문을 배달 받는다.
  • 신문을 더이상 받아보고 싶지 않으면 구독 해지 신청을 한다.
  • 신문사가 계속 영업을 하는 이상 꾸준히 구독 및 해지를 하게 된다.

이와 같은 특성을 객체간의 관계에 적용이 되면 아래와 같다.

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

옵져버패턴을 적용한 객체관계 :

[옵져버 패턴 - 클래스 다이어그램]
클래스다이어그램

디자인 원칙

  • 헐리우드 원칙 : 서로 상호작용을 하는 개체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.

옵져버 패턴을 이용하면 정보를 제공하는 객체(subject)와 정보를 조회하는 객체(observer)간의 결합을 느슨하게 해준다.

  • subject는 옵저버에 대해서 특정 인터페이스(Observer인터페이스)를 구현한다는 것만 알뿐 개별 옵저버 객체에 대해서는 알 필요가 없다.
  • 옵저버는 언제든지 새로 추가 할 수 있다. (삭제되고 추가 되어도 subject의 코드가 변화할 될 필요가 없다)
  • 새로운 형식의 옵저버를 추가하려고 할 때도 subject를 전혀 변경할 필요가 없다.
  • subject와 옵저버가 바뀌더라도 서로에게 영향을 미치지 않으며 서로 독립적으로 재사용할 수 있다.

코드로 만들어 보자

설정 : 기상청 미세먼지 측정 시스템에서는 미세먼지 농도 지수가 위험수치 이상이 되면 앱푸시, SMS 전송으로 경고 메세지를 보낸다. 전체 소스 참고

  1. subject(발행자) interface와 observer(구독자) interface를 만듬
     public interface Publisher {
       void registerDescriber();
       void removeDescriber();
       void notifyDescribers();
     }
    
     public interface Describer {
       void alert();  //미세먼지 농도가 위험수준이라고 알리는 함수
     }
    
  2. publisher에 구독자를 등록 및 해지하려면 해당 객체를 넘겨받아야 함으로 등록, 해지 함수에 파라미터 추가
      public interface Publisher {
     void registerDescriber(Describer db);
     void removeDescriber(Describer db);
     void notifyDescribers();
      }
    
  3. pulisher와 Describer를 구현하는 각 객체를 만듬
     //미세먼지를 위험성을 알리는 객체
     public class FineDustData implements Publisher{
    
       @Override
       public void registerDescriber(Describer db) {
    
       }
    
       @Override
       public void removeDescriber(Describer db) {
    
       }
    
       @Override
       public void notifyDescribers() {
    
       }
     }
    
     //앱푸시를 보내는 객체
     public class AppPush implements Describer {
    
       @Override
       public void alert(int level) {
    
       }
     }
    
     //sms전송을 위한 객체
     public class SMS implements Describer {
    
       @Override
       public void alert(int level) {
    
       }
     }
    
  4. FineDustData에 구독객체 리스트와 현재 미세먼지 농도수치, 위험수준의 농도수치를 저장할 변수를 선언 하였다. 그리고 객체를 생성할때 각 변수의 값이 초기화 되도록 처리.
     public class FineDustData implements Publisher{
    
       List<Describer> describers;
    
       int currentLevel;
       int dangersLevel;
    
       public FineDustData(int dangersLevel) {
         this.dangersLevel = dangersLevel;
         this.describers = new ArrayList<>();
       }
          
       ..(생략)..
     }
    
  5. 구독자를 등록하고 해지하는 함수 구현
     public class FineDustData implements Publisher {
    
       ..(생략)..
    
       @Override
       public void registerDescriber(Describer dc) {
         describers.add(dc);
       }
    
       @Override
       public void removeDescriber(Describer dc) {
         describers.remove(dc);
       }
     }
    
  6. 미세먼지가 위험수치에 다다를때마다 구독자에게 알리는 함수 구현
     public class FineDustData implements Publisher {
    
       ...(생략)..
    
       @Override
       public void notifyDescribers() {    
         //구독자 리스트를 돌면서 알림 메소드를 호출한다.
         for (Describer dc : describers) {
           dc.alert();
         }
       }
    
       //현재 미세먼지 수치를 변경하는 함수 
       public void changeDustLevel(int level) {
         this.currentLevel = level;
    
         //위험수치에 다다르면 구독자에체 알리는 함수를 호출한다.
         if(currentLevel >= dangersLevel) {
           notifyDescribers();
         }
       }
     }
    
  7. 각 구독객체인 AppPush와 SMS는 미세먼지 농도가 위험 수치라는 것을 알린다!
     public class AppPush implements Describer {
    
       @Override
       public void alert() {
         System.out.println("앱푸시! - 미세먼지가 위험수준에 도달하였습니다.");
       }
     }
    
     public class SMS implements Describer {
    
       @Override
       public void alert() {
         System.out.println("SMS발송! - 미세먼지가 위험수준에 도달하였습니다.\n 외출하실때 마스크 착용하세요!!");
       }
     }
    
  8. 각 Describer이 Publisher에게 구독을 요청을 하기 위해 초기화 함수에서 publisher를 넘겨 받을 수 있도록 함. 추가로 구독 해지 함수를 Describer insertface에 추가하고 각각의 구현 객체에 추가해줌.
    public interface Describer {
       void alert();
       void stopDescribe();
     }
    
     public class AppPush implements Describer {
    
       Publisher pb;
    
       public AppPush(Publisher pb) {
         pb.registerDescriber(this);
       }
    
       @Override
       public void alert() {
         System.out.println("앱푸시! - 미세먼지가 위험수준에 도달하였습니다.");
       }
    
       @Override
       public void stopDescribe(){
         pb.removeDescriber(this);
       }
     }
    
     public class SMS implements Describer {
    
       Publisher pb;
    
       public SMS(Publisher pb) {
         this.pb = pb;
         pb.registerDescriber(this);
       }
    
       @Override
       public void alert() {
         System.out.println("SMS발송! - 미세먼지가 위험수준에 도달하였습니다.\n 외출하실때 마스크 착용하세요!!");
       }
    
       @Override
       public void stopDescribe() {
         pb.removeDescriber(this);
       }
     }
    
  9. 테스트
    public class TestFineDustObserver {
    
      @Test
      public void test() {
    
        //publisher정의
        FineDustData dustData = new FineDustData(10);
    
        //앱푸시 구독자
        AppPush appPush = new AppPush(dustData);
        //SMS 구독자
        SMS sms = new SMS(dustData);
    
    
        //미세먼지 농도가 올라간다.
        dustData.changeDustLevel(5);
        //..점점 올라가더니 위험수치에 다다랐다.
        dustData.changeDustLevel(10);
      }
    }
    

처리결과

    앱푸시! - 미세먼지가 위험수준에 도달하였습니다.
    SMS발송! - 미세먼지가 위험수준에 도달하였습니다.
    외출하실때 마스크 착용하세요!!