GoF 디자인 패턴: 행위 패턴(1) - 전략, 옵서버 | 자바스크립트 예시 + 🚀정처기 실기 대비 문제

정처기디자인 패턴GoF행위 패턴전략 패턴옵서버 패턴객체지향TypeScript
읽는데 약 11분 정도 소요
처음 쓰여진 날: 2025-07-04
마지막으로 고쳐진 날: 2025-07-08
이 글을 보러온 횟수: 25

요약

GoF 디자인 패턴 중 객체의 행동과 소통 방식을 다루는 행위 패턴을 소개합니다. 다양한 알고리즘을 동적으로 교체하는 전략(Strategy) 패턴과, 한 객체의 상태 변화를 여러 객체에 자동으로 전파하는 옵서버(Observer) 패턴의 개념과 예시를 알아봅니다.

📚 GoF 디자인 패턴이란?

GoF는 'Gang of Four'의 약자로, 객체지향 프로그래밍 분야에서 자주 발생하는 문제들에 대한 해결책을 담은 디자인 패턴 책의 저자 4명을 의미합니다. 이들이 정리한 패턴들은 코드의 재사용성, 유지보수성, 확장성을 높이는 데 큰 도움을 줍니다. 💡

디자인 패턴은 크게 생성(Creational) 🏗️, 구조(Structural) 🧩, 행위(Behavioral) 🎭 세 가지로 나뉩니다. 이번 글에서는 객체 간의 상호작용과 책임 분배를 다루는 행위 패턴 🎭 을 살펴보겠습니다.

🎭 행위 패턴이란 무엇일까?

행위 패턴은 단어 뜻 그대로 객체들의 '행동'과 '소통' 방식에 중점을 둡니다. 구조 패턴이 객체들을 조립해 튼튼한 구조를 만드는 데 집중했다면, 행위 패턴은 그 구조 안에서 객체들이 어떻게 서로 메시지를 주고받고, 역할을 나누며, 협력하여 공동의 목표를 달성하는지에 대한 방법을 다룹니다.

마치 한 편의 연극 🎬 에서 배우(객체)들이 각자의 역할에 따라 어떻게 대사를 주고받고 상호작용하여 극을 이끌어가는지와 같습니다.

예를 들어:

  • 📬 요청을 보내는 객체와 처리하는 객체를 분리하거나 (커맨드 패턴)
  • 🧐 한 객체의 상태가 변하면 다른 객체들에게 자동으로 알려주거나 (옵서버 패턴)
  • ♟️ 알고리즘을 통째로 캡슐화하여 동적으로 교체할 수 있게 하거나 (전략 패턴)
  • ⛓️ 여러 객체에게 요청을 처리할 기회를 연쇄적으로 제공하는 것 (책임 연쇄 패턴)

이 모든 것이 행위 패턴에 해당합니다.

핵심은 '책임'과 '소통' 🔗 입니다. 행위 패턴을 사용하면 객체 간의 결합도를 낮추면서도, 복잡한 상호작용을 유연하고 체계적으로 관리할 수 있습니다. 이를 통해 시스템은 변화에 더 쉽게 대응하고, 각 객체의 역할이 명확해져 유지보수가 쉬워집니다. ✨

♟️ 1. 전략 (Strategy) 패턴

전략 패턴은 알고리즘군(a family of algorithms)을 정의하고, 각 알고리즘을 캡슐화하여, 필요에 따라 서로 교체해서 사용할 수 있게 만드는 디자인 패턴입니다. 즉, '무엇을' 할 것인지는 Context(문맥) 객체에 그대로 두고, '어떻게' 할 것인지를 다양한 Strategy(전략) 객체에 위임하는 방식입니다.

현실 세계의 예로 '경로 찾기' 🗺️ 를 들 수 있습니다. 서울에서 부산까지 가는 방법에는 KTX, 버스, 비행기 등 여러 가지가 있습니다. 어떤 교통수단을 선택하든 '서울에서 부산으로 이동한다'는 목표(Context)는 동일하지만, 이동 방식(Strategy)은 달라집니다. 전략 패턴은 이처럼 다양한 '방법(알고리즘)'들을 부품처럼 쉽게 갈아 끼울 수 있도록 해줍니다.

🏗️ 기본 구조

  • ♟️ Context: 전략을 사용하는 주체입니다. 내부에 Strategy 인터페이스에 대한 참조를 가지고 있으며, 클라이언트의 요청을 받으면 자신이 가진 전략 객체에게 작업을 위임합니다.
  • 📜 Strategy: 모든 구체적인 전략 클래스들이 구현해야 하는 공통 인터페이스입니다. 보통 알고리즘을 실행하는 단일 메서드(예: execute())를 정의합니다.
  • 🚗 ConcreteStrategy: Strategy 인터페이스를 구현하여 실제 알고리즘을 정의하는 클래스입니다. (예: BusStrategy, TrainStrategy)

💻 예시: 결제 시스템 구현하기

온라인 쇼핑몰에서 다양한 결제 방법을 지원하는 시스템을 만든다고 가정해봅시다. 사용자는 신용카드, 카카오페이, 네이버페이 등 여러 방법으로 결제할 수 있어야 합니다.

먼저, 모든 결제 전략이 따라야 할 PaymentStrategy 인터페이스를 정의합니다.

javascript
// Strategy: 공통 결제 인터페이스
class PaymentStrategy {
  pay(amount) {
    throw new Error("pay() 메서드는 반드시 구현되어야 합니다.");
  }
}

이제 구체적인 결제 방법들을 ConcreteStrategy로 구현합니다.

javascript
// ConcreteStrategy A: 신용카드 결제
class CreditCardStrategy extends PaymentStrategy {
  constructor(name, cardNumber) {
    super();
    this.name = name;
    this.cardNumber = cardNumber;
  }

  pay(amount) {
    console.log(
      `${amount}원을 ${this.name}님의 신용카드(${this.cardNumber})로 결제합니다.`
    );
    // 실제 카드사 연동 로직...
  }
}

// ConcreteStrategy B: 카카오페이 결제
class KakaoPayStrategy extends PaymentStrategy {
  constructor(email) {
    super();
    this.email = email;
  }

  pay(amount) {
    console.log(`${amount}원을 ${this.email} 카카오페이 계정으로 결제합니다.`);
    // 실제 카카오페이 API 연동 로직...
  }
}

다음으로, 결제 프로세스를 담당할 ShoppingCart(장바구니) 클래스를 Context로 만듭니다.

javascript
// Context: 결제 전략을 사용하는 장바구니
class ShoppingCart {
  constructor(amount) {
    this.amount = amount;
    this.paymentStrategy = null; // 처음에는 전략이 설정되지 않음
  }

  // 클라이언트가 동적으로 전략을 변경할 수 있도록 setter를 제공
  setPaymentStrategy(strategy) {
    this.paymentStrategy = strategy;
  }

  // 결제 실행
  checkout() {
    if (!this.paymentStrategy) {
      console.log("결제 방법이 선택되지 않았습니다.");
      return;
    }
    this.paymentStrategy.pay(this.amount);
  }
}

// 클라이언트 코드
const cart = new ShoppingCart(10000); // 10,000원짜리 상품

// 신용카드로 결제하기
const creditCard = new CreditCardStrategy("김재현", "1234-5678-9012-3456");
cart.setPaymentStrategy(creditCard);
cart.checkout();
// 출력: 10000원을 김재현님의 신용카드(1234-5678-9012-3456)로 결제합니다.

console.log("
--- 결제 방법을 변경합니다 ---");

// 카카오페이로 결제하기
const kakaoPay = new KakaoPayStrategy("test@kakao.com");
cart.setPaymentStrategy(kakaoPay);
cart.checkout();
// 출력: 10000원을 test@kakao.com 카카오페이 계정으로 결제합니다.

ShoppingCart 클래스는 어떤 결제 방법이 선택되었는지 구체적으로 알 필요가 없습니다. 그저 paymentStrategy에 할당된 객체의 pay() 메서드를 호출할 뿐입니다. 만약 '네이버페이'라는 새로운 결제 수단이 추가되어도, NaverPayStrategy 클래스를 새로 만들기만 하면 ShoppingCart 코드는 전혀 수정할 필요가 없습니다.

이처럼 전략 패턴은 **'개방-폐쇄 원칙(OCP)'**을 잘 따르는 설계입니다. 새로운 기능(전략) 추가에는 열려 있고, 기존 코드의 수정에는 닫혀 있습니다.

전략 패턴 중요 키워드 🔑

  • ↔️ 알고리즘을 캡슐화하고 동적으로 교체할 수 있습니다.
  • 📜 '어떻게' 하는가(How)를 '무엇을' 하는가(What)로부터 분리합니다.
  • 🤝 위임(Delegation) 을 통해 문제를 해결합니다. (Context가 Strategy에게 위임)
  • 🧩 if-elseswitch 문으로 가득 찬 코드를 리팩토링하는 좋은 방법입니다.
  • 💡 개방-폐쇄 원칙(OCP)을 만족시킵니다.

🧐 2. 옵서버 (Observer) 패턴

옵서버 패턴은 한 객체의 상태가 변하면, 그 객체에 의존하는 다른 객체들에게 자동으로 알림(notification)을 보내고 상태를 갱신할 수 있게 하는 일대다(one-to-many) 의존성 관계를 정의하는 패턴입니다. 상태를 가진 객체를 Subject(주제), 상태의 변화를 통지받는 객체들을 Observer(관찰자)라고 합니다.

현실 세계의 예로 '유튜브 채널 구독' 📢 을 들 수 있습니다. 유튜버(Subject)가 새로운 영상을 올리면, 그 채널을 구독(register)한 모든 구독자(Observer)들에게 자동으로 알림이 갑니다. 구독을 취소(unregister)하면 더 이상 알림을 받지 않습니다. 옵서버 패턴은 이처럼 느슨하게 연결된(loosely coupled) 객체들 간의 자동화된 소통 방식을 구현할 때 매우 유용합니다.

🏗️ 기본 구조

  • 📢 Subject: 관찰 대상이 되는 객체입니다. 내부에 옵서버 목록을 가지고 있으며, 옵서버를 등록(register)하고 제거(unregister)하는 메서드를 제공합니다. 상태가 변경되면 notify() 메서드를 호출하여 모든 옵서버에게 변경 사실을 알립니다.
  • 👀 Observer: Subject의 상태 변화를 통지받는 객체들이 구현해야 할 인터페이스입니다. 보통 update()와 같은 메서드를 정의하며, Subject로부터 알림이 오면 이 메서드가 호출됩니다.
  • ConcreteSubject: Subject 인터페이스를 구현한 구체적인 클래스입니다. 상태를 저장하고, 상태가 변경될 때 옵서버들에게 알리는 역할을 합니다.
  • ConcreteObserver: Observer 인터페이스를 구현한 구체적인 클래스입니다. update() 메서드가 호출되면 특정 동작을 수행합니다.

💻 예시: 뉴스레터 구독 시스템

새로운 기사가 발행될 때마다 구독자들에게 이메일로 알려주는 뉴스레터 시스템을 만들어 보겠습니다.

먼저, 모든 옵서버(구독자)가 구현할 Observer 인터페이스를 정의합니다.

javascript
// Observer: 구독자 인터페이스
class Subscriber {
  update(article) {
    throw new Error("update() 메서드는 반드시 구현되어야 합니다.");
  }
}

다음으로, 관찰 대상인 NewsPublisher(뉴스 발행사)를 Subject로 만듭니다.

javascript
// Subject: 뉴스 발행사
class NewsPublisher {
  constructor() {
    this.subscribers = []; // 옵서버(구독자) 목록
    this.latestArticle = null;
  }

  // 옵서버 등록
  register(subscriber) {
    this.subscribers.push(subscriber);
    console.log(`${subscriber.name}님이 구독을 시작했습니다.`);
  }

  // 옵서버 제거
  unregister(subscriber) {
    this.subscribers = this.subscribers.filter((sub) => sub !== subscriber);
    console.log(`${subscriber.name}님이 구독을 취소했습니다.`);
  }

  // 모든 옵서버에게 알림
  notify() {
    console.log("\n--- 새로운 기사 발행! 모든 구독자에게 알림 전송 ---");
    this.subscribers.forEach((subscriber) => {
      subscriber.update(this.latestArticle);
    });
  }

  // 새로운 기사 발행 (상태 변경)
  publishNewArticle(article) {
    this.latestArticle = article;
    this.notify();
  }
}

이제 구체적인 구독자, EmailSubscriberConcreteObserver로 구현합니다.

javascript
// ConcreteObserver: 이메일 구독자
class EmailSubscriber extends Subscriber {
  constructor(name) {
    super();
    this.name = name;
  }

  update(article) {
    console.log(
      `[${this.name}님에게 이메일 전송] 새로운 기사: "${article.title}"`
    );
  }
}

// 클라이언트 코드
const publisher = new NewsPublisher();

const sub1 = new EmailSubscriber("김재현");
const sub2 = new EmailSubscriber("이영희");

publisher.register(sub1); // 김재현님 구독 시작
publisher.register(sub2); // 이영희님 구독 시작

publisher.publishNewArticle({
  title: "AI, 인간의 일자리를 대체할까?",
  content: "...",
});
// 출력:
// --- 새로운 기사 발행! 모든 구독자에게 알림 전송 ---
// [김재현님에게 이메일 전송] 새로운 기사: "AI, 인간의 일자리를 대체할까?"
// [이영희님에게 이메일 전송] 새로운 기사: "AI, 인간의 일자리를 대체할까?"

publisher.unregister(sub2); // 이영희님 구독 취소

publisher.publishNewArticle({
  title: "2025년 기술 트렌드 전망",
  content: "...",
});
// 출력:
// --- 새로운 기사 발행! 모든 구독자에게 알림 전송 ---
// [김재현님에게 이메일 전송] 새로운 기사: "2025년 기술 트렌드 전망"

NewsPublisher는 어떤 종류의 구독자가 있는지, 구독자가 이메일을 받는지 앱 푸시를 받는지 전혀 알 필요가 없습니다. 그저 subscribers 목록에 있는 각 객체의 update() 메서드를 호출할 뿐입니다. 만약 '앱 푸시 알림' 기능이 필요하다면, AppPushSubscriber라는 새로운 옵서버 클래스를 만들어 등록하기만 하면 됩니다.

이처럼 옵서버 패턴은 SubjectObserver 간의 결합도를 낮춰, 시스템을 유연하고 확장 가능하게 만듭니다.

옵서버 패턴 중요 키워드 🔑

  • 📢 일대다(one-to-many) 의존 관계를 정의합니다.
  • 🔗 느슨한 결합(Loose Coupling): Subject는 Observer의 구체적인 클래스를 몰라도 됩니다.
  • 🔄 상태 변경 시 자동으로 의존 객체에 전파(broadcast)됩니다.
  • 📰 이벤트 기반 시스템, MVC(Model-View-Controller) 아키텍처 등에서 널리 사용됩니다.
  • ✅ 발행/구독(Publish/Subscribe) 모델이라고도 불립니다.

정처기 실기 대비 문제

문제알고리즘 군을 정의하고(추상 클래스) 같은 알고리즘을 각각 하나의 클래스로 캡슐화한 다음, 필요할 때 서로 교환해서 사용할 수 있게 하는 패턴
정답정답 확인하기