[디자인패턴] 옵저버패턴

이나겸's avatar
Nov 14, 2024
[디자인패턴] 옵저버패턴
💡
옵저버들의 목록을 객체에 등록하여 상태 변화가 있을때마다 메소드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하고, 옵저버들은 알림을 받아 조치를 취하는 디자인 패턴
💡

옵저버?

객체의 상태 변화를 관찰하는 관찰자들
어떤 사건, 상황, 맥락에 따라 관찰, 기록의 역할을 하는 사람이나 시스템

특징

  • 콜백
    • 콜을 먼저 하고 백은 나중에 함 ⇒ 언제 끝날지 모르니까 콜해놓고 끝나면 백 (즉각적으로 백하는게 아님)
  • 일대다(1:N) 관계
    • 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들에게 알림이 가고, 자동으로 정보가 갱신됨
  • 인터페이스를 통해 연결하여 느슨한 결합성 유지
    • Publisher 인터페이스
    • Observer 인터페이스

참고

  • 수동적인 서버(polling 방식)가 아닌 능동적인 서버(push 방식) 이용
    • polling은 수동적이어서 요청이 있어야만 함
    • push는 능동적이어서 클라이언트 상태 관리 필요 없음 ⇒ 변수로 안둬도 되는데 상태를 알고는 있어야 함

폴링 방식(polling)

  • 옵저버가 주기적으로 주체(특정 장치나 시스템)의 상태 조회 및 변경 사항 확인하고 필요 데이터 요청
    • 구현이 간단함 (장점)
    • 주기적으로 확인해서 예측 가능한 동작 할 수 있음 (장점)
    • 불필요한 요청 발생으로 자원 낭비할 수 있음 (단점)
    • 실시간성이 낮아 즉각적인 반응이 필요한 경우 타이밍 잘 못 맞춤 (단점)

푸쉬 방식(push)

  • 주체가 자신의 상태가 변경될 때마다 옵저버들에게 직접 알림과 필요한 데이터 보냄
    • 데이터 변경 즉시 반영, 실시간 반응 가능 ⇒ 타이밍 잘 맞춤 (장점)
    • 주체의 상태가 자주 변경된다면 옵저버들에게 데이터를 지속적으로 push하므로 주체에 부하 생길 수 있음 (단점)
 

예제 1

  • PubListener 인터페이스를 Pub 클래스에서 구현(implements)
    • SubListener를 담는 구독자 명단(List) 생성
    • 구독자 명단에 추가 (오버라이드)
    • 구독자 명단에서 삭제 (오버라이드)
    • 상품이 입고되면, 구독자들에게 알림을 보냄 (오버라이드)
  • SubListener 인터페이스를 Sub1, Sub2 클래스에서 구현(implements)
    • sub1, sub2가 각각 받은 알림 (오버라이드)
  • App 클래스의 main에서 실행
    • 객체 생성 (Pub, Sub1, Sub2를 new)
    • 구독하기 ⇒ Pub의 add메소드에 sub1(구독자), sub2(구독자)를 적용 ⇒ 구독자 명단에 추가됨
    • 상품 입고(가정)
    • 상품이 입고됐다는 알림 보냄(notifyChange 호출)
 
package ch07.pub; import ch07.sub.SubListener; public interface PubListener { void add(SubListener subListener); // 컬렉션 추가 void remove(SubListener subListener); // 컬렉션 삭제 void notifyChange(String msg); // 추가되든 삭제되든 알림 } package ch07.pub; import ch07.sub.SubListener; import java.util.ArrayList; import java.util.List; // 인터페이스 구현(implements PubListener) // => 오버라이드, 동적바인딩 public class Pub implements PubListener { // 1. 구독자 명단 List<SubListener> subList = new ArrayList<>(); // 2. 이 메소드의 책임 : 구독자 명단에 추가 @Override public void add(SubListener sub) { subList.add(sub); } // 2. 이 메소드의 책임 : 구독자 명단에 삭제 @Override public void remove(SubListener sub) { subList.remove(sub); } // 2. 이 메소드의 책임 : 상품이 입고되면, 구독자들에게 알림을 보냄 @Override public void notifyChange(String msg) { // 5초 잠자기 for (int i = 1; i < 6; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("."); } for (SubListener sub : subList) { sub.update(msg); } } } package ch07.sub; public interface SubListener { void update(String msg); } package ch07.sub; // 인터페이스 구현(implements SubListener) // => 오버라이드, 동적바인딩 public class Sub1 implements SubListener { @Override public void update(String msg) { System.out.println("sub1이 받은 알림 : " + msg); // 무엇을 해야할지는 이곳에서 구현 } } package ch07.sub; public class Sub2 implements SubListener { @Override public void update(String msg) { System.out.println("sub2가 받은 알림 : " + msg); // 무엇을 해야할지는 이곳에서 구현 } } package ch07; import ch07.pub.Pub; import ch07.pub.PubListener; import ch07.sub.Sub1; import ch07.sub.Sub2; import ch07.sub.SubListener; // 옵저버 패턴 -> 콜백 // 콜을 먼저 하고 백은 나중에(언제 끝날지 모르니까 / 콜해놓고 끝나면 백) public class App { public static void main(String[] args) { // 1. 객체 생성 init // 타입이 왜 부모형(이 경우 인터페이스)인가? // 각 클래스에 implement(구현) 해놨으니까 PubListener pub = new Pub(); SubListener sub1 = new Sub1(); SubListener sub2 = new Sub2(); // 2. 구독하기 pub.add(sub1); pub.add(sub2); // 3. 상품 들어옴(가정) // 4. notifyChange 호출 pub.notifyChange("상품 들어옴"); } }
 

예제2 - Thread(스레드) 응용

  • Pub 클래스
    • 상품 선언과 상품 입고 메소드
    • 구독자 명단
    • 구독자 명단에 추가 메소드
    • 상품 입고시 구독자에게 알림 보냄 메소드
  • SubListener 인터페이스를 Sub1, Sub2 클래스에서 구현(implements)
    • Sub1, Sub2 클래스에서 오버라이드 (메소드의 책임(기능) 작성)
  • App 클래스의 main에서 실행 (main 스레드)
    • 객체 생성
    • 구독하기
    • main 스레드가 새로운 스레드 달라고 요청
      • 마트에 상품 입고하는 스레드
      • 상품 입고되면 마트가 구독자에게 알림보내는 스레드
      •  
        ⇒ 스레드 객체 생성(new)하고 start()로 os로부터 스레드 빌림 ⇒ os가 run(실행)해주고 다 하면 생명주기 종료 ⇒ 스레드에서 run 메소드 (() → {}) 실행
 
package ch08.pub; import ch08.sub.SubListener; import java.util.ArrayList; import java.util.List; public class Pub { // 상품 public String value = null; // 상품 들어옴 메소드 public void income() { // 어떤 상품인지 초기화 value = "바나나"; } // 1. 구독자 명단 public List<SubListener> subList = new ArrayList<>(); // 2. 이 메소드의 책임 : 구독자 명단에 추가 public void add(SubListener sub) { subList.add(sub); } // 2. 이 메소드의 책임 : 물건이 들어오면, 구독자들에게 알림을 보냄 public void notifyChange(String msg) { // 구독자들에게 알림주기 // for-each문으로 subList 순회 for (SubListener sub : subList) { sub.update(msg); } } } package ch08.sub; public interface SubListener { void update(String msg); } package ch08.sub; // 인터페이스 구현(implements SubListener) // => 오버라이드, 동적바인딩 public class Sub1 implements SubListener { @Override public void update(String msg) { System.out.println("sub1이 받은 알림 : " + msg); // 무엇을 해야할지는 이곳에서 구현 } } package ch08.sub; public class Sub2 implements SubListener { @Override public void update(String msg) { System.out.println("sub2가 받은 알림 : " + msg); // 무엇을 해야할지는 이곳에서 구현 } } package ch08; import ch08.pub.Pub; import ch08.sub.Sub1; import ch08.sub.Sub2; import ch08.sub.SubListener; // 옵저버 패턴 -> 콜백 // 콜을 먼저 하고 백은 나중에 // => 언제 끝날지 모르니까 콜하자마자 백하는게 아님 public class App { public static void main(String[] args) { // 1. 객체 생성 init // 타입이 왜 부모형(이 경우 인터페이스)인가? // 각 클래스에 implement 해놨으니까 Pub pub = new Pub(); SubListener sub1 = new Sub1(); SubListener sub2 = new Sub2(); // 구독하기 pub.add(sub1); pub.add(sub2); // 2. 마트에 상품 입고하는 스레드 // 스레드 생성(구독자/서버, new로 객체 생성)하고 // start()로 os로부터 스레드를 빌림 // => os가 run(실행)을 해주고 다 하면 생명주기 종료 // 이 경우 현재 스레드는 3개 (main 스레드가 이미 존재하기 때문) // => main 스레드가 새로운 스레드를 달라고 요청함 // 스레드 파라미터 안의 ()->{} : run 메소드 new Thread(()->{ try { Thread.sleep(10000); // 10초 } catch (InterruptedException e) { throw new RuntimeException(e); } pub.income(); // 상품 들어옴 }).start(); // 3. 마트 스레드 new Thread(()->{ while(true) { if (pub.value == null) { // 상품 입고 안 됐을 경우 구독자한테 줄 알림 System.out.println("상품 미입고"); } else { // 상품 입고됐을 경우 구독자한테 줄 알림 pub.notifyChange(pub.value + " 입고됨"); break; // 상품 입고되면 break } try { Thread.sleep(1000); // 1초 } catch (InterruptedException e) { throw new RuntimeException(e); } } }).start(); // 이 경우, 이 시점에서 main 스레드는 종료(생명주기)되고 // main 스레드가 요청해놓은 스레드 2개는 살아있음 } }
Share article

Nakyeom's Study