한빛출판네트워크

기획연재

디자인 패턴 다시 보기: 관찰자 패턴 (2/2)

한빛미디어

|

2016-07-12

by 로버트 나이스트롬

7.1. “너무 느려”

관찰자 패턴을 제대로 이해하지 못한 프로그래머들이 이런 얘기를 많이 한다. 이런 개발자들은 ‘디자인 패턴’ 비슷한 이름만 붙어 있어도 쓸데없이 클래스만 많고 우회나 다른 희한한 방법으로 CPU를 낭비할 것으로 지레짐작한다.

관찰자 패턴은 특히 ‘이벤트’, ‘메시지’, 심지어 ‘데이터 바인딩’ 같은 몇몇 어두운 친구들과 어울려 다닌다는 얘기 때문에 부당한 평가를 받아왔다. 이런 시스템 중 일부는 (대부분은 다 생각이 있어서 그렇지만) 알림이 있을 때마다 동적 할당을 하거나 큐잉queuing하기 때문에 실제로 느릴 수 있다. (이래서 패턴의 문서화가 중요하다. 용어가 모호하면 분명하고 간결하게 의사소통할 수 없다. 같은 걸 놓고 사람마다 ‘관찰자’니 ‘이벤트’니 ‘메시징’이니 하는 식으로 다르게 부르는 이유는 누구도 이들 차이를 써두지 않았거나, 아무도 그런 글을 읽지 않아서다. 용어를 분명하게 하는 것도 이 책을 낸 이유 중 하나다. 내 말에 책임을 지기 위해 이벤트와 메시지를 다루는 이벤트 큐(15장)를 책에 수록했다.)

하지만 관찰자 패턴 예제 코드를 봐서 알겠지만 전혀 느리지 않다. 그냥 목록을 돌면서 필요한 가상 함수를 호출하면 알림을 보낼 수 있다. 정적 호출보다야 약간 느리긴 하겠지만, 진짜 성능에 민감한 코드가 아니라면 이 정도 느린 건 문제가 되지 않는다.

게다가 관찰자 패턴은 성능에 민감하지 않은 곳에 가장 잘 맞기 때문에, 동적 디스패치를 써도 크게 상관없다. 이 점만 제외하면 성능이 나쁠 이유가 없다. 그저 인터페이스를 통해 동기적synchronous으로 메서드를 간접 호출할 뿐 메시징용 객체를 할당하지도 않고, 큐잉도 하지 않는다.

 

 

7.1.1. 너무 빠르다고?

사실, 주의해야 할 점은 관찰자 패턴이 동기적이라는 점이다. 대상이 관찰자 메서드를 직접 호출하기 때문에 모든 관찰자가 알림 메서드를 반환하기 전에는 다음 작업을 진행할 수 없다. 관찰자 중 하나라도 느리면 대상이 블록될 수도 있다.

무시무시하게 들리겠지만 실제로는 그렇게 큰일은 아니다. 그냥 알고 있으면 된다. 오랫동안 이벤트 기반 프로그래밍을 해온 UI 프로그래머들은 ‘UI 스레드를 최대한 멀리하라’는 말을 예전부터 들어왔다.

이벤트에 동기적으로 반응한다면 최대한 빨리 작업을 끝나고 제어권을 다시 넘겨줘서 UI가 멈추지 않게 해야 한다. 오래 걸리는 작업이 있다면 다른 스레드에 넘기거나 작업 큐를 활용해야 한다.

관찰자를 멀티스레드, 락lock과 함께 사용할 때는 정말 조심해야 한다. 어떤 관찰자가 대상의 락을 물고 있다면 게임 전체가 교착상태에 빠질 수 있다. 엔진에서 멀티스레드를 많이 쓰고 있다면, 이벤트 큐(15장)를 이용해 비동기적으로 상호작용하는 게 더 좋을 수도 있다.

 

 

7.2. “동적 할당을 너무 많이 해”

게임 개발자를 포함한 많은 프로그래머 무리가 가비지 컬렉션을 지원하는 언어로 이주한 뒤로 동적 할당은 더 이상 예전만큼 무서운 존재가 아니다. 하지만 아무리 관리 언어managed language(가비지 컬렉션 기능이 있는 언어 - 옮긴이)로 만든다고 해도 게임같이 성능에 민감한 소프트웨어에서는 메모리 할당이 여전히 문제가 된다. 저절로 된다고는 하나 메모리를 회수reclaim하다 보면 동적 할당이 오래 걸릴 수 있다. (게임 개발자들은 메모리 할당보다 메모리 단편화를 더 신경 쓴다. 콘솔 업체로부터 인증을 받으려면 실행해놓은 게임이 며칠간 뻗지 않아야 하는데, 힙이 단편화로 점점 커진다면 출시가 어려울 수 있다. 단편화와 이를 막기 위한 기법은 객체 풀 패턴(19장)에서 자세히 다룬다.)

앞서 본 예제에서는 코드를 정말 간단하게 만들기 위해 고정 배열을 사용했다. 실제 게임 코드였다면 관찰자가 추가, 삭제될 때 크기가 알아서 늘었다가 줄어드는 동적 할당 컬렉션을 썼을 것이다. 일부 프로그래머들은 이렇게 메모리가 왔다 갔다 하는 걸 두려워한다.

물론 실제로는 관찰자가 추가될 때만 메모리를 할당한다. 알림을 보낼 때는 메서드를 호출할 뿐 동적 할당은 전혀 하지 않는다. 게임 코드가 실행될 때 처음 관찰자를 등록해놓은 뒤에 건드리지 않는다면 메모리 할당은 거의 일어나지 않는다.

그래도 찜찜해할까 봐 동적 할당 없이 관찰자를 등록, 해제하는 방법을 살펴보겠다.

 

 

7.2.1. 관찰자 연결 리스트

지금까지 본 코드에서는 Subject가 자신에게 등록된 Observer의 포인터 목록을 들고 있다. Observer 클래스 자신은 이들 포인터 목록을 참조하지 않는다. Observer는 그냥 순수 가상 인터페이스다. 상태가 있는stateful 구체 클래스보다는 인터페이스가 낫기 때문에 일반적으로는 문제가 없다.

하지만 Observer에 상태를 조금 추가하면 관찰자가 스스로를 엮게 만들어 동적 할당 문제를 해결할 수 있다. 대상에 포인터 컬렉션을 따로 두지 않고, 관찰자 객체가 연결 리스트의 노드가 되는 것이다.

 

 

1.png

▲ 대상은 관찰자 연결 리스트를 포인터로 가리킨다.

 

이를 구현하려면, 먼저 Subject 클래스에 배열 대신 관찰자 연결 리스트의 첫째 노드를 가리키는 포인터를 둔다.

 

class Subject {

  Subject() : head_(NULL) {}

 

  // 메서드들... 

private:

  Observer* head_;

};

 

이제 Observer에 연결 리스트의 다음 관찰자를 가리키는 포인터를 추가한다.

class Observer {

  friend class Subject;

 

public:

  Observer() : next_(NULL) {}

 

  // 그 외... 

private:

  Observer* next_;

};

 

또한 Subjectfriend 클래스로 정의한다. Subject에는 관찰자를 추가, 삭제하기 위한 API가 있지만 Subject가 관리해야 할 관찰자 목록은 이제 Observer 클래스 안에 있다. Subject가 이들 목록에 접근할 수 있게 만드는 가장 간단한 방법은 Observer에서 Subjectfriend 클래스로 만드는 것이다. (C++에서는 A 클래스가 B 클래스를 friend로 지정하면 B에서 A의 private 영역까지 접근할 수 있다. – 옮긴이)

새로운 관찰자를 연결 리스트에 추가하기만 하면 대상에 등록할 수 있다. 앞쪽에 추가하는 방식이 간단하므로 그 방식으로 구현하겠다.

 

void Subject::addObserver(Observer* observer) { 

  observer->next_ = head_;

  head_ = observer;

}

 

연결 리스트 뒤쪽으로 추가할 수도 있지만 관찰자를 추가할 때마다 연결 리스트를 쭉 따라가면서 마지막 노드를 찾거나 마지막 노드를 따로 tail_ 포인터로 관리해야 하기 때문에 좀 더 복잡하다.

관찰자를 앞에서부터 추가하면 구현이 간단하지만, 전체 관찰자에 알림을 보낼 때는 맨 나중에 추가된 관찰자부터 맨 먼저 알림을 받는다는 부작용이 있다. 관찰자를 A, B, C 순서대로 추가했다면, C, B, A 순서대로 알림을 받게 된다.

이론상으로는 이래도 아무 문제가 없어야 한다. 원칙적으로 같은 대상을 관찰하는 관찰자끼리는 알림 순서로 인한 의존 관계가 없게 만들어야 한다. 순서 때문에 문제가 있다면 관찰자들 사이에 미묘한 커플링이 있다는 얘기이므로, 나중에 문제가 될 소지가 크다.

등록 취소 코드는 다음과 같다. (연결 리스트에서 노드를 삭제할 때는 첫 번째 노드를 삭제하는 특수한 경우 때문에 이 코드에서처럼 지저분한 예외 처리가 필요하다. 포 인터의 포인터를 사용하면 좀 더 우아하게 만들 수 있다. 포인터의 포인터를 사용한 코드를 본 사람 중에서 반 이상이 이해를 못 했기 때문에 여기에 싣지는 않겠다. 직접 구현해보면 포인터를 다시 보게 될 것이다(저자가 알려준 포인터의 포인터로 만든 코드는 다음 주소에서 볼 수 있다. https://git.io/vrCG2 - 옮긴이).

void Subject::removeObserver(Observer* observer) { 

  if (head_ == observer) {

    head_ = observer->next_; 

    observer->next_ = NULL; 

    return;

  }

 

  Observer* current = head_; 

  while (current != NULL) {

    if(current->next_ == observer) { 

      current->next_ = observer->next_; 

      observer->next_ = NULL;

      return;

    }

    current = current->next_;

  }

}

 

이건 단순 연결 리스트라서 노드를 제거하려면 연결 리스트를 순회해야 한다. 배열로 만들어도 마찬가지다. 이중 연결 리스트라면 모든 노드에 앞, 뒤 노드를 가리키는 포인터가 있기 때문에 상수 시간에 제거할 수 있다. 실제 코드였다면 이중 연결 리스트로 만들었을 것이다.

이제 알림만 보내면 된다. 단지 목록을 따라가기만 하면 된다. (전체 목록을 순회하면서 하나하나 알림을 보내기 때문에 모든 관찰자들이 동등하게, 서로 독립적으로 처리된다. 이걸 관찰자가 알림을 받았을 때 대상이 순회를 계속할지 말지를 알려주는 플래그를 반환하도록 약간 변형할 수도 있다. 이러면 GoF의 책임 연쇄 패턴에 가까워진다.)

 

void Subject::notify(const Entity& entity, Event event) { 

  Observer* observer = head_;

  while (observer != NULL) {

    observer->onNotify(entity, event); 

    observer = observer->next_;

  }

}

 

괜찮지 않나? 대상은 동적 메모리를 할당하지 않고도 얼마든지 관찰자를 등록할 수 있다. 추가, 삭제는 단순 배열로 만든 것과 다름없이 빠르다. 다만 사소한 기능 하나를 희생했다.

관찰자 객체 그 자체를 리스트 노드로 활용하기 때문에, 관찰자는 하나의 대상 관찰자 목록에만 등록할 수 있다. 다시 말해 관찰자는 한 번에 한 대상만 관찰할 수 있다. 좀 더 전통적인 구현 방식대로 대상마다 관찰자 목록이 따로 있다면 하나의 관찰자를 여러 대상에 등록할 수 있다.

한 대상에 여러 관찰자가 붙는 경우가 그 반대보다 훨씬 일반적이다 보니, 이런 한계를 감수하고 갈 수도 있다. 이게 문제가 된다면, 훨씬 복잡하기는 해도 여전히 동적 할당 없이 처리할 수 있는 방법이 있다. 전부 다 언급하려면 이번 장이 너무 길어지니 뼈대만 간단하게 소개하겠다. 나머지는 직접 고민해보자.

 

 

7.2.2. 리스트 노드 풀

전과 마찬가지로 대상이 관찰자 연결 리스트를 들고 있다. 다만, 이 연결 리스트의 노드는 관찰자 객체가 아니다. 대신 따로 간단한 ‘노드’를 만들어, 관찰자와 다음 노드를 포인터로 가리키게 한다. (연결 리스트는 보통 두 가지 방법으로 구현한다. 학교에서 배웠을 법한 첫 번째 방법에서는 노드 객체가 데이터를 관리한다. 방금 본 연결 리스트 예제에서는 반대다. 즉, 데이터(Observer)가 노드 정보(next_ 포인터)를 들고 있다. 이런 걸 ‘침습성(intrusive)’ 연결 리스트라고 부른다. 목록에 들어갈 객체에 연결 리스트 관련 기능을 끼워 넣기 때문이다. 침습성 리스트 는 유연함은 떨어지지만 효율이 더 좋기 때문에 리눅스 커널 같이 성능이 더 중요한 곳에서 많이 사용된다.)

 

2.png

▲ 대상과 관찰자를 가리키는 노드들의 연결 리스트

 

같은 관찰자를 여러 노드에서 가리킬 수 있다는 것은, 같은 관찰자를 동시에 여러 대상에 추가할 수 있다는 뜻이다. 다시 여러 대상을 한 번에 관찰할 수 있게 된 것이다.

동적 할당을 피하는 방법은 간단하다. 모든 노드가 같은 자료형에 같은 크기니까 객체 풀(19장)에 미리 할당하면 된다. 이러면 고정된 크기의 목록 노드를 확보할 수 있어서 필요할 때마다 동적 메모리 할당 없이 재사용할 수 있다.

 

 

남은 문제점들

관찰자 패턴을 꺼리게 하던 세 가지 우려는 충분히 해소되었다고 본다. 앞에서 본 것처럼 관찰자 패턴은 간단하고 빠르며 메모리 관리 측면에서도 깔끔하게 만들 수 있다. 그렇다면 항상 관찰자 패턴을 써야 하는 걸까?

그건 전혀 다른 얘기다. 다른 모든 디자인 패턴과 마찬가지로 관찰자 패턴 역시 만능은 아니다. 제대로 구현했다고 해도 올바른 해결책이 아닐 수 있다. 디자인 패턴의 평판이 나빠진 것은 사람들이 좋은 패턴을 상황에 맞지 않는 문제에 적용하는 바람에 문제가 더 심각해진 경우가 많아서다.

기술적인 문제와 유지보수 문제 두 개가 남아 있다. 유지보수보다는 기술적인 문제가 더 쉬운 법이니 이것부터 보자.

 

 

7.2.3. 대상과 관찰자 제거

지금까지 다룬 예제 코드는 잘 만들어져 있지만 대상이나 관찰자를 제거하면 어떻게 될 것인가라는 중요한 문제 하나를 빼먹고 있었다. 관찰자를 부주의하게 삭제하다 보면 대상에 있는 포인터가 이미 삭제된 객체를 가리킬 수 있다. 해제된 메모리를 가리키는 무효 포인터dangling pointer 에다가 알림을 보낸다면… 그날은 힘든 하루가 될 것이다. (따지려 드는 건 아니지만, 『GoF의 디자인 패턴』에서는 이 문제를 전혀 다루지 않는다(『GoF의 디자인 패턴』 387쪽에서 언급이 되기는 한다. - 옮긴이). )

보통은 관찰자가 대상을 참조하지 않게 구현하기 때문에 대상을 제거하기가 상대적으로 쉽다. 그래도 대상 객체를 삭제할 때 문제가 생길 여지는 있다. 대상이 삭제되면 더 이상 알림을 받을 수 없는데도 관찰자는 그런 줄 모르고 알림을 기다릴 수도 있다. 스스로를 관찰자라고 생각할 뿐, 대상에 추가되어 있지 않은 관찰자는 절대로 관찰자가 아니다.

대상이 죽었을 때 관찰자가 계속 기다리는 걸 막는 건 간단하다. 대상이 삭제되기 직전에 마지막으로 ‘사망’ 알림을 보내면 된다. 알림을 받은 관찰자는 필요한 작업(추모, 헌화, 조문 등…)을 알아서 하면 된다.

관찰자는 제거하기가 더 어렵다. 대상이 관찰자를 포인터로 알고 있기 때문이다. 해결 방법이 몇 가지 있다. 가장 쉬운 방법은 관찰자가 삭제될 때 스스로를 등록 취소하는 것이다. 관찰자는 보통 관찰 중인 대상을 알고 있으므로 소멸자에서 대상의 removeObserver( )만 호출하면 된다. (흔히 그렇듯이, 뭔가를 하는 건 어렵지 않다. 할 일을 기억하기가 어렵다.)

사람은 누구나 실수를 한다. 심지어 회사에서 오랫동안 컴퓨터로 작업하면서 컴퓨터의 정확함이 몸에 밴 우리 프로그래머들도 예외는 아니다. 인간이 자주 하는 실수를 컴퓨터는 하지 않는다는 점, 바로 이게 컴퓨터를 발명한 이유다.

실수를 막는 더 안전한 방법은 관찰자가 제거될 때 자동으로 모든 대상으로부터 등록 취소하게 만드는 것이다. 상위 관찰자 클래스에 등록 취소 코드를 구현해놓으면 이를 상속받는 모든 클래스는 등록 취소에 대해 더 이상 고민하지 않아도 된다. 다만 두 방법 모두 관찰자가 자기가 관찰 중인 대상들의 목록을 관리해야 하기 때문에 상호참조가 생겨 복잡성이 늘어나는 단점이 있다.

 

 

7.2.4. GC가 있는데 무슨 걱정이람

이쯤 되면 가비지 컬렉터garbage collector(GC)가 있는 최신 언어로 개발하는 독자들은 으쓱하고 있을지 모르겠다. 아무것도 명시적으로 삭제하지 않아도 된다고 해서 이런 걸 고민할 필요가 없을까? 정말?

캐릭터 체력 같은 상태를 보여주는 UI 화면을 생각해보자. 유저가 상태창을 열면 상태창 UI 객체를 생성한다. 상태창을 닫으면 UI 객체를 따로 삭제하지 않고 GC가 알아서 정리하게 한다.

캐릭터는 얼굴이든 어디든 얻어맞을 때마다 알림을 보낸다. 캐릭터를 관찰하던 UI 창은 알림을 받아 체력바를 갱신한다. 여기까진 좋다. 이제 유저가 상태창을 닫을 때 관찰자를 등록 취소하지 않는다면 어떻게 될까?

UI는 더 이상 보이지 않지만 캐릭터의 관찰자 목록에서 여전히 상태창 UI을 참조하고 있기 때문에 GC가 수거해 가지 않는다. 상태창을 열 때마다 상태창 인스턴스를 새로 만들어 관찰자 목록에 추가하기 때문에 관찰자 목록은 점점 커진다.

캐릭터는 뛰어다니거나 전투하는 동안 계속해서 이 모든 상태창 객체에 알림을 보낸다. 상태창은 더 이상 화면에 없지만 알림을 받을 때마다 눈에 보이지도 않는 UI 요소를 업데이트하느라 CPU 클럭을 낭비한다. 상태창에서 효과음이라도 난다면, 연속해서 같은 효과음이 나는 걸 듣고서 뭔가 이상하다는 걸 눈치챌 것이다.

이는 알림 시스템에서 굉장히 자주 일어나는 문제다 보니 사라진 리스너 문제lapsed listener problem 라는 고유한 이름이 붙었을 정도다. (얼마나 중요한지 심지어 위키피디아에 따로 페이지가 있을 정도다.) 대상이 리스너 레퍼런스를 유지하기 때문에, 메모리에 남아 있는 좀비 UI 객체가 생긴다. 이래서 등록 취소는 주의해야 한다.

 

 

7.3. 무슨 일이 벌어진 거야?

더 어려운 문제는 관찰자 패턴의 원래 목적 때문에 생긴다. 관찰자 패턴을 사용하는 이유는 두 코드 간의 결합을 최소화하기 위해서다. 덕분에 대상은 다른 관찰자와 정적으로 묶이지 않고도 간접적인 상호작용을 할 수 있다.

대상이 어떻게 동작하는지 이해하기 위해 코드를 볼 때 잡다한 코드가 없으면 집중하기에 좋다. 물리 엔진 코드를 열었는데 편집기(혹은 우리의 정신)에 업적 관련 코드가 흩어져 있는 꼴은 보고 싶지 않다.

반대로 말하면, 프로그램이 제대로 동작하지 않을 때 버그가 여러 관찰자에 퍼져 있다면 상호작용 흐름을 추론하기가 훨씬 어렵다. 코드가 명시적으로 커플링되어 있으면 어떤 메서드가 호출되는지만 보면 된다. 이런 건 요즘 IDE에서는 식은 죽 먹기다.

하지만 관찰자 목록을 통해 코드가 커플링되어 있다면 실제로 어떤 관찰자가 알림을 받는지는 런타임에서 확인해보는 수밖에 없다. 프로그램에서 코드가 어떻게 상호작용하는지를 정적으로는 알 수 없고, 명령 실행 과정을 동적으로 추론해야 한다.

이 문제에 대해서 간단히 조언하자면 이렇다. 코드를 이해하기 위해 양쪽 코드의 상호작용을 같이 확인해야 할 일이 많다면, 관찰자 패턴 대신 두 코드를 더 명시적으로 연결하는 게 낫다.

큰 프로그램을 작업하다 보면 다 같이 작업해야 하는 덩어리들이 있기 마련이다. ‘관심의 분리separation of concerns’, ‘응집력coherence and cohesion’, ‘모듈’ 같은 용어로 부르기도 하는데, 결국은 ‘이것들은 같이 있어야 하고, 다른 것과는 섞이면 안 된다’는 얘기다.

관찰자 패턴은 서로 연관 없는 코드 덩어리들이 하나의 큰 덩어리가 되지 않으면서 서로 상호작용하기에 좋은 방법이지, 하나의 기능을 구현하기 위한 코드 덩어리 안에서는 그다지 유용하지 않다.

그래서 예제에서도 업적과 물리같이 서로 전혀 상관없는 분야를 선택했다. 이런 때엔 각 담당자가 다른 분야를 잘 몰라도 작업에 문제가 없도록 상호작용을 최소한으로 유지하는 게 좋다.

 

 

7.4. 오늘날의 관찰자

『GoF의 디자인 패턴』은 1994년에 출간되었다. (같은 해에 에이스 오브 베이스 같은 밴드가 싱글을 세 장이나 히트시켰다는 걸 보면 당시 우리의 수준이 어땠는지 대략 짐작할 수 있다.) 그 당시에는 객체지향 프로그래밍이 최신 기법이었다. 모든 프로그래머가 ‘한 달 안에 OOP 배우기’를 원했고, 관리자는 클래스를 몇 개나 만들었느냐에 따라 프로그래머를 평가했다. 프로그래머는 클래스 상속 구조의 깊이와 자신의 실력을 동일시했다.

그런 시절에 관찰자 패턴이 알려지다 보니 당연히 클래스에 많이 의존하게 되었다. 하지만 요즘 프로그래머들은 이전보다는 함수형 언어에 더 익숙하다. 알림 하나 받겠다고 인터페이스를 상속받는 건 요즘 기준으로는 아름답지 않다.

이런 방식은 무겁고 융통성 없어 보이고, 실제로도 무겁고 융통성이 없다. 한 클래스가 대상 인스턴스별 알림 메서드를 다르게 정의할 수 없다는 점만 해도 그렇다. (이래서 일반적으로 관찰자 알림 함수 매개변수에 대상 객체가 들어 있다. 관찰자에 onNotify 메서드가 하나뿐이다 보니, 여러 대상을 관찰할 때는 알림을 보낸 대상을 알기 위해 대상 객체가 필요하다.)

좀 더 최신 방식은 메서드나 함수 레퍼런스만으로 ‘관찰자’를 만드는 것이다. 일급 함수, 그중에서도 클로저를 지원하는 언어에서는 이렇게 관찰자를 만드는 게 훨씬 일반적이다. (최근에는 거의 모든 언어에 클로저가 있다. C++은 GC 없이도 람다 기능을 통해서 클로저를 지원하기 시작했고, 심지어 자바도 정신을 차렸는지 JDK 8부터 람다가 추가되었다.)

예를 들어 C#에는 언어 자체에 event가 있어서 메서드를 참조하는 delegate으로 관찰자를 등록할 수 있다. 자바스크립트의 이벤트 시스템에서는 EventListener 프로토콜을 지원하는 객체가 관찰자가 되는데, 이것 역시 함수로 할 수 있고, 다들 그렇게 쓰고 있다.

관찰자 패턴을 내가 다시 고안한다면 클래스보다는 함수형 방식으로 만들 것이다. C++에서도 Observer 인터페이스를 상속받은 인스턴스보다는 멤버 함수 포인터를 관찰자로 등록하게 만들었을 것이다.

 

 

7.5. 미래의 관찰자

이벤트 시스템이나 다른 유사 관찰자 패턴들이 이제는 너무 흔하고 정형화되어 있다. 하지만 관찰자 패턴을 이용해 대규모 프로그램을 만들다 보면 관찰자 패턴 관련 코드 중에서 많은 부분이 결국에는 다음과 같은 공통점이 있다는 걸 알게 된다.

  1. 어떤 상태가 변했다는 알림을 받는다.
  2. 이를 반영하기 위해 UI 상태 일부를 바꾼다.

‘체력이 7이라고? 그럼 체력바 너비를 70픽셀로 바꿀게’라고 하는 식이다. 하다 보면 상당히 지겹다. 컴퓨터 과학자들과 소프트웨어 엔지니어들은 이런 지루함을 제거하기 위해 오랫동안 노력해왔다. 이런 노력은 ‘데이터 흐름 프로그래밍dataflow programming(언리얼 엔진의 블루프린트나 유니티의 메카님을 생각하면 된다. – 옮긴이)이나 ‘함수형 반응형 프로그래밍functional reactive programming(http://goo.gl/wkerWw 등을 참고 - 옮긴이)과 같은 결과로 나타났다.

이런 방식은 사운드 프로세싱이나 칩 설계 같은 제한적인 분야에서나마 어느 정도 성공을 거뒀지만, 아직까지 성배는 찾지 못했다. 그 사이 약간 보수적인 접근법이 인기를 얻고 있다. 요즘 나오는 많은 애플리케이션 프레임워크에서는 ‘데이터 바인딩’을 지원한다.

다른 급진적인 방식과는 달리, 데이터 바인딩은 순차 프로그래밍 코드를 전부 제거하려 들거나 전체 코드를 거대한 선언식 데이터 흐름 그래프로 만들려고 하지 않는다. 대신 어떤 값이 변경되면 관련된 UI 요소나 속성을 바꿔줘야 하는 귀찮은 작업을 알아서 해준다.

다른 선언형 시스템과 마찬가지로, 데이터 바인딩도 게임의 핵심 코드에 적용하기에는 너무 느리고 복잡하다. 하지만 UI같이 게임에서 성능에 덜 민감한 분야에서는 분명 데이터 바인딩이 대세가 될 것이다.

기존 방식의 관찰자 패턴 역시 충분히 훌륭하기 때문에 계속 사용될 것이다. 이름에 ‘함수형’과 ‘반응형’을 같이 붙여놓은 최신 방식보다는 덜 흥미롭겠지만 여전히 단순하고 잘 동작한다. ‘단순’하고 ‘잘 동작한다’는 것은 내가 해결책을 고를 때 가장 중요하게 보는 기준이다.

이전 글 : 디자인 패턴 다시 보기 : 관찰자 패턴(1/2)

다음 글 : 다음글이 없습니다.

댓글 입력