디자인 패턴(구)/헤드 퍼스트 디자인 패턴

[디자인 패턴] 2장 - 옵저버 패턴(Observer Pattern) 알아보기

공대키메라 2023. 2. 18. 23:05

지난 시간에는 디자인 패턴의 필요성과 디자인 원칙에 대해 공부하였다.

 

지난 내용이 궁금하면 여기 클릭!!

 

이번 시간에는 옵저버 패턴에 대해 공부할 것이다.

 

이를 위해 상황 설명, 얻을 수 있는 장점, 그리고 코드로 옵저버 패턴에 대해 이해할 것이다.

 

참고한 내용은 다음과 같다.

https://m.hanbit.co.kr/store/books/book_view.html?p_code=B6113501223 

1. 객체와 객체지향?

객체는 실제를 본떠 새로운 세계를 창조하는 것으로,

 

각각의 객체는 자신의 역할과 책임을 다하고 각자의 역할을 충실한 채로 서로 협력하는 것을 객체 지향이라고 한다.

 

이 객체란 무엇인가에 대해 인지를 하고 다음 상황 설명을 보고, 음미해보면 더욱 이해가 잘 된다.

2. 상황 설명

우리는 재난 감지 시스템용 어플리케이션 제작을 수주받았다!(경축!)

초기 재난 감지 시스템의 초기에는 화재 경보, 지진 경보, 그리고 외부 침입 경보 기능을 제공하고,
화재, 지진, 외부 침입관련 정보를 관리 센터에서 관리한다. 

그리고 이 정보를 경보 시스템을 구매한 사용자에게 알림 메시지로 전달할 것이다. 

그러면 우리는 어플리케이션을 어떻게 구현해야 할까?

3. 구조 알아보기 + 구상 + 옵저버 패턴 적용

현재 위의 상황 설명을 우리가 해야할 일의 흐름으로 정리하자면,

 

재난 관리 본부에서 일차적으로 각각 재난에 대해 감지를 하고 => (1)

 

이러한 정보를 정보를 잘 정리해서 => (2)

 

알림문자를 사용자에게 전송 => (3) 하면 된다.

 

 

과연... 위의 그림처럼 원하는 대로 잘 될지 우선 해보자!

 

 

알림문자는 재난 상황에 대해 브리핑한다.

 

지진, 화재, 외부 침입에 대한 현재의 상황에 대해 알려주는 것이다. 

 

이 요구사항을 만족시키는데 문제는 확장성이 좋은 코드로 개발해야 한다.

(소프트웨어 개발에서 바뀌지 않는 단 하나의 사실은 변화한다는 사실이다.)

 

이를 위해 먼저 필자는 '정보 저장소' 객체를 생성할 것이다.

 

여기서 '정보 저장소' 객체를 하나 만들 것인데, 단순히 정보만 저장하는게 아닌, 하나의 역할을 하는 객체로 생각하자.

즉, 우리가 창고가 하나 있다고 가정한다면, 그 창고가 직접 필요로 하는 물품을 찾아 주고, 창고 정리도 잘 하는 것이다.

다시 말해서, 실제를 본떠 새로운 세계를 창조하는 것! 이니 이를 절대 잊지말자!

새로운 것을 창조한다면 그 어떤 일이 일어나도, 역할에만 충실하면 된다!

 

근데 객체를 생성하는것도 생성 하는 것인데...

 

어떤 구조가 보통 Observer패턴이라고 하는지 diagram을 보자.

 

출처 : https://dejavuhyo.github.io/posts/observer-pattern/

 

GoF(Gang Of Four)에서는 다음과 같이 Obsever 패턴의 의도를 다음과 같이 설명한다.

객체 사이에 일 대 다의 의존 관계를 정의해 두어, 어떤 객체의 상태가 변할 때 그 객체에 의존성을 가진 다른 객체들이그 변화를 통지받고 자동으로 갱신될 수 있게 만든다.
 
즉, 다수의 객체가 특정 객체 상태 변화를 감지하고 알림을 받는 패턴이다.
 
이것을 이용해 우리는 발행(publish)-구독(subscribe) 패턴을 구현할 수 있다.

 

우리가 만들고자 하는 구조는 하나의 재난 관리 센터(일 - One) 에서 여러 관리자(다수 - Many)에게 변화를 통지받는다(메시지 전송!)  즉, 다수의 객체가 특정 객체 상태의 변화를 감지하고 알림을 받는 패턴이다.

 

다음 다이어그램에 맞게 코드를 작성했다.

 

 

Subject 인터페이스 <= Subject

public interface Subject {
    void registerOwner(Owner o);
    void removeOwner(Owner o);
    void notifyOwners();
}

Owner 인터페이스 <= Observer

public interface Owner {
    void detect();
}

MessageSending <= 메시지 전송용 객체 

public interface MessageSending {
    public void sendMessage(String hhp, String msg, LocalDateTime time);
}

DetecetedDisasterInfoRepository <= ConcreteSubject

public class DetectedDisasterInfoRepository implements Subject {
    private List<Owner> owners;
    private String msg;
    public DetectedDisasterInfoRepository() {
        this.owners = new ArrayList<>();
    }

    @Override
    public void registerOwner(Owner o) {
        this.owners.add(o);
    }

    @Override
    public void removeOwner(Owner o) {
        this.owners.remove(o);
    }

    @Override
    public void notifyOwners() {
        for (Owner owner : owners) {
            owner.detect();
        }
        System.out.println("\n");
    }

    public void setDetectedInfo(String msg) {
        this.msg = msg;
        changeInfo();
    }

    public void changeInfo() {
        notifyOwners();
    }

	// getter & setter 선언
    
}

MasterChef.java <= ConcreteObserver

public class MasterChef implements Owner, MessageSending {

    private DetectedDisasterInfoRepository repository;
    private String info;
    private String hhp = "010123123123";

    //연락 받을 사람을 생성 시 바로 Observer로 등록해버림!
    public MasterChef(DetectedDisasterInfoRepository repository) {
        this.repository = repository;
        repository.registerOwner(this);
    }

    @Override
    public void detect() {
        sendMessage(hhp, repository.getMsg(), LocalDateTime.now());
    }

    @Override
    public void sendMessage(String hhp, String msg, LocalDateTime time) {
        System.out.println(String.format("대장님께 전송 => 전화번호 : %s / 내용 : %s / 시간 : %s", hhp, Optional.ofNullable(msg).orElse("nothing"), time.toString()));
    }
}

Workers.java <= ConcreteObserver

public class Workers implements Owner, MessageSending {
    private DetectedDisasterInfoRepository repository;
    private String info;
    private List<String> hhps = List.of("12341234","5454544");

    //연락 받을 사람을 생성 시 바로 Observer로 등록해버림!
    public Workers(DetectedDisasterInfoRepository repository) {
        this.repository = repository;
        repository.registerOwner(this);
    }

    @Override
    public void detect() {
        for (String hhp : hhps) {
            sendMessage(hhp, repository.getMsg(), LocalDateTime.now());
        }
    }

    @Override
    public void sendMessage(String hhp, String msg, LocalDateTime time) {
        System.out.println(String.format("당직 근무자에게 전송 => 전화번호 : %s / 내용 : %s / 시간 : %s", hhp, Optional.ofNullable(msg).orElse("nothing"), time.toString()));
    }

}

ObservetTest.java <= 테스트

public class ObjectTest {
    public static void main(String[] args) {
        DetectedDisasterInfoRepository repository = new DetectedDisasterInfoRepository();
        MasterChef masterChef = new MasterChef(repository);
        Workers workers = new Workers(repository);

        repository.setDetectedInfo("지진이다!");
        repository.setDetectedInfo("화재다!");
        repository.setDetectedInfo("으악! 도둑이다!");

		/*
        	실행 결과
            대장님께 전송 => 전화번호 : 010123123123 / 내용 : 지진이다! / 시간 : 2023-02-18T22:20:31.882431200
            당직 근무자에게 전송 => 전화번호 : 12341234 / 내용 : 지진이다! / 시간 : 2023-02-18T22:20:31.897480700
            당직 근무자에게 전송 => 전화번호 : 5454544 / 내용 : 지진이다! / 시간 : 2023-02-18T22:20:31.898477


            대장님께 전송 => 전화번호 : 010123123123 / 내용 : 화재다! / 시간 : 2023-02-18T22:20:31.898477
            당직 근무자에게 전송 => 전화번호 : 12341234 / 내용 : 화재다! / 시간 : 2023-02-18T22:20:31.898477
            당직 근무자에게 전송 => 전화번호 : 5454544 / 내용 : 화재다! / 시간 : 2023-02-18T22:20:31.898477


            대장님께 전송 => 전화번호 : 010123123123 / 내용 : 으악! 도둑이다! / 시간 : 2023-02-18T22:20:31.898477
            당직 근무자에게 전송 => 전화번호 : 12341234 / 내용 : 으악! 도둑이다! / 시간 : 2023-02-18T22:20:31.898983
            당직 근무자에게 전송 => 전화번호 : 5454544 / 내용 : 으악! 도둑이다! / 시간 : 2023-02-18T22:20:31.898983

        */
    }
}

4. 옵저버 패턴의 장단점

장점

  • 상태를 변경하는 객체(publisher)와 변경을 감지하는 객체(subsriber)의 관계를 느슨하게 유지할 수 있다.
  • Subject 상태 변경을 주기적으로 조회하지 않고 자동으로 감지할  있다.
  • 런타임에 옵저버를 추가하거나 제거할  있다.

단점

  • 복잡도가 증가한다.
  • 다수의 Observer 객체를등록이후해지않는다면 memory leak이발생할수도있다

5. 느슨한 결합??

느슨한 결합은 느슨하게 결합된 것이라고?(???)

 

이것의 정확한 의미는 무엇인지 알아보았다.

 

느슨한 결합은 객체들이 상호작용할 수 는 있지만, 서로를 잘 모르는 관계를 의미한다.

이를 통해 유연성이 아주 좋아진다.

 

정말로 곰곰히 생각해보자.

 

Subject의 구현체인 DetectedDisasterInfoRepository는 Owner(옵저버)가 무엇인지 알 필요가 없다. 

 

현재 코드 구조에서는 특정 인터페이스를 구현한다는 사실만 알기 때문이다.

 

그리고, DetectedDisasterInfoRepository는 Observer 인터페이스르 구현하는 객체의 목록에만 의존하므로 언제든지 새로운 옵저버를 추가할 수 있다.

 

이렇게 되면 Subject와 Owner는 서로 독립적으로 재사용이 가능하며, 달라져도 전혀 문제가 없다!

 

디자인 원칙 4 - 상호작용하는 객체 사이에는 가능하면 느슨한 결합을 사용해야 한다!

느슨하게 결합하는 디자인을 사용하면 변경 사항이 생겨도 무난히 처리할 수 있는 유연한 객체지향 시스템이 구축 가능!

 


이렇게 옵저버 패턴에 대해 알아보았다.

 

지난 글에는 디자인 패턴의 필요성에 대해 알아보았다.

 

지난 글에서 소개한 디자인 원칙에 이번 옵저버 패턴 예시 코드가 적절한지 생각해보면 더욱 재미있을 것이다.