[ Programming > Design Pattern ]
[디자인 패턴] Observer 패턴(feat. React Query)
옵저버 패턴
옵저버 패턴은 책임 분산을 위한 행위 패턴의 한 종류이고, 주로 분산 이벤트 처리 시스템을 구현하는데 사용한다.
주로 어떤 Subject의 상태 변화에 따라 Observer가 행위를 취하는 패턴을 구현하는데 사용된다 그래서 관찰 당하는 subject와 다수의 관찰하는 observer로 구성된다.(1:n 관계)
즉, 단일 피관찰자(subject)의 상태 변화에 따라 다수의 관찰자(observer)가 액션(행동, 행위)가 요구될 때 사용하는 패턴이다.
그리고 이 옵저버 패턴(Observer Pattern)을 확장한 디자인 패턴으로 발행-구독 패턴(Pub-Sub Pattern)이 있다. 옵저버 패턴이 오늘의 주인공이기 때문에 간단한 차이를 설명하면 pub-sub 패턴은 별도의 이벤트 채널로 publisher가 발행을 하면 subscriber가 구독 상태일때 처리하지만, observer 패턴은 별도의 이벤트 채널(중간 매개) 없이 발행자(Publisher)가 발행(or 상태 변화)를 하면 바로 구독자(Observer, Subscriber)가 처리한다는 것이다. 이제 본격적으로 옵저버 패턴에 대해 알아보자.
1. 옵저버 패턴 특징과 설명
옵저버 패턴을 설명하려면 먼저 옵저버 패턴 적용 전 구조와 적용 후 구조를 보고 뭐가 달라졌는지 비교하고 확인해야한다.

위 그림은 홈페이지의 언어 설정을 변경했을때, 가격 출력기로 언어에 맞는 국가의 화폐단위와 소수점 처리 방식, 환율을 적용한 가격을 출력해주는 코드의 다이어그램이다. 그래서 이 기능 구현을 위해 홈페이지 언어 관리 객체에서 언어가 변경될 때 가격 계산기와 가격 형식 관리기의 상태를 변경해줘서 가격 출력기에 변경된 상태에 따라 출력해주도록 만들었다.
그런데 문제점은 홈페이지 언어 관리기는 도메인적으로 관련 없어 보이는 가격 계산기과 가격 형식 관리기를 내부 구성으로 가지고 있다. 심지어 가격 계산기와 형식 관리기의 국가 변경() 구현체를 사용해야 한다. 그래서 언어 관리기는 알기 싫은 객체와 의존성이 생겨 강한 결합도를 가지게 된다. 그러면 이를 해결하기 위해서는 어떻게 해야 할까?? 아래와 같이 바꿔봤다.

이제 각 객체의 Class와 그 구현에 의존성이 없어졌다. 인터페이스에 의존한 구현으로만 상태 변경에 따른 행위를 할 수 있다. 그래서 언어 관리기는 어떤 객체인지는 모르겠지만, subscribe한 옵저버는 observers에 추가하고 상태 변경시 일괄 적으로 observer 인터페이스의 update함수를 실행하도록 하여 알아서 각 클래스 구현에 맞게 작동되도록 변경됬다. 언어 관리기는 더이상 어떤 객체가 뭘 하는지 상관 없이 상태 변경에 따른 행위 유발을 할 수 있게되었다. 그래서 결합도가 낮아졌고 더 객체 지향적인 설계가 observer 패턴 적용으로 완성되었다.
2. React Query의 옵저버 패턴
옵저버 패턴이 프론트엔드에서 쓰인 예시로 Tanstack Query의 React 버전을 가져왔다. 아래 다이어그램에서 useExternalStore과 그 오른쪽 부분은 사실 없어도 되지만 어떻게 작동되는지 설명하려고 추가했다.(실제로 React Query 구현은 좀 더 복잡합니다!)

queryObserver와 Query가 어떤 순서로 React의 화면까지 리렌더링할 수 있도록 유발하는지 설명해보겠다.(실제로 약간 더 복잡!)
useQuery 훅을 사용하면 Query와 QueryObserver 객체가 생성된다.
queryObserver는 queryKey에 맞는 Query를 subscribe 함수로 구독하게 된다
QueryObserver.subscribe(Query)는 Query.addObserver(queryObserver or this)로 구독자를 추가한다.
Query의 상태 변화가 발생하면 모든 observer의 observer.onQueryUpdate()를 실행한다.
queryObserver.onQueryUpdate() 함수는 externalStore에 리렌더링을 요청하여 React 상태와 화면에 바뀐 값을 반영한다.
위 과정에서 Query와 QueryObserver는 서로 옵저버 관계임을 알 수 있다. Query의 상태 변화에 따라 Query는 내부 구현이 어떻게 되었는지 전혀 모르는 QueryObserver의 내부 구현된 함수를 실행하게 되어 느슨한 결합력을 가진다. 또한 useQuery훅을 여러개 사용하면 여러 QueryObserver가 생기면서도 같은 QueryKey를 가진 Query를 구독하기 때문에 1:n 관계를 가진다.
마무리
옵저버 패턴은 서로 상호작용 하는 객체 사이의 결합을 느슨하게 해주는 디자인 패턴이다. 예시들을 보면 인터페이스에만 맞춰 구현하고 객체간 내부 구현에 신경쓰지 않으며, 의존성이 줄어들어 객체 사이의 상호 의존성을 최소화한다. 더 길게 설명하면 옵저버는 Observerable이라는 특정 인터페이스만 구현하여 구독 가능하게 만들기만 하면 되고, 어떤 옵저버를 추가하고 싶으면 Observerable 인터페이스를 구현하는 객체를 추가하면 되며, 어떤 옵저버의 내부 구현을 변경하려고 해도 굳이 Subject를 변경할 필요가 없고, 옵저버는 서로 독립적이며, 옵저버는 다른 Subject를 구독할 수도 있다. 이렇듯 옵저버 패턴은 어떤 객체의 상태변화에 따라 연관된 다른 객체의 상호 작용이 필요할 경우 사용하면 더 객체 지향적 코드를 작성할 수 있다!!