본문 바로가기

OOP, FP/디자인패턴

헤드퍼스트 디자인 패턴: 2. 옵저버 패턴

지난 포스팅에서 알고리즘을 캡슐화하고 동적으로 알고리즘을 변경할 수 있는 스트래티지 패턴에 대해 복습했다.

오늘은 상태의 변화를 알리면서 의존성을 줄이는데 초점을 두는 옵저버 패턴에 대해 복습한다.


옵저버 패턴

어떤 객체의 상태가 바뀌거나 중요한 메소드가 호출 되었을 때 이와 관련된 객체들에게 소식을 전달할 수 있는 패턴이다.


일대일로 연락을 전할 뿐 아니라, 연락을 전달하는 입장에서는 일대다 관계,

연락을 받는 입장에서는 다대일 관계, 즉 다대다 관계로 연락을 전달할 수 있다.


옵저버 패턴은 이러한 연락을 주고받는 관계에서 의존성을 줄이는데 목적을 둔다.

디자인패턴의 4번째 원칙 "서로 상호작용을 하는 객체 사이에서는 가능한 느슨하게 결합하는 디자인을 사용한다"

와 밀접한 관계를 가진 패턴이다.


옵저버 패턴의 정의

한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고

연락받은 객체에서는 자동으로 내용이 갱신되는 일대다(One to Many) 의존성을 정의한다.



일대다 관계는 주제와 옵저버에 의해 정의된다.

주제는 연락을 전달하는 오브젝트, 옵저버는 연락을 받는 오브젝트를 의미한다.

여기서 옵저버들이 주제에 의존하게 된다.



옵저버 패턴에서 상태를 저장하고 상황을 지배하는 것은 주제 객체이다.

따라서 상태가 들어있는 주제는 하나의 역할에서 하나만 있을 수 있다.

옵저버는 상태를 주제로부터 가져오거나 받아서 사용하긴 하지만 반드시 직접적으로 상태를 갖고있어야 하지는 않다.

주제 객채에서 상태가 바뀌었다는 것을 알려주기를 기다리는, 혹은 주제로부터만 상태를 가져올 수 있는 상황이기 때문에

옵저버들이 주제에 의존적인 성질을 가지게 된다.


이러한 방법을 사용하면 여러 객체에서 동일한 데이터를 제어하도록 하는 것에 비해 더 깔끔한 디자인을 만들 수 있다.

주제에 해당하는 객체없이 옵저버들에서 같은 상태를 관리하는 것보단,

이와 같이 하나의 주제 클래스에서 상태를 관리하고 옵저버에게 상태를 던져주거나 가져가도록 하는 것이

의존성을 줄이고 유지보수성을 높이는 한 방법이다.


느슨한 결합(Loose Coupling)

앞서 상호작용하는 객체 사이에서는 느슨한 결합을 갖도록 디자인하라는 원칙을 살펴봤다.

여기서 두 객체가 느슨하게 결합되어있다는 것의 의미는 둘이 무슨 메세지를 주고 받긴 하지만

익명 채팅처럼 서로에 대해서는 잘 모른다는 것을 의미한다.


옵저버 패턴으로 디자인을 함으로써 느슨한 결합의 효과를 얻을 수 있다.

그 이유는 다음과 같다.


(1) 주제의 입장에서 옵저버라는 객체들에 대해 알고있는 것은 특정 인터페이스를 구현한다는 것 뿐이다.

- 연락을 받거나 상태를 가져갈 수 있는 Observer라는 인퍼테이스를 구현한 구상 클래스


(2) 옵저버는 언제든지 새로 추가할 수 있다.

- 주제는 Observer라는 인터페이스를 구현한 객체라면 그 객체가 어느 종류의 클래스인지는 상관없이 옵저버로 등록할 수 있다.

- 구체적인 구상클래스에 의존하지 않기 때문에 실행중에 옵저버 객체를 제거하거나 다른 클래스의 인스턴스로 변경할 수 있다.


(3) 새로운 형식(규격)을 갖는 옵저버를 추가하려고해도 주제를 전혀 변경할 필요가 없다.

- 옵저버 클래스가 Oberser의 인터페이스를 어떤 형식으로든 구현만 하면 다른 규격을 갖는 클래스나 인터페이스를 구현해도

  주제 입장에서는 전혀 상관이 없다(예: class ConcreateObserver implements Observer, Comparable)


(4) 주제와 옵저버는 서로 독립적으로 재사용할 수 있다.

- 주제와 옵저버는 서로에 대해 아는것은 특정 인터페이스(Subject, Observer)를 구현한 객체라는 것 뿐이다.

  이렇게 느슨한 결합을 갖고있기때문에 주제나 옵저버를 전혀 다른 용도로 재사용할 수 있다.

  Ex) Observer를 인스턴스 변수로 구성하고 기능을 확장하거나 변경할 수 있다.


(5) 주제나 옵저버가 바뀌더라도 서로한테 영향을 미치지는 않는다.

- 앞서 언급한 이유들과 상통한다.(느슨한 결합)


결국 느슨한 결합을 사용하면 어떤 변경 사항이 생겨도 의존성이 최소화되어있기 떄문에 무난히 처리할 수 있는 유연한 시스템을 구축할 수 있다.



우리가 스프링을 이용해서 웹을 만들때의 간략한 상황이다.

모델은 오늘의 날짜에 해당하는 상태를 갖고있는 주제이다.

뷰는 날짜라는 상태를 화면에 표시하는 역할을 하는 옵저버이다.


모델에서 setDate()로 날짜를 변경하면 notify()를 통해 observers(View)에게 소식을 전달한다.

뷰에서 주제인 모델의 상태가 변경되었음을 update()를 통해 통보받았으니 화면의 날짜정보를 갱신한다.



푸시방식과 풀 방식

여태까지 살펴본 방식은 주제가 일방적으로 옵저버에게 통보만 가능한 푸시 방식이었다.

하지만 옵저버들은 다양하게 재사용될 수 있으므로 소식을 전달받는 특징은 한 부분에 불과하다.

이는 옵저버들은 연락을 받는 것 외에도 중요한 역할이 있음을 의미한다.


또 어떤 옵저버에서는 주제의 모든 상태를 알 필요가 없을 수도 있다.

날짜를 관리하는 주제객체에서 온도정보만 필요한 옵저버가 있을 수도 있다는 얘기다.


이러한 상황때문에 옵저버가 직접 주제로부터 데이터를 가져오는 방식인 풀 방식이 존재한다.

고맙게도 자바에는 푸시 방식과 풀 방식의 옵저버 패턴을 마음대로 쓸 수 있게 해주는 Observable(주제)과 Observer(옵저버)클래스가 존재한다.


하지만 자바에 내장된 주제인 Observable은 인터페이스가 아닌 클래스이다.

우리는 앞서 구현보다는 인터페이스를 주로 프로그래밍하라는 원칙을 만났었다.


그럼 이러한 구현(클래스)를 사용하게 되는 단점은 무엇일까?

(1) 어떤 클래스를 주제로 사용하고 싶을 때 이미 수퍼클래스가 있는 경우 해당 클래스는 주제로 사용할 수 없다.

- 다중 상속의 문제, 재사용 불가


(2) Observable이 인터페이스가 아니기 때문에 자바에 내장된 Observer API하고 잘 맞는 클래스를 직접 구현할 수 없다.

- Observable이라는 클래스는 java.util이라는 패키지에 속해있다.

  때문에 java.util 구현을 멀티스레드로 구현한다거나 하는 일이 불가능하다.


* 이는 아직 정확히 무슨 의미를 설명하는 건지 모르겠다.

  인터페이스가 아니기때문에 안좋다는 건 알겠는데 Observer API....?

  나중에 옵저버 패턴을 쓰게되면 이게 무슨 의미인지 깊게 파고들어봐야겠다.


(3) setChanged()라는 메소드가 protected로 선언되어 있다.

- 즉 Observable의 서브클래스에서만 setChanged()를 호출 할 수가 있다.

  java.util.Observable은 changed가 true로 설정되어 있지 않으면 notifyObservers()를 호출해도 아무 일이 일어나지 않는다.

  내가 어떤 클래스를 만들고 Observable의 서브클래스를 인스턴스 변수로 구성할 수가 없다.

  결국 상속으로만 이를 해결할 수 있다. (디자인 패턴 원칙에서 상속보다는 구성을 사용하라고 강조했다.)




내가 만든 클래스가 java.util.Observable을 확장(상속)이 가능하다면 자바에서 내장된 옵저버 패턴을 사용하는 것도 좋다.

하지만 사용이 불가능하다면 직접 옵저버 패턴을 디자인해야한다.

위에서 예시에도 등장했듯이 옵저버 패턴은 우리의 프레임워크에 녹아있는 MVC 패턴과 관계가 깊다.



상태의 변화를 알리면서 의존성을 줄이는데 초점을 두는 옵저버 패턴에 대한 복습을 마치겠다.


1. 스트래티지 패턴

2. 옵저버 패턴