ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 옵저버 패턴
    디자인 패턴 2022. 9. 7. 00:35

    객체 하나의 상태가 변할 때마다 등록된 객체들에게 자신의 상태값을 전달하는 패턴입니다.

     

    예시로, 상품을 구매하였을 때 알림을 전달해주는 프로그램을 작성해봅시다.

     

    해당 내용을 설명하자면, 

    상품 구매 시스템에서 구매가 일어나면 구매정보 객체로 해당 내용들을 전달하고

    홈페이지, 관리자앱, 회원앱으로 해당 정보들을 혹은 메시지를 전달하게 됩니다.

     

    코드를 작성해 보겠습니다.

    1. 초기 코드 작성

    public class Homepage, ManagerApp, MemberApp {
    
        public static void notify(String productName, int count, String buyer, String buyDt){
            System.out.println("Homepage Logg ======> \n productName : " + productName + "\n count : " +count + "\n buyer : " + buyer + "\n buyDt : " + buyDt);
        }
    }

     

    public class Order {
        private String productName;
        private int count;
        private String buyer;
        private String buyDt;
    
        public void orderGenerated(String productName, int count, String buyer, String buyDt) {
            this.productName = productName;
            this.count = count;
            this.buyer = buyer;
            this.buyDt = buyDt;
    
            Homepage.notify(productName, count, buyer, buyDt);
            ManagerApp.notify(productName, count, buyer, buyDt);
            MemberApp.notify(productName, count, buyer, buyDt);
        }
    }

     

    public class Main {
        public static void main(String[] args) {
            Order order = new Order();
    
            /* 시스템 입력값 */
            String productName = "새우깡";
            int count = 5;
            String buyer = "taehyun";
            String buyDt = "2022-09-01";
    
            order.orderGenerated(productName, count, buyer, buyDt);
        }
    }
    

     

    출력 로그

     

    2. 애플리케이션 추가하기

        public void orderGenerated(String productName, int count, String buyer, String buyDt) {
            this.productName = productName;
            this.count = count;
            this.buyer = buyer;
            this.buyDt = buyDt;
    
            Homepage.notify(productName, count, buyer, buyDt);
            ManagerApp.notify(productName, count, buyer, buyDt);
            MemberApp.notify(productName, count, buyer, buyDt);
            ExtraApp.notify(productName, count, buyer, buyDt);
        }

    위와 같이 코드를 작성하면 불편한점이 하나 발생합니다.

     

    만약, 애플리케이션이 하나라도 추가되면 메인 코드를 수정해야 합니다.

    -> 하나의 애플리케이션에게 알림을 전달하기 위해  모든 애플리케이션이 마비되는 구조 발생합니다.

    제거를 하더라도 마찬가지였습니다.

     

     

     

    3. 옵저버 패턴의 이해

    유튜브 구독과 같은 느낌으로 생각해봅시다.

     

    위와 같이 알림 미신청자는 데이터 전송을 하지 않고, 구독자 영역에만 데이터를 전송하게 처리하게 하는 것이

    옵저버 패턴의 원리입니다. 

     

    구독자는 자유롭게 구독과 해지를 할 수 있으며, 주체(구매정보 객체)는 그것을 신경 쓰지않고,

    구독자목록만 신경쓰면 됩니다.

     

     

    4. 옵저버 패턴 적용하기

    다형성을 이용하기위해 인터페이스를 활용하여 작성합니다.

    Subject 인터페이스 (구매 정보 객체용)

    public interface OrderSubject {
    
        public void registerApp(String key, App app);
        public void removeApp(String key);
        public void notifiApp();
    }

     

    Observer 인터페이스(각 애플리케이션용)

    public interface App {
        public void getData(String productName, int count, String buyer, String buyDt);
    }
    
    public interface Display {
        public void alert();
    }
    

     

    Subject 구현체

    public class Order implements OrderSubject {
    
        private List<App> appList = new ArrayList<>();
        private String productName;
        private int count;
        private String buyer;
        private String buyDt;
    
        public void orderGenerated(String productName, int count, String buyer, String buyDt) {
            this.productName = productName;
            this.count = count;
            this.buyer = buyer;
            this.buyDt = buyDt;
            notifiApp();
        }
    
        @Override
        public void registerApp(App app) {
            appList.add(app);
        }
    
        @Override
        public void removeApp(App app) {
            appList.remove(app);
        }
    
        @Override
        public void notifiApp() {
            appList.forEach(item -> item.getData(productName, count, buyer, buyDt));
        }
    }

     

    Observer 구현체 (1개만)

    public class Homepage implements App, Display {
        private String productName;
        private int count;
        private Order order;
    
        public Homepage(Order order) {
            this.order = order;
            order.registerApp(this);
        }
    
        @Override
        public void getData(String productName, int count, String buyer, String buyDt) {
            this.productName = productName;
            this.count = count;
            alert();
        }
    
        @Override
        public void alert() {
            System.out.println("Homepage Logg ======> \n productName : " + productName + "\n count : " + count);
        }
    }

     

     

    실행코드

    public static void main(String[] args) {
        Order order = new Order();
    
        /* 시스템 입력값 */
        String productName = "새우깡";
        int count = 5;
        String buyer = "taehyun";
        String buyDt = "2022-09-01";
    
        Homepage homepage = new Homepage(order);
        ManagerApp managerApp = new ManagerApp(order);
        MemberApp memberApp = new MemberApp();
        order.orderGenerated(productName, count, buyer, buyDt);
    }

     

     

    위를 확인해보면 주체인 Order가 직접 애플리케이션에 알림을 전달하지 않고

    appList에 등록된 구독을 신청한 Homepage와 ManagerApp만 출력되는 것을 확인 할 수 있습니다.

     

    5. 풀 방식 사용해보기

    Order의 notify() 메서드 수정

    @Override
    public void notifiApp() {
        appList.forEach(item -> item.getData());
    }

     

    Observer인 App의 인터페이스, 구현체 getData()수정

    public interface App {
        public void getData();
    }
    
    @Override
    public void getData() {
        this.productName = order.getProductName();
        this.count = order.getCount();
        alert();
    }

    이 방법을 사용하면 order에 사용되는 정보 (상품명, 갯수 등) 외에 추가정보(가격, 할인)이 늘어나게 될 경우 

    해당 메서드를 변경해줄 필요없이 하위에서 직접 필요한 정보만 뽑아 쓸 수 있습니다.

     

    6. 사용예시 살펴보기

     

    안드로이드나, JAVA 프론트에서 처리할 때 evenListener로 잘 활용됩니다.

    예시로 보자면,

    JButton button = new JButton();
    
    button.addActionListener(event -> System.out.println("액션 1번"));
    button.addActionListener(event -> System.out.println("액션 2번"));
    button.addActionListener(event -> System.out.println("액션 3번"));

    버튼 1번만 눌렀어도 행위가 3번 행해질수 있도록 ActionListner 구현체를 3개 등록한 예시입니다.

     

    버튼과 액션리스너를 뜰어보면 다음과 같이 위에서 작성한 옵저버 패턴과 동일함을 알 수 있습니다.

    /**
     * Adds an <code>ActionListener</code> to the button.
     * @param l the <code>ActionListener</code> to be added
     */
    public void addActionListener(ActionListener l) {
        listenerList.add(ActionListener.class, l);
    }
    

     

    public interface ActionListener extends EventListener {
    
        /**
         * Invoked when an action occurs.
         */
        public void actionPerformed(ActionEvent e);
    
    }
    protected void fireActionPerformed(ActionEvent event) {
        // Guaranteed to return a non-null array
        Object[] listeners = listenerList.getListenerList();
        ActionEvent e = null;
        // Process the listeners last to first, notifying
        // those that are interested in this event
        for (int i = listeners.length-2; i>=0; i-=2) {
            if (listeners[i]==ActionListener.class) {
                // Lazily create the event:
                if (e == null) {
                      String actionCommand = event.getActionCommand();
                      if(actionCommand == null) {
                         actionCommand = getActionCommand();
                      }
                      e = new ActionEvent(AbstractButton.this,
                                          ActionEvent.ACTION_PERFORMED,
                                          actionCommand,
                                          event.getWhen(),
                                          event.getModifiers());
                }
                ((ActionListener)listeners[i+1]).actionPerformed(e);
            }
        }
    }

     

    7.  VS Pub Sub

    옵저버 패턴 VS 출판 / 구독 패턴

    매우 유사한 느낌이 있지만,

    Observer패턴은 Subject가 옵저버들에게 직접 메시지를 전달하고 대부분 동기적으로 작동합니다.

     

    Publisher는 Subscriber에게 직접 메시지를 보내도록 하지 않고 대부분 비동기적으로 구현됩니다.(메시지큐 사용)

    대신, 브로커, 메시지 브로커 또는 이벤트 버스라는 구성요소가 존재하여 들어오는 모든 메시지를 필터링 하고

    그에 따라 배포합니다.

     

    즉, 게시자(Publisher)와 구독자(Subscriber)는 서로 알지 못한 상태로 메시지를 전달 받습니다.

    8. 정리하기

    옵저버 패턴은 흔히 우리가 보는 유튜브 구독, 신문사 구독과 같은 서비스와 비슷하다고 생각하면 됩니다.

    데이터 변경이 일어났을 때 , 상대 데이터 클래스나 객체의 의존하지 않으면서 데이터를 전송할 때 유용합니다.

    제대로된 정의는

    한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고
    자동으로 내용이 갱신되는
    방식으로 일대다 의존성을 정의합니다
    .

    라고 합니다.

     

     

    9. 실무 생각해보기

    스프링 애플리케이션은 대부분의 데이터들을 DB에서  관리하기 때문에, 자바내 컬렉션 구조로 인터페이스들을

    저장하는 건 사실 무리가 있는 것 같습니다. (EX: DB의 목록이나 설정여부를 읽어와서 처리) 

    실시간 설정에 따라 (사용량 과다로 인한 알림취소라던지) 옵저버에서 제하는 느낌으로 사용할 순 있을 것 같습니다.

    ( 해당 서버를 죽이게되면 예외가 발생할 수 있으므로 )

     

    DB데이터로는 GitHub의 WebHook 같이 외부에서 자사 서비스를 이용할 때 훅 URL을 등록한다던가 ,

    알림서비스를 할 때 등록된 이용자만 사용 할 수 있게 한다던가의 방향성은 있을 것 같습니다.

     

     

    '디자인 패턴' 카테고리의 다른 글

    전략 패턴  (0) 2022.08.23
Designed by Tistory.