[디자인 패턴] 9. Observer 패턴

김주희's avatar
Jul 24, 2025
[디자인 패턴] 9. Observer 패턴

Observer 패턴

1. Listener VS Observer

Observe는 관찰하다라는 뜻으로 Observer는 지켜보고 있는 관찰자를 의미한다. 그러면 리스너는 뭘까. 리스너 또한 관찰자인데 리스너는 행위가 추가된다. 계속 지켜보고 있다가 행위를 하는 것은 리스너이고 계속 보고만 있는 것이 옵저버이다.
이때 리스너의 특징은 동기적이다는 것이다. 예를 들어 아래의 예시와 같이 버튼에 클릭 이벤트 리스너를 추가한 경우 버튼을 클릭하면 바로 동작할 행동을 정의하여 버튼을 클릭하면 바로 행동을 한다. 이를 동기적이라고 한다.
// 1. 버튼 요소 가져오기 const button = document.getElementById('myButton'); // 2. 클릭 이벤트 리스너 추가 button.addEventListener('click', function () { alert('버튼이 클릭되었습니다!'); });
 
반면 옵저버 패턴의 경우 비동기적으로 만들 수 있는데 비동기는 일의 순서가 없다는 것이다. 버튼 클릭시 리스너처럼 바로 동작하지 않고 예를 들면 사과 5개 입고되면 알려줘 했는데 사과 5개 아직 들어오지 않았기 때문에 바로 응답 못한다고 하자. 사과 5개가 들어오면 응답이 오도록 동기적으로 만들게 될 경우 이 시스템은 사과 5개가 입고될 때까지 아무것도 안되고 그냥 뻗게 된다. 여기서 지금 CPU가 block 당해있기 때문이다. 사과 5개가 들어왔는지 관찰하고 있느라 그 시간동안 block 당해서 계속 멍 때리고만 있는 것이다.
 
이번에는 A와 B가 있고 A가 먼저 사과를 달라고 요청한다. 확인해보니 현재 사과가 없어서 일단 할 일 하면서 기다려 라고 응답하고 가만히 기다린다. 그 사이에 B가 딸기를 달라고 요청이 들어오고 사과와 마찬가지로 딸기도 없어서 일단 기다려라고 응답한다. 그리고 나서 나는 이제 사과와 딸기를 타임 슬라이싱 하면서 입고됐는지, 누가 또 요청을 하는지 지켜본다. 이 일을 계속하고 있으면 일은 동기적으로 끝나지 않는다. 딸기가 먼저 입고되었을 경우 B가 먼저 응답을 받을 수도 있고 이게 바로 일의 순서가 없는 비동기 전략이다.
 
따라서 옵저버 패턴에서는 사과 5개 줘!가 아닌 사과 5개 들어오면 알려줘! 하고 가는 것이다. 즉 subscribe, 구독하는 것이다. publish는 사과가 5개 채워지는 것이고, 가만히 있다가, 보다가, 사과가 1개가 들어왔을 때는 응답하지 않고 기다리다가, 5개가 들어오면 구독자에게 알림이 간다.
 

+ BackPressure

BackPressure는 Consumer와 Producer의 처리 속도 차이로 인해 데이터가 계속 쌓이거나 손실/누락이 발생할 수 있고 이 문제를 해결하기 위해 생산 속도를 늦추거나, 소비자가 준비될 때까지 대기하거나 등의 방식으로 압력을 조절하는 것이다.
 

2. 데이터 변경을 구독자에게 알리는 2가지 방식

이제 Polling 방식과 Push 방식 두 가지의 예시를 살펴보자.

1. Polling 방식

먼저 Polling 방식은 소비자가 직접 요청해서 상태를 감시하는, 공급자는 데이터를 직접 알리지 않는 방식이다. 예를 들면 소비자가 계속해서 있는지 물어보고 없다고 대답하면 다시 또 물어보고를 반복해서 있다고 대답할때까지 계속 요청하는 방식이다.
 
아래의 예시의 목표는 value에 값이 들어오면 알림을 주는 것이다.
 
LotteMart 클래스는 value라는 상태를 가지고 있고 value의 기본값은 null이라고 하자. 그리고 이 value는 private으로 지정해뒀기 때문에 getter를 통해서 조회가능하다. received 메서드 책임은 상품이 입고가 되었다고 상태를 변경하는 것이다.
package ex08.polling; // 목표 : value에 값이 들어오면 알림을 주기 public class LotteMart { private String value = null; public String getValue() { return value; } // 상품 입고 public void received() { for (int i = 0; i < 5; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } value = "상품"; } }
 

1. 동기 처리

아래의 코드를 보면 마트는 received 메서드를 통해 입고 준비를 하게 되고 5초 뒤 상품이 입고된다. 그리고 그 다음 코드에서 getter를 통해 value 값을 확인하게 된다. 지금의 코드는 동기 방식이기 때문에 received 메서드가 다 끝나고 나서 다음 줄이 실행되므로 받은 값: 상품이 출력된다.
package ex08.polling; public class App { public static void main(String[] args) { LotteMart lotteMart = new LotteMart(); Customer1 customer1 = new Customer1(); // 1. 마트는 입고 준비 lotteMart.received(); // 5초 // 2. 입고 확인 String value = lotteMart.getValue(); System.out.println("받은 값 : " + value); } }
 

2. 비동기 처리 (1)

이번에는 마트가 입고 준비를 할때 main 스레드가 아닌 새로운 백그라운드 스레드에서 received 메서드가 실행되고 main 스레드는 아래로 바로 내려가게 되면서 received 메서드가 value의 상태를 변경하기 전에 getter가 호출되면서 value == null인 상태로 출력된다.
notion image
package ex08.polling; public class App { public static void main(String[] args) { LotteMart lotteMart = new LotteMart(); Customer1 customer1 = new Customer1(); // 1. 마트는 입고 준비 new Thread(() -> { lotteMart.received(); // 5초 }).start(); // 2. 입고 확인 String value = lotteMart.getValue(); System.out.println("받은 값 : " + value); } }
 

3. 비동기 처리 (2) - 최종 Polling 방식의 Observer 패턴 구조

우선 Customer1 클래스는 전달받은 메세지를 출력하는 책임을 갖는 update 메서드를 갖는다.
package ex08.polling; public class Customer1 { public void update(String msg) { System.out.println("손님1이 받은 알림: " + msg); } }
 
그리고 마트의 입고 준비는 비동기 처리 (1)과 똑같이 main 스레드가 아닌 새로운 스레드에서 실행된다. 그리고 입고 확인이 비동기 처리 (1)과는 달리 while문을 계속 도는 데몬 상태이다. 즉 실제 Polling 방식으로 getValue 메서드를 통해 value가 null이 아닌지 계속 확인(요청)하고 null이 아니면 customer에게 상품이 들어왔다고 메세지를 전달하고 끝이 난다.
 
지금 아래의 코드를 보면 계속 상태를 체크하기 전에 Thread.sleep(100)이 실행되는데 이를 통해 메인 스레드가 value가 바뀌었지 계속 확인하는 게 아니라 100ms마다 value를 확인하게 된다. while문이 계속 돌면서 CPU가 독점된 상태였는데 Thread.sleep을 통해 잠깐 점유권을 뺏어갈 수 있도록 기회를 주는 것이다. 계속 while(true)로 돌게 되면 CPU 낭비가 크기 때문에 잠깐 기다렸다가 확인하는 방식을 통해 선점형 스케줄링(운영체제가 원하면 언제든지 CPU를 선점해서 다른 작업에 할당할 수 있는 방식) 환경에서도 자원을 아낄 수 있다.
polling에서 중요한 것은 요청 횟수, 정확하게는 요청 시간을 얼마마다 가질 것인지이다. 요청이 너무 많으면 서버의 부하가 심해지기 때문에 적당히 요청해야 하는데 적당히는 비지니스에 따라 달라진다. 예를 들면 인기 상품을 내가 무조건 사고 싶으면 최대한 자주 요청을 해서 확인을 해야하고 비인기 상품이면 하루에 한 번 요청을 해서 확인하더라도 살 수 있을 것이다. 그러므로 요청 횟수는 비지니스에 따라 결정하면 된다.
 
package ex08.polling; public class App { public static void main(String[] args) { LotteMart lotteMart = new LotteMart(); Customer1 customer1 = new Customer1(); // 1. 마트는 입고 준비 new Thread(() -> { lotteMart.received(); // 5초 }).start(); // 2. 입고 확인 (데몬) while (true) { try { Thread.sleep(100); // 점유권 ? (선점/비선점) } catch (InterruptedException e) { throw new RuntimeException(e); } if (lotteMart.getValue() != null) { // request (폴링) customer1.update(lotteMart.getValue() + "이 들어왔습니다."); break; } else { System.out.println("상품이 아직 들어오지 않았어요!"); } } } }
 
 
notion image
 
notion image
 

2. Push 방식

Push 방식은 사과가 들어오면 알려줘! 하고 간 고객이 구독자가 되고 사과가 입고되면 사과를 구독한 고객을 찾아서 알림을 주는 방식이다. 즉 통신선(연결)이 유지되고 있다.
 
Push 방식 예제 코드에서는 우선 pub와 sub로 나뉘게 되는데 우선 상품이 입고되기를 기다리는 Customer가 subscriber 되고 상품이 입고되면 고객에게 알려주는 Mart가 publisher가 된다.
 
Mart는 LotterMart와 EMart라는 구체적인 것의 추상화로
 
notion image
 
현재 자바에서 push 서버를 만들 때 publisher와 subscriber 클래스를 만들고 onSubscribe, onNext 등의 메서드로 만들어서 구현해라는 프로토콜이 존재한다. 나는 지금 그 메서드에 대해 공부하기 보다 직접 구현하여 push 방식의 원리에 이해하고자 한다.
 
 
 
 
Runnable에서 run 메서드 = 스레드의 메인 메서드와 같다 메인 스레드는 종료되고 첫번째 스레드는 물건 다 받으면 notify 두번째 스레드도 물건 다 받으면 notify 롯데마트는 cus1한테 알림 이마트는 cus1, cus2한테 알림
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Webflux, nodejs 로 서버 만들면 비동기?
 
 
notion image
notion image
 
 
 
 
 

2. push

알림을 받고 나서 뭘 할지를 구현해야하는 거긴 한데 일단 push에 대해서 알고 나서..?
 
지금부터 배우는건 push의 기본
 
 
 
나 롯데마트임. 고객 3명이 구독요청옴 구독 요청이 온 고객들 collection(배열은 사이즈 몰라서 위험)에 저장할거임
 
notion image
Share article

jay0628