18. 똑똑한 코드 패턴 (디자인 패턴)/18.4 옵저버 패턴

18.4.1 옵저버 패턴이란? - 서로를 바라보는 따뜻한 마음들

thejavascript4kids 2025. 7. 28. 04:53

📘 18.4.1 옵저버 패턴이란? - 서로를 바라보는 따뜻한 마음들

안녕하세요, 여러분. 혹시 가을날 창가에 앉아 하늘을 바라본 적이 있나요? 구름이 천천히 움직이는 모습을 지켜보다가, 문득 우리도 누군가를 지켜보고 있고, 또 다른 누군가는 우리를 바라보고 있다는 생각이 들 때가 있어요. 오늘 배울 '옵저버 패턴'은 바로 그런 따뜻한 시선들에 관한 이야기예요. 프로그래밍 세계에서도 이런 아름다운 관계를 만들 수 있다니, 정말 신기하지 않나요?

🧠 새로운 단어들과 친해지기

코딩의 세계로 들어가기 전에, 우리가 오늘 배울 특별한 용어들과 먼저 친구가 되어 보겠습니다.

단어 쉬운 설명
옵저버 패턴 한 친구가 변하면 그것을 지켜보던 다른 친구들에게 자동으로 알려주는 따뜻한 방법입니다.
주체(Subject) 변화가 일어나는 중심이 되는 친구로, 다른 친구들에게 소식을 전해주는 역할을 합니다.
옵저버(Observer) 주체를 지켜보다가 변화가 생기면 반응하는 친구들입니다.
구독하기 옵저버가 주체에게 "나에게도 소식을 알려줘!"라고 요청하는 것입니다.
알림 보내기 주체가 모든 옵저버들에게 "변화가 생겼어요!"라고 소식을 전하는 것입니다.

✨ 옵저버 패턴의 핵심 개념

옵저버 패턴은 마치 따뜻한 동네 우체국과 같습니다. 우체국에서는 중요한 소식이 생기면 그 소식을 기다리고 있는 모든 사람들에게 정성스럽게 알려주죠. 프로그래밍에서도 이와 같은 마음으로 일이 일어날 수 있어요.

예를 들어, 우리가 만든 게임에서 플레이어의 점수가 변할 때마다 화면의 점수 표시, 순위표, 업적 시스템 등 여러 부분이 모두 업데이트되어야 한다고 생각해보세요. 옵저버 패턴을 사용하면 점수가 변할 때마다 자동으로 모든 관련된 부분들이 업데이트됩니다.

가장 아름다운 점은 주체와 옵저버가 서로를 자세히 알 필요가 없다는 것입니다. 우체국은 편지를 받을 사람들이 어떤 사람인지, 어떤 일을 하는지 알 필요 없이 그저 소식만 전해주면 되죠. 편지를 받는 사람들도 우체국이 어떻게 편지를 분류하는지 알 필요 없이 소식만 받으면 되고요.

비유로 이해하기: 마을의 종소리

옵저버 패턴을 더 쉽게 이해하기 위해 '마을의 종소리' 이야기를 들려드릴게요.

옛날 어떤 작은 마을에는 마을 종각의 할아버지가 계셨습니다. 이 할아버지는 마을의 모든 중요한 소식을 가장 먼저 알게 되는 역할을 하셨어요. 아침이 되거나, 점심시간이 되거나, 중요한 공지사항이 있을 때마다 종각 할아버지가 가장 먼저 알게 되셨죠.

마을의 사람들은 "저희도 소식을 알려주세요!"라고 종각 할아버지께 요청할 수 있었어요. 농부는 일할 시간을, 상인은 장터 소식을, 아이들은 놀 시간을 알고 싶어했죠.

종각 할아버지는 각 사람이 원하는 소식을 마음에 새겨두셨다가, 관련된 소식이 생기면 종을 쳐서 해당하는 모든 사람들에게 동시에 알려주셨어요.

이 마을의 가장 아름다운 점은 종각 할아버지가 각 사람의 자세한 사정을 알 필요가 없다는 것이었어요. 그저 "아침 시간에 관심 있는 사람들"이라고만 알면 되었죠. 마을 사람들도 종각 할아버지가 어떻게 소식을 알아내시는지 궁금해할 필요가 없었고요.

우리가 배울 옵저버 패턴도 이 마을의 종소리와 같은 따뜻한 마음으로 동작한답니다.

🎯 옵저버 패턴을 사용하는 이유

그렇다면 우리는 왜 이런 특별한 알림 시스템을 사용해야 할까요? 여러 가지 좋은 이유들이 있어요.

첫 번째 이유는 코드를 아름답게 정리할 수 있기 때문입니다. 만약 옵저버 패턴이 없다면, 점수가 변할 때마다 화면 업데이트, 순위표 업데이트, 업적 확인 등의 코드를 모두 한 곳에 써야 할 거예요. 하지만 옵저버 패턴을 사용하면 각각의 일을 담당하는 부분들이 독립적으로 동작할 수 있습니다.

두 번째는 새로운 기능을 쉽게 추가할 수 있기 때문입니다. 나중에 "점수가 변할 때 효과음도 재생하고 싶어"라고 생각한다면, 기존 코드를 건드리지 않고도 새로운 옵저버만 추가하면 됩니다.

세 번째는 실제 세상과 닮아있기 때문입니다. 우리가 실제로 유튜브 구독, SNS 팔로우, 뉴스 알림 등을 받는 것처럼, 프로그램도 이런 방식으로 동작하면 이해하기 쉬워집니다.

마지막으로는 각 부분이 자유롭다는 점입니다. 화면 업데이트 부분에 문제가 생겨도 순위표 업데이트는 정상적으로 동작할 수 있어요.

⚙️ 옵저버 패턴의 기본 구조

옵저버 패턴을 만들기 위해서는 몇 가지 중요한 부분들이 필요해요.

1. 주체(Subject) 만들기 - 소식을 만들어내고 전달하는 역할

2. 옵저버(Observer) 만들기 - 주체로부터 소식을 받아서 자신만의 특별한 일을 하는 역할

3. 구독 시스템 만들기 - 옵저버가 주체에게 "나도 알림 받고 싶어!"라고 말할 수 있는 방법

4. 알림 시스템 만들기 - 주체가 모든 옵저버들에게 동시에 소식을 전달할 수 있는 방법

🧪 직접 해보면서 배우기

이제 실제로 옵저버 패턴을 사용한 따뜻한 예제들을 만들어 보겠습니다.

🔹 예제 1: 우리 학교 급식 알림 시스템

첫 번째 예제에서는 우리 학교의 급식 메뉴가 바뀔 때마다 관심 있는 친구들에게 알려주는 시스템을 만들어 보겠습니다.

// 급식 알림 시스템 - 소식을 전하는 주체
class SchoolLunch {
  constructor() {
    // 알림 받고 싶어하는 친구들 명단을 저장하는 배열
    this.observers = [];
    // 오늘의 메뉴를 저장하는 변수
    this.todayMenu = "";
  }

  // 친구가 알림 신청하기
  subscribe(friend) {
    // 이미 명단에 있는지 확인하기
    if (!this.observers.includes(friend)) {
      // 새로운 친구를 명단에 추가하기
      this.observers.push(friend);
      console.log(`${friend.name}님이 급식 알림을 신청했어요!`);
    }
  }

  // 친구가 알림 취소하기
  unsubscribe(friend) {
    // 명단에서 친구의 위치 찾기
    const index = this.observers.indexOf(friend);
    if (index !== -1) {
      // 친구를 명단에서 제거하기
      this.observers.splice(index, 1);
      console.log(`${friend.name}님이 급식 알림을 취소했어요.`);
    }
  }

  // 모든 친구들에게 알림 보내기
  notifyAllFriends() {
    console.log("\n📢 급식 알림을 전송 중이에요...\n");
    // 명단에 있는 모든 친구들에게 차례대로 알림 보내기
    for (const friend of this.observers) {
      friend.receiveMenuUpdate(this.todayMenu);
    }
  }

  // 오늘의 메뉴 업데이트하기
  updateMenu(newMenu) {
    console.log(`🍽️ 급식 메뉴가 변경되었어요: ${newMenu}`);
    // 새로운 메뉴를 저장하기
    this.todayMenu = newMenu;
    // 모든 친구들에게 알려주기
    this.notifyAllFriends();
  }
}

// 급식 알림을 받는 친구들
class Student {
  constructor(name, favoriteFood) {
    // 친구의 이름 저장하기
    this.name = name;
    // 좋아하는 음식 저장하기
    this.favoriteFood = favoriteFood || "모든 음식";
  }

  // 급식 메뉴 알림을 받았을 때 반응하기
  receiveMenuUpdate(menu) {
    console.log(`${this.name}: "${menu}" 메뉴 알림을 받았어요!`);

    // 좋아하는 음식이 포함되어 있는지 확인하기
    if (menu.includes(this.favoriteFood)) {
      console.log(`${this.name}: 와! 제가 좋아하는 ${this.favoriteFood}가 있네요! 😊`);
    } else {
      console.log(`${this.name}: 오늘도 맛있게 먹을게요! 😋`);
    }
  }
}

// 급식 알림 시스템 사용해보기
const schoolLunch = new SchoolLunch();

// 우리 반 친구들 만들기
const mingsu = new Student("민수", "치킨");
const jiwon = new Student("지원", "피자");
const soobin = new Student("수빈", "떡볶이");

// 친구들이 급식 알림 신청하기
schoolLunch.subscribe(mingsu);
schoolLunch.subscribe(jiwon);
schoolLunch.subscribe(soobin);

// 오늘의 급식 메뉴 발표하기
schoolLunch.updateMenu("치킨마요덮밥, 미소된장국, 배추김치");

console.log("\n" + "=".repeat(50) + "\n");

// 내일의 급식 메뉴 발표하기
schoolLunch.updateMenu("스파게티, 피자, 샐러드, 콘스프");

🔹 예제 2: 우리 반 날씨 관측소

두 번째 예제에서는 우리 반에서 직접 운영하는 작은 날씨 관측소를 만들어 보겠습니다.

// 우리 반 날씨 관측소
class WeatherStation {
  constructor() {
    // 날씨 정보를 받고 싶어하는 관측기들의 목록
    this.observers = [];
    // 현재 온도, 습도, 날씨 상태 저장하기
    this.temperature = 0;
    this.humidity = 0;
    this.weatherCondition = "";
  }

  // 날씨 정보를 받고 싶은 관측기 등록하기
  addWeatherObserver(observer) {
    this.observers.push(observer);
    console.log(`${observer.name} 날씨 관측기가 연결되었어요!`);
  }

  // 모든 관측기들에게 날씨 정보 전송하기
  broadcastWeatherData() {
    console.log("\n🌤️ 날씨 정보를 모든 관측기에 전송 중...\n");
    for (const observer of this.observers) {
      observer.updateWeatherInfo(this.temperature, this.humidity, this.weatherCondition);
    }
  }

  // 새로운 날씨 정보 업데이트하기
  setWeatherData(temperature, humidity, condition) {
    console.log(`🌡️ 새로운 날씨 데이터: ${temperature}°C, 습도 ${humidity}%, ${condition}`);
    this.temperature = temperature;
    this.humidity = humidity;
    this.weatherCondition = condition;
    this.broadcastWeatherData();
  }
}

// 현재 날씨 상태를 보여주는 디스플레이
class CurrentWeatherDisplay {
  constructor() {
    this.name = "현재 날씨 화면";
  }

  updateWeatherInfo(temperature, humidity, condition) {
    console.log(`📺 [${this.name}] 현재 날씨`);
    console.log(`   온도: ${temperature}°C`);
    console.log(`   습도: ${humidity}%`);
    console.log(`   날씨: ${condition}`);

    // 온도에 따라 옷차림 추천하기
    if (temperature >= 25) {
      console.log("   💡 반팔과 반바지를 입으세요!");
    } else if (temperature >= 15) {
      console.log("   💡 긴팔 셔츠가 좋겠어요!");
    } else {
      console.log("   💡 따뜻한 옷을 입으세요!");
    }
    console.log("");
  }
}

// 날씨 경보를 보내주는 시스템
class WeatherAlert {
  constructor() {
    this.name = "날씨 경보 시스템";
  }

  updateWeatherInfo(temperature, humidity, condition) {
    console.log(`🚨 [${this.name}] 날씨 경보 확인`);

    if (temperature >= 35) {
      console.log("   ⚠️ 폭염 경보! 물을 많이 마시고 그늘에서 쉬세요!");
    } else if (temperature <= -10) {
      console.log("   ⚠️ 한파 경보! 따뜻하게 입고 외출에 주의하세요!");
    } else if (humidity >= 80 && condition === "비") {
      console.log("   ⚠️ 습도가 높고 비가 와요! 우산을 꼭 챙기세요!");
    } else {
      console.log("   ✅ 안전한 날씨입니다. 좋은 하루 보내세요!");
    }
    console.log("");
  }
}

// 날씨 관측소 시스템 사용해보기
const weatherStation = new WeatherStation();

// 날씨 디스플레이들 만들기
const currentDisplay = new CurrentWeatherDisplay();
const alertSystem = new WeatherAlert();

// 날씨 관측소에 디스플레이들 연결하기
weatherStation.addWeatherObserver(currentDisplay);
weatherStation.addWeatherObserver(alertSystem);

// 날씨 정보 업데이트해보기
weatherStation.setWeatherData(22, 65, "맑음");
weatherStation.setWeatherData(37, 45, "맑음");  // 폭염 경보!

✏️ 연습문제로 개념 다지기

연습을 시작하기 전에 잠시 생각해보세요. 여러분이 좋아하는 사람의 소식을 기다리는 마음, 친구가 보내는 메시지를 받는 기쁨을 말이에요. 옵저버 패턴도 그런 따뜻한 마음들로 이루어져 있답니다. 서로를 바라보고, 소식을 나누고, 함께 반응하는 것이죠.

Q1. 생일파티 초대장 시스템 만들기

// 생일파티 초대 시스템
class BirthdayPartyInvitation {
  constructor() {
    this.friends = [];
    this.partyInfo = null;
  }

  addFriend(friend) {
    if (!this.friends.includes(friend)) {
      this.friends.push(friend);
      console.log(`${friend.name}님이 초대 목록에 추가되었어요!`);
    }
  }

  sendInvitations() {
    console.log("\n🎉 생일파티 초대장을 보내는 중...\n");
    for (const friend of this.friends) {
      friend.receiveInvitation(this.partyInfo);
    }
  }

  planParty(birthday, date, location) {
    this.partyInfo = { birthday, date, location };
    console.log(`🎂 ${birthday}님의 생일파티가 계획되었어요!`);
    this.sendInvitations();
  }
}

// 파티 초대를 받는 친구들
class Friend {
  constructor(name, favoriteActivity) {
    this.name = name;
    this.favoriteActivity = favoriteActivity || "놀기";
  }

  receiveInvitation(partyInfo) {
    console.log(`${this.name}: ${partyInfo.birthday}님의 생일파티 초대장을 받았어요!`);

    if (this.favoriteActivity === "춤") {
      console.log(`${this.name}: 파티에서 신나게 춤출 수 있겠네요! 💃`);
    } else if (this.favoriteActivity === "게임") {
      console.log(`${this.name}: 재미있는 게임을 준비해갈게요! 🎮`);
    } else {
      console.log(`${this.name}: 정말 기대돼요! 🎈`);
    }
    console.log("");
  }
}

// 사용 예시
const partyInvitation = new BirthdayPartyInvitation();
const minho = new Friend("민호", "춤");
const jiyeon = new Friend("지연", "게임");

partyInvitation.addFriend(minho);
partyInvitation.addFriend(jiyeon);
partyInvitation.planParty("수연", "이번 주 토요일", "우리 집");

Q2. 교실 온도 조절 시스템 만들기

// 교실 온도 관리 시스템
class ClassroomTemperature {
  constructor() {
    this.devices = [];
    this.currentTemperature = 22;
  }

  addDevice(device) {
    this.devices.push(device);
    console.log(`${device.name} 장치가 연결되었어요!`);
  }

  setTemperature(newTemperature) {
    console.log(`🌡️ 교실 온도: ${newTemperature}도`);
    this.currentTemperature = newTemperature;

    console.log("\n온도 변화 알림 전송 중...\n");
    for (const device of this.devices) {
      device.handleTemperatureChange(this.currentTemperature);
    }
  }
}

// 에어컨 시스템
class AirConditioner {
  constructor() {
    this.name = "에어컨";
    this.isRunning = false;
  }

  handleTemperatureChange(temperature) {
    console.log(`❄️ [${this.name}] 온도 ${temperature}도 감지!`);

    if (temperature > 25) {
      if (!this.isRunning) {
        this.isRunning = true;
        console.log(`❄️ [${this.name}] 더우니까 에어컨을 켤게요!`);
      }
    } else if (temperature < 20) {
      if (this.isRunning) {
        this.isRunning = false;
        console.log(`❄️ [${this.name}] 충분히 시원하니까 에어컨을 끌게요!`);
      }
    }
    console.log("");
  }
}

// 온도 표시판
class TemperatureDisplay {
  constructor() {
    this.name = "온도표시판";
  }

  handleTemperatureChange(temperature) {
    console.log(`📊 [${this.name}] 현재 교실 온도: ${temperature}도`);

    if (temperature > 26) {
      console.log(`📊 [${this.name}] 🔥 너무 더워요!`);
    } else if (temperature >= 20) {
      console.log(`📊 [${this.name}] 😊 적당한 온도예요!`);
    } else {
      console.log(`📊 [${this.name}] 🥶 너무 추워요!`);
    }
    console.log("");
  }
}

// 사용 예시
const classroom = new ClassroomTemperature();
const aircon = new AirConditioner();
const display = new TemperatureDisplay();

classroom.addDevice(aircon);
classroom.addDevice(display);

classroom.setTemperature(27);  // 더운 온도
classroom.setTemperature(19);  // 추운 온도

🔥 심화 내용으로 더 깊이 알아보기

앞의 내용을 잘 이해했다면, 이제 조금 더 깊은 내용을 살펴보세요!

심화 Q1. 옵저버 패턴의 장점과 단점

장점:

  1. 코드 분리: 각 부분이 독립적으로 동작해요
  2. 확장 용이: 새로운 기능을 쉽게 추가할 수 있어요
  3. 재사용 가능: 다른 프로젝트에서도 사용할 수 있어요

단점:

  1. 복잡성: 간단한 일에도 여러 클래스가 필요해요
  2. 디버깅 어려움: 문제가 어디서 생겼는지 찾기 어려워요

심화 Q2. 안전한 뉴스 시스템 만들기

// 안전한 뉴스 발행 시스템
class SafeNewsPublisher {
  constructor() {
    this.subscribers = [];
    this.news = "";
  }

  subscribe(subscriber) {
    if (!this.subscribers.includes(subscriber)) {
      this.subscribers.push(subscriber);
      console.log(`${subscriber.name}님이 뉴스 구독을 시작했어요!`);
      return true;
    }
    return false;
  }

  publishNews(news) {
    this.news = news;
    console.log(`📰 새로운 뉴스: "${news}"`);

    // 안전하게 알림 보내기
    const subscribersCopy = [...this.subscribers];
    subscribersCopy.forEach(subscriber => {
      try {
        subscriber.receiveNews(news);
      } catch (error) {
        console.error(`${subscriber.name}에게 뉴스 전송 중 오류:`, error.message);
      }
    });
  }
}

// 뉴스 구독자
class NewsSubscriber {
  constructor(name, isActive = true) {
    this.name = name;
    this.isActive = isActive;
  }

  receiveNews(news) {
    if (!this.isActive) {
      throw new Error(`${this.name}은 현재 비활성 상태입니다.`);
    }
    console.log(`${this.name}: "${news}" 뉴스를 받았어요!`);
  }
}

// 사용 예시
const publisher = new SafeNewsPublisher();
const subscriber1 = new NewsSubscriber("철수");
const subscriber2 = new NewsSubscriber("영희", false);  // 비활성

publisher.subscribe(subscriber1);
publisher.subscribe(subscriber2);
publisher.publishNews("오늘은 맑은 날씨입니다!");

🔄 17단원 복습 - 모듈과 옵저버 패턴

18단원에서 옵저버 패턴을 배우고 있으니, 17단원에서 배운 모듈 시스템과 어떻게 함께 사용할 수 있는지 복습해보겠습니다!

복습 문제 1: ES6 모듈과 옵저버 패턴 함께 사용하기

17단원에서 배운 ES6 모듈 방식으로 옵저버 패턴을 구현해보세요!

// 17단원 복습: observable.js (ES6 모듈 방식)
// 옵저버 패턴을 모듈로 만들기

// 기본 주체 클래스 내보내기
export class Subject {
  constructor() {
    this.observers = [];
  }

  subscribe(observer) {
    if (!this.observers.includes(observer)) {
      this.observers.push(observer);
      return true;
    }
    return false;
  }

  unsubscribe(observer) {
    const index = this.observers.indexOf(observer);
    if (index !== -1) {
      this.observers.splice(index, 1);
      return true;
    }
    return false;
  }

  notify(data) {
    this.observers.forEach(observer => {
      observer.update(data);
    });
  }
}

// 기본 옵저버 클래스 내보내기
export class Observer {
  constructor(name) {
    this.name = name;
  }

  update(data) {
    console.log(`${this.name}: ${data} 데이터를 받았어요!`);
  }
}

// main.js에서 사용하기
// import { Subject, Observer } from './observable.js';
// 
// const subject = new Subject();
// const observer1 = new Observer("관찰자1");
// 
// subject.subscribe(observer1);
// subject.notify("안녕하세요!");

복습 문제 2: CommonJS 방식으로 옵저버 패턴 사용하기

17단원에서 배운 Node.js의 CommonJS와 옵저버 패턴을 함께 사용해보세요!

// 17단원 복습: eventSystem.js (Node.js 파일)
// CommonJS 방식으로 이벤트 시스템 만들기

const EventSystem = (function() {
  const events = {};  // 이벤트별 리스너 저장

  return {
    // 이벤트 리스너 등록
    on: function(eventName, listener) {
      if (!events[eventName]) {
        events[eventName] = [];
      }
      events[eventName].push(listener);
      console.log(`${eventName} 이벤트 리스너가 등록되었어요!`);
    },

    // 이벤트 발생시키기
    emit: function(eventName, data) {
      if (events[eventName]) {
        console.log(`${eventName} 이벤트 발생!`);
        events[eventName].forEach(listener => {
          listener(data);
        });
      }
    },

    // 이벤트 리스너 제거
    off: function(eventName, listener) {
      if (events[eventName]) {
        const index = events[eventName].indexOf(listener);
        if (index !== -1) {
          events[eventName].splice(index, 1);
        }
      }
    }
  };
})();

// CommonJS로 내보내기 (17단원에서 배운 방법)
module.exports = EventSystem;

// 다른 파일에서 사용하기
// const EventSystem = require('./eventSystem.js');
// 
// EventSystem.on('message', (data) => {
//   console.log('메시지 받음:', data);
// });
// 
// EventSystem.emit('message', '안녕하세요!');

지금까지 옵저버 패턴에 대해 배워봤어요. 이 패턴은 객체들이 서로 소통하는 아름다운 방법을 제공해줍니다. 마치 친구들끼리 소식을 주고받는 것처럼 자연스럽고 따뜻해요!

다음 시간에는 또 다른 흥미로운 프로그래밍 패턴을 배워볼 거예요. 오늘 배운 옵저버 패턴을 꾸준히 연습해서 멋진 코딩 실력을 키워나가세요! 🎁✨

✅ 학습 완료 체크리스트

이번 시간에 배운 내용들을 모두 이해했는지 확인해보세요!

학습 내용 이해했나요?
옵저버 패턴의 기본 개념
기본 사용법과 문법
주요 특징과 차이점
자주 하는 실수들
실전 예제 이해
17단원 복습 내용

📂 마무리 정보

오늘 배운 18.4.1 옵저버 패턴 내용이 여러분의 자바스크립트 지식 정리함에 잘 저장되었나요? 다음 시간에는 더 재미있는 내용으로 만나요!

기억할 점: 오늘 배운 내용을 꼭 연습해보시고, 궁금한 점이 있으면 언제든 다시 돌아와서 읽어보세요.


🚀 더 체계적인 JavaScript 학습을 원하신다면?
이 포스팅에서 다룬 내용을 실제로 실습해보세요!
무료 JavaScript 학습 플랫폼에서 단계별 학습과 실시간 코드 실행을 통해
더욱 효과적이고 재미있게 학습하실 수 있습니다.
📝 실시간 코드 실행 📊 학습 진도 관리 👥 체계적 커리큘럼
📚 171개 체계적 학습레슨 · 📋 855개 4지선다 연습문제 · 🆓 완전 무료 · ⚡ 즉시 시작