관리 메뉴

HAMA 블로그

굿바이~ 옵저버 패턴 and FRP 본문

소프트웨어 사색

굿바이~ 옵저버 패턴 and FRP

[하마] 이승현 (wowlsh93@gmail.com) 2017. 8. 17. 10:44


문제 공유

우리는 오랫 동안 상호작용 되는 많은 부분에 있어서 옵저버패턴을 당연하듯 활용해 왔지만,

옵저버(관찰자, 소비자, 리스너) 패턴을 사용하다보면 경험 많은 개발자라면 누구나 "아 이거 먼가 깨름칙 한데" 라는 경험을 해보았을 것이다. 나 같은 평범한 개발자의 경우 그런 깨름칙한 냄새를 맡고서도, "내가 모자라서 그렇지 뭐" 자책을 하거나,  "여기서 어떻게 더 잘 고칠수 있지? 옵저버패턴은 Gof 패턴 중 하나이며 훌륭한것이니 더 나은것은 없을 거야" 라고 이른 만족을 하거나, "그냥 잘 굴러가는 거 같아 보이니, 냅두자", "나는 코드를 잘 이해하고 있어, 다른 신참이나 이해 부족한 개발자 네 탓" 이 라고 기술 부채를 남기며 자기 최면을 건다든지 할 것이다. 

하지만 역시 구루님들은 달랐다. 옵저버패턴의 문제점을 요목조목 따져가며 샅샅히 지적을 했으며, 그것에 대한 해결책까지 나왔다.  따라서 이젠 거인의 어깨에 올라 타서 그것에 관한 이해하고, 잘 만들어진 라이브러리를 사용하면 된다. 그 전반적인 내용에 대해서 설명 해 볼 예정이지만 모든 것을 쉽게 풀어서 적을 수는 없기에,  중간중간 이해 하기 힘든 구멍들은 각자 수고스럽지만 찾아보거나, 깊은 사색을 해야 할 것이다. 

옵저버 패턴

어떤 상태를 관리하는 Subject 객체(상태머신,생산자,Observable)가 있고 , 이 객체에서 일어나는 상태 변화에 따라서 해당 이벤트를 받길 원하는 옵저버(관찰자,소비자)들은 매니져 객체에 자신을 등록 한다.

그 후에, Subject 객체에서 어떤 상태가 변경 되었을 때 , 자신에게 등록된 옵저버들에게 이벤트를 notify 해주는게 옵저버 패턴의 주요 골자이며, Subject 객체은 관찰자들에 대해서 유연하게 커플링 되어 있게 된다. 즉 매니저 객체가 소비자들에 대해서 정확한 정보를 알 필요가 없이, 소비자들이 알아서 Subject 객체가 선언한 인터페이스를 따라주고, 등록해주면 되므로, 매니저 객체는 독립적인 컴포넌트가 될 수 있는 여지가 생기는데 옵저버패턴은 그런 유연성이라는 장점을 내세우는 패턴이라고 할 수 있다.

자~ 그럼 옵저버패턴은 어디서 사용 될 까?

글쓴이 본인은 주로 그래픽스/편집기 솔루션을 개발한 경험이 많기 때문에 관련하여 설명 해본다. (이러한 곳에서 정말 많이 사용된다) 

각종 도형의 리스트가 나열 되어 있는 리스트 박스가 있다. 우리는 그 리스트에 추가,삭제를 할 수 있는데, 도형 하나를 추가하면 (상태를 변경하면) 그 소식을 리스트의 데이터와 밀접한 관계를 가지는 다른 컴포넌트(객체) 들에게도 연락을 해줘야 한다. 만약 삼각형3을 추가해줬다면 아래와 같은 변경이 전파되어야 한다.

- 마우스 커서는 삼각형3에 해당하는 커서모양으로 변경
- 상태바에는 삼각형3가 추가되었다고 글씨가 추가
- 뷰화면에는 삼각형3에 해당하는 도형이 그려짐.

GUI 부분에서는 이것 말고도 매우 다양하게 사용되는데 다른 예는 마우스 버튼이 Subject 가 되며, 버튼이 클릭되는 상태변경 (좌클릭,우클릭,더블클릭등) 에 따라서 옵저버들이 고지 받아서 행동(선 그리기)하게 될 것이다. 

웹이나 통신 개발에서의 예를 들면 아래 처럼 구성이 될 것이다.

MVC 패턴에서, M (model) 은 subject 역할을 맡고, V(view) 는 Observer 역할을 맡는 것도 유추 할 수 있다. 

옵저버 패턴의 특징 

디커플링 : 상태머신이 자신이 호출해 줘야 할 객체들을 모두 강하게 내부 변수로 가지고 있게 되면, 상태머신 자체를 다른 곳에서 재활용하기 힘들게 된다.

제어역전 : 옵저버 패턴은 전형적인 제어 역전 구조로 (사실 제어역전이란 말은 모호하기 때문에 그냥 DI 로 사용하는게 나음) 옵저버들이 상태머신을 계속 polling 하거나, 상태머신을 호출하는 것이아니라,  상태머신에 자신의 레퍼런스를 넘겨서 (addObserver , addListener , subscribe ) 상태머신에 의해 콜백되게 만든다.

옵저버 패턴의 문제 

1. 예측 불가능한 순서

Subject (상태머신) 자체내에서는 옵저버들을 보통 리스트를 순회하며 Notify 를 해주게 되지만, 옵저버들의 추가,삭제가 자유로이 이루어지고 있고, 개별 옵저버 입장에서는 자신이 호출되는 순서에 대해서 알 수가 없다. 따라서 상태변경에 따른 행위를 할 때 , 자신의 순서 앞에서 어떤 다른 옵저버가 어떤 행위를 했는지를 미리 알 수가 없게 된다. 제어권을 Subject 로 제어역전을 시켜 준 결과 이렇게 되버리는데, 이것을 해결 하기 위해서는 옵저버들 전체를 일종의 트랜잭션으로 감싸서 트랙잭션이 끝날 때 어떤 행위를 하게 끔 하는 수 밖에 없지만, 코드복잡도가 상당히 올라가게 마련이다. 

2. 첫번째 이벤트 소실

Subject(상태머신) 에 DI 를 해주는 시점이, Subject 에서 첫번째 이벤트(상태변경)가 발생하는 시점 보다 늦게 될 수가 있다. 예를들어 클라이언트가 접속되었다는 이벤트를 통지 받지 못한다면, 클라이언트와 상호통신하는 옵저버는 무용지물이 될 것이다.

3. 지저분한 상태

Subject(상태머신) 은 위에서 보았다시피, 계속 변경되기 때문에 그로 인한 부수효과(side-effects) 가 발생하기 마련이다. 상태가 1~2개라면 모르겠으나, 상태가 만약 5개이상을 가지고 있다고 하자. 그 상태를 변경하는 이벤트들의 종류가 10가지 라고하면, 50개의 조합이 생겨난다. 그런 조합에 의해 옵저버들이 호출 되었는데, 먼가가 작동을 안하는 버그가 생겼을 때 , 현재 상태가 올바른 상태인지에 대한 디버깅이 힘들어지기 마련이다. 

4. 캡슐화 문제

옵저버패턴은 캡슐화를 종종 깨버리는데,  상태머신의 변경에 따라서 a 옵저버가 mylist 라는 변수를 초기화 시키고, b 옵저버는 mylist 라는 변수를 사용하게 되는 경우가 많이 있다.

5. 스레드 안전 문제

가장 곤혹스러운 문제이다. 일단 Subject (상태머신) 내에서도 락이나 경쟁관계를 해소해야하지만, 그것 보다 더 큰 문제는 각각의 옵저버들과 그 옵저버들이 호출하는 함수체인 속에서 어떤 락을 잡고 있는지 알 수 없게 되는 경향이 있다. 즉 A 옵저버가 a 락을 잡고 b 락을 잡으려고 하지만, B 옵저버가 이미 b락을 잡고 있다면 (a락을 잡아야 b락을 풀어준다면) 터지는거다. 역시 상태를 가지고있는 모든 OOP 프로그래밍에서의 쓰레드 사용은 큰 문제거리가 될 수 밖에 없을 것이다. 더군다나 옵저버패턴처럼 제어권이 역전 된 상태이면 더더욱~

6. 콜백 누수

가장 옵저버(리스너)를 등록 시킨 후에, 쓸모가 없어 졌을때 removeObserver, removeListener, dipose 등을 호출하는 것을 잊어 버렸다고 하자. 앞으로 상태가 변경 될 때 마다 쓸때없는 CPU 사이클만 날려버릴 것이다. 
옵저버 패턴이 생산자와 소비자의 제어 관계를 자연스럽게 역전 시켜서 생산자가 소비자에게 의존 하지 못하게 만드는 것이지만, 생산자의 실수를 소비자는 알 수가 없게 된다. 이상적이라면 이 관계를 다시 역전 시켜야 한다. 

7. 의도치 않은 재귀 

실제 현업에서 복잡한 솔루션을 짤 때, 이 문제도 쓰레드 문제와 같이 가장 크게 다가오곤 한다. 예를들어 커맨드 패턴에서 execute 가 발생해서 -> 상태머신(S) 의 상태를 변경하면 -> 옵저버 A가 호출되고 -> 옵저버 B가 호출되고 -> ... -> 옵저버A 는 어떤 다른 함수를 호출하고 그 함수는 -> 무엇인가를 하고 -> 여기서 끝나야 하지만 ->  마지막 함수는 다시 상태머신(S) 를 변경한다. 

바보라고 말 할 수 있겠지만, 정말 복잡한 수백만 라인의 코드에서는 종종 일어나는 일이다. 자신이 코드의 모든 곳을 속속들이 알지 못하는 신참일 경우, 일 부분에 대해서만 작업을 하게 되는데 자신의 작업이 가져 올 여파까지는 미리 알 수가 없는 상황인 경우가 발생한다.

8. 기타등등

Composability, SOC , Scalabilty, Abstraction, Resource management, Semantic distance 등이 있으며. 아래 레퍼런스 중 첫번째 마틴오더스키의 논문에 짧은 코멘트가 적혀져 있다.

자! 이 모든 문제를 해결 해야 할 때가 왔다. FRP(Functional Reactive Programming) 이 그것을 해준다.
다음 편에서는 아래의 주제로 이것에 대한 해결 방안들을 알아 보자.


함수적 반응형 프로그래밍 (FRP) 

먼저 리액티브라는 단어에는 다양한 의미가 있지만 가장 개발자로써 와닿을 수 있는 예시를 하나 보자.
{
  a();
  b():
  c();
}

절차지향에 익숙한 우리는 위의 함수들이 순서대로 발생할 것이라는 것을 염두해 두코 코딩을 하게 된다.

즉 c() 함수는 a() 함수와 b() 함수가 무엇인가 처리를 한 후에 그것을 가지고 처리한다는 건데, 물론 순서가 중요하지 않을 때 도 있다. 이때 나중에 참여한 개발자가 저것을 다 파악하기란 힘들 것이다. (물론 소스가 복잡해 진 다면 말이죠) 또한 옵저버패턴이 사용된다면 그 순서란 더 감춰지기 마련이니 파악하기 힘들어 질 것이다.

의존 관계 와 순서는 이렇게 우리가 짜왔던 절차지향,객체지향에서는 혼동을 주게 되는데 최근에 코어를 더 적극적으로 활용해야하는 시대에 와서 멀티쓰레드라는 동시성을 다루는 부분에 있어서 더더욱 순서와 의존관계를 파악하기 힘들어 졌다.

FRP 와 Rx 의 반응형이라는 말에 뒤에는 이런 순서&의존관계를 명확하게 인지 시키준다는 의미도 포함되어 있으며 앞으로 FRP 와 Rx 를 공부하는데 있어서 이 점을 머리속에 넣어 두면 좋을 거 같다.

첨가)  FRP와 Rx 시리즈들과는 다르다. Sodium FRP 저자가 작성한 내용을 참고하면 



작성중.... 



레퍼런스:

https://infoscience.epfl.ch/record/148043/files/DeprecatingObserversTR2010.pdf
Functional Reactive Programming 
https://www.scala-lang.org/
https://github.com/ReactiveX/RxJava
https://github.com/SodiumFRP/sodium
https://github.com/ReactiveX/RxPY

Comments