디자인 패턴: 옵저버 (observer) 패턴

DanChu 🌟·2022년 7월 15일
0
  • 옵저버 패턴에서는 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로, 일대다(one-to-many) 의존성을 정의함.

  • 일대다 관계는 주제(Subject)와 옵저버(Observer)에 의해 정의된다.

  • 옵저버 패턴의 구현 방법은 여러가지가 존재하나, 대부분 주제(subject) 인터페이스와 관찰자(observer) 인터페이스가 들어있는 클래스 디자인으로 함.

  • 주제에서는 관찰자를 등록 및 해지, 각 관찰자에 상태 변경을 알리기 위한 메서드를 포함하고 있음


느슨한 결합 (Loose Coupling)

주제와 옵저버가 서로 상호작용을 하긴 하지만 서로에 대해 잘 모른다는 것을 의미. 디자인 원칙상 서로 상호작용을 하는 객체 사이에는 가능한 느슨하게 결합하는 디자인을 사용해야 함 -> 객체 사이 상호의존성 최소화

  1. 주제가 옵저버에 대해서 아는것은 옵저버가 특정 인터페이스(Observer 인터페이스)를 구현한다는 것 뿐이다.

    • 옵저버의 구상클래스가 무엇인지, 옵저버가 무엇을 하는지 등에 대해서는 알 필요가 없다.
  2. 옵저버는 언제든지 새로 추가할 수 있다.

    • 주제는 Observer 인터페이스를 구현하는 객체의 목록에만 의존하기 때문에 언제든지 새로운 옵저버를 추가하거나, 기존 옵저버를 제거하거나 등의 작업을 할 수 있다.
  3. 새로운 형식의 옵저버를 추가하려고 할 때도 주제를 전혀 변경할 필요가 없다.

    • 새로운 클래스에서 Observer 인터페이스를 구현하고 옵저버로 등록하기만 하면 된다.
  4. 주제와 옵저버는 서로 독립적으로 재사용할 수 있다.

    • 주제와 옵저버가 서로 단단하게 결합되어 있지 않기 때문에 다른 용도로 손쉽게 재사용이 가능하다.
  5. 주제나 옵저버가 바뀌더라도 서로한테 영향을 미치지 않는다.

    • 둘이 서로 느슨하게 결합되어 있기 때문이다.

옵저버 패턴의 구조

https://en.wikipedia.org/wiki/Observer_pattern

  • Subject: Observer 를 알고 있는 주체입니다. 이는 Observer 를 등록하고 제거하는 데 필요한 인터페이스를 정의합니다. 데이터를 관리하고, 데이터가 달라지면 관찰자에게 데이터 전달 (e.g. 신문 출판사)
  • Observer: Subject 에서 변화했다고 알렸을때 갱신해야하는데 필요한 인터페이스를 정의합니다. (e.g. 구독자)
  • ConcreteSubject: 객체에게 (Observer 들에게) 알려줘야할 상태를 저장하고, notify 해야할 함수를 만들도록 합니다.
  • ConcreteObserver: noti 를 받았을때 행동할 로직을 작성합니다.

옵저버 패턴의 장단점

장점

  • 변경 사항이 있더라도 서로에게 영향을 미치지 않는 (객체 사이의 상호의존성 최소화) 유연한 객체지향 시스템을 구축할 수 있음
  • pull 방식이 아닌 push 방식을 사용함으로 직관적으로 이해하기 쉬움

단점

  • Thread safe 하지 않아 구독을 신청/취소하는 동안 원하는 결괏값을 얻기 힘들수도 있습니다.
  • observer 를 제때 제거해주지 않으면 메모리 누수가 일어날 수 있습니다.
  • 너무 많이 사용하게 되면, 상태 관리가 힘들 수 있습니다.
  • 비동기 방식이기 때문에 이벤트 구독을 원하는 순서대로 받지 못할 수 있습니다.

옵저버 패턴 예시

유명인이 트위터에 글을 쓰면 뉴욕타임즈, 가디언, 르몽드가 관심 키워드에 따라 반응하는 방식

interface Observer {
    void notify(String tweet);
}
/* 삭제 예정 */
class NYTimes implements Observer {
    public void notify(String tweet) {
        if (tweet != null && tweet.contains("money")) {
            System.out.println("Breaking news in NY! " + tweet);
        }
    }
}

/* 삭제 예정 */
class Guardian implements Observer {
    public void notify(String tweet) {
        if (tweet != null && tweet.contains("queen")) {
            System.out.println("Yet more news in London... " + tweet);
        }
    }
}

/* 삭제 예정 */
class LeMonde implements Observer {
    public void notify(String tweet) {
        if (tweet != null && tweet.contains("wine")) {
            System.out.println("Today cheese, wine, and news! " + tweet);
        }
    }
}
interface Subject {
    void registerObserver(Observer o);
    void notifyObservers(String tweet);
}
class Feed implements Subject {
    private final List<Observer> observers = new ArrayList<>();
    
    public void registerObserver(Observer o) {
        this.observers.add(o);
    }
    
    public void notifyObservers(String tweet) {
        observers.forEach(o -> o.notify(tweet));
    }
}

사용 :

Feed f = new Feed();
f.registerObserver(new NYTimes());
f.registerObserver(new Guardian());
f.registerObserver(new LeMonde());
f.notifyObservers( "The queen said her favourite book is Java 8 in Action!");

그런데 Observer가 한 개의 메소드만 갖고 있는 인터페이스이므로, 람다를 사용하면 클래스를 만들지 않고도 옵저버를 등록할 수 있다.

f.registerObserver((String tweet) -> {
    if (tweet != null && tweet.contains("money")) {
        System.out.println("Breaking news in NY! " + tweet);
    }
});

즉, NYTimes 클래스를 삭제하고 실행 코드를 다음과 같이 수정할 수 있다.

Feed f = new Feed();
// NYTimes를 대체하는 코드. 이제 NYTimes 클래스는 삭제해도 된다.
f.registerObserver((String tweet) -> {
    if (tweet != null && tweet.contains("money")) {
        System.out.println(
                "Breaking news in NY! " + tweet
        );
    }
});
f.registerObserver(new Guardian());
f.registerObserver(new LeMonde());
f.notifyObservers( "The queen said her favourite book is Java 8 in Action!");
f.notifyObservers( "money!");




references

0개의 댓글