실제로 사용되는 자바스크립트 디자인 패턴 공부하기

Singleton 패턴

Singleton 패턴은 오직 하나의 인스턴스만을 생성하여 전역에서 접근 가능하도록 하는 디자인 패턴입니다. 이를 통해 객체의 생성 비용을 줄이고, 공유 리소스에 대한 중복 생성을 방지할 수 있습니다.


class Singleton {
  // 1. private static 변수에 Singleton 인스턴스 저장
  private static instance: Singleton;
  
  // 2. private 생성자를 통해 외부 인스턴스 생성 방지
  private constructor() {}

  // 3. 인스턴스 가져오기 메서드
  public static getInstance(): Singleton {
    // 이미 인스턴스가 존재한다면 해당 인스턴스 반환
    if (!this.instance) {
      this.instance = new Singleton();
    }
    return this.instance;
  }

  // 기타 메서드와 속성
}
  
// 사용 예시
const mySingleton1 = Singleton.getInstance();
const mySingleton2 = Singleton.getInstance();

console.log(mySingleton1 === mySingleton2); // true

Singleton 패턴 예시

Singleton 패턴을 이용하여 로그인 상태를 관리하는 UserManager 클래스 예시입니다.


class UserManager {
  private static instance: UserManager;
  private isLoggedIn: boolean;

  private constructor() {
    this.isLoggedIn = false;
  }

  public static getInstance(): UserManager {
    if (!this.instance) {
      this.instance = new UserManager();
    }
    return this.instance;
  }

  public login(username: string, password: string): void {
    // 로그인 처리
    this.isLoggedIn = true;
    console.log(`${username} 님 로그인`);
  }

  public logout(): void {
    // 로그아웃 처리
    this.isLoggedIn = false;
    console.log(`로그아웃`);
  }

  public isLoggedInUser(): boolean {
    return this.isLoggedIn;
  }
}

// 사용 예시
const userManager1 = UserManager.getInstance();
const userManager2 = UserManager.getInstance();

userManager1.login("John", "password123");
console.log(userManager1.isLoggedInUser()); // true
console.log(userManager2.isLoggedInUser()); // true

userManager2.logout();
console.log(userManager1.isLoggedInUser()); // false
console.log(userManager2.isLoggedInUser()); // false

Factory 패턴

Factory 패턴은 객체의 생성을 담당하는 패턴으로, 객체를 생성하는 부분과 사용하는 부분을 분리하여 유연성과 확장성을 높여줍니다. 클라이언트는 팩토리 클래스를 통해 필요한 객체를 생성하고, 생성된 객체를 사용할 수 있습니다.

Factory 패턴은 생성자 함수나 정적 메서드를 사용하여 객체 생성을 캡슐화합니다. 이를 통해 클라이언트는 직접 객체를 생성하는 코드를 작성하지 않아도 되며, 객체에 대한 상세한 내용을 알 필요가 없습니다. 또한, 객체 생성 과정이 변한다면 팩토리 클래스 내부만 수정하면 되므로 변화에 대응하기 쉽습니다.


// 클래스 팩토리
class ShapeFactory {
  public createShape(type: string): Shape | null {
    if (type === "circle") {
      return new Circle();
    } else if (type === "rectangle") {
      return new Rectangle();
    } else if (type === "triangle") {
      return new Triangle();
    } else {
      console.log("유효한 도형 타입이 아닙니다.");
      return null;
    }
  }
}

// 사용 예시
const factory = new ShapeFactory();

const circle = factory.createShape("circle");
if (circle) {
  circle.draw();
}

const rectangle = factory.createShape("rectangle");
if (rectangle) {
  rectangle.draw();
}

const invalidShape = factory.createShape("star");
// "유효한 도형 타입이 아닙니다." 출력

Factory 패턴 예시

Factory 패턴을 이용하여 로그인 방식에 따라 인증 객체를 생성하는 AuthFactory 클래스 예시입니다. 인증 객체는 Auth 인터페이스를 구현하며, 로그인 방식별로 구체적인 인증 객체를 반환할 수 있습니다.


interface Auth {
  authenticate(): void;
}

class EmailAuth implements Auth {
  authenticate() {
    // 이메일 인증 처리
    console.log("이메일로 인증합니다.");
  }
}

class GoogleAuth implements Auth {
  authenticate() {
    // 구글 인증 처리
    console.log("구글로 인증합니다.");
  }
}

class AuthFactory {
  static createAuth(provider: string): Auth | null {
    if (provider === "email") {
      return new EmailAuth();
    } else if (provider === "google") {
      return new GoogleAuth();
    } else {
      console.log("지원하지 않는 인증 방식입니다.");
      return null;
    }
  }
}

// 사용 예시
const emailAuth = AuthFactory.createAuth("email");
if (emailAuth) {
  emailAuth.authenticate();
}

const googleAuth = AuthFactory.createAuth("google");
if (googleAuth) {
  googleAuth.authenticate();
}

const invalidAuth = AuthFactory.createAuth("facebook");
// "지원하지 않는 인증 방식입니다." 출력

Observer 패턴

Observer 패턴은 객체 간의 일대다 관계를 구현하는 디자인 패턴으로, 어떤 객체의 상태가 변경되었을 때 의존하는 다른 객체들에게 상태 변경을 알려주고 자동으로 업데이트되도록 합니다. 이를 통해 객체 간의 결합도를 낮추고, 유연하고 확장 가능한 시스템을 구축할 수 있습니다.

Observer 패턴은 주제(subject) 객체와 옵저버(observer) 객체로 구성됩니다. 주제 객체는 옵저버들을 등록하고 알림을 보내는 역할을 하며, 옵저버 객체는 주제 객체의 알림을 받고 특정 작업을 수행합니다. 주제 객체는 알림을 보낼 때 옵저버들에 대한 정보를 알지 못하고, 옵저버들도 다른 옵저버들의 존재를 모릅니다.


// 주제 객체
class Subject {
  private observers: Observer[] = [];

  public attach(observer: Observer): void {
    this.observers.push(observer);
  }

  public detach(observer: Observer): void {
    const index = this.observers.indexOf(observer);
    if (index >= 0) {
      this.observers.splice(index, 1);
    }
  }

  public notify(): void {
    for (const observer of this.observers) {
      observer.update();
    }
  }
}

// 옵저버 객체
interface Observer {
  update(): void;
}

class ConcreteObserver implements Observer {
  private subject: Subject;

  constructor(subject: Subject) {
    this.subject = subject;
    this.subject.attach(this);
  }

  public update(): void {
    // 주제 객체의 상태 변경에 대한 처리
    console.log("주제 객체의 상태가 변경되었습니다.");
  }
}

// 사용 예시
const subject = new Subject();

const observer1 = new ConcreteObserver(subject);
const observer2 = new ConcreteObserver(subject);

subject.notify();
// "주제 객체의 상태가 변경되었습니다." 출력 (observer1, observer2 모두 업데이트)

subject.detach(observer2);
subject.notify();
// "주제 객체의 상태가 변경되었습니다." 출력 (observer2 제외)

Observer 패턴 예시

Observer 패턴을 이용하여 주식 시장에서 주식 가격 변동을 관찰하는 시스템을 구현하는 예시입니다. StockMarket은 주식 가격을 업데이트하고, Portfolio와 NewsFeed는 주식 가격 변동을 구독하여 업데이트됩니다.


class StockMarket {
  private observers: Observer[] = [];
  private price: number = 0;

  public attach(observer: Observer): void {
    this.observers.push(observer);
  }

  public detach(observer: Observer): void {
    const index = this.observers.indexOf(observer);
    if (index >= 0) {
      this.observers.splice(index, 1);
    }
  }

  public updatePrice(price: number): void {
    this.price = price;
    this.notifyObservers();
  }

  private notifyObservers(): void {
    for (const observer of this.observers) {
      observer.update(this.price);
    }
  }
}

interface Observer {
  update(price: number): void;
}

class Portfolio implements Observer {
  private stocks: number[] = [];

  public update(price: number): void {
    this.stocks.push(price);
    console.log(`포트폴리오: 주식 가격 - ${price}`);
  }
}

class NewsFeed implements Observer {
  public update(price: number): void {
    console.log(`뉴스 피드: 주식 가격 - ${price}`);
  }
}

// 사용 예시
const stockMarket = new StockMarket();
const portfolio = new Portfolio();
const newsFeed = new NewsFeed();

stockMarket.attach(portfolio);
stockMarket.attach(newsFeed);

stockMarket.updatePrice(100); // 포트폴리오, 뉴스 피드 모두 업데이트
stockMarket.updatePrice(120); // 포트폴리오, 뉴스 피드 모두 업데이트

stockMarket.detach(newsFeed);
stockMarket.updatePrice(150); // 포트폴리오만 업데이트

Strategy 패턴

Strategy 패턴은 동일한 목표를 가진 여러 알고리즘 혹은 동작들을 캡슐화하고, 클라이언트에서 유연하게 변경하여 사용할 수 있도록 하는 디자인 패턴입니다. Strategy 패턴은 알고리즘과 객체 사이의 결합도를 낮추고, 유연하고 재사용 가능한 코드를 작성할 수 있도록 도와줍니다.

Strategy 패턴은 전략(strategy)을 인터페이스 형태로 정의하고, 여러 전략들을 구현하며, 클라이언트에서는 전략을 선택하여 사용합니다. 이를 통해 알고리즘을 독립적으로 변경하거나 확장할 수 있습니다. 또한, 서로 다른 전략들이 동일한 인터페이스를 사용하므로 코드 재사용이 용이합니다.


// 전략 인터페이스
interface Strategy {
  execute(): void;
}

// 전략 구현 클래스들
class ConcreteStrategyA implements Strategy {
  execute() {
    console.log("전략 A를 실행합니다.");
  }
}

class ConcreteStrategyB implements Strategy {
  execute() {
    console.log("전략 B를 실행합니다.");
  }
}

// 컨텍스트
class Context {
  private strategy: Strategy;

  constructor(strategy: Strategy) {
    this.strategy = strategy;
  }

  executeStrategy() {
    this.strategy.execute();
  }
}

// 사용 예시
const strategyA = new ConcreteStrategyA();
const strategyB = new ConcreteStrategyB();

const context = new Context(strategyA);
context.executeStrategy();
// "전략 A를 실행합니다." 출력

context.strategy = strategyB;
context.executeStrategy();
// "전략 B를 실행합니다." 출력

Strategy 패턴 예시

Strategy 패턴을 이용하여 결제 시스템에서 다양한 결제 수단을 처리하는 예시입니다. PaymentStrategy는 결제 전략을 정의하고, CreditCardStrategy와 PayPalStrategy는 해당 전략을 구현합니다. PaymentContext는 결제를 진행하는 컨텍스트 역할을 합니다.


interface PaymentStrategy {
  pay(amount: number): void;
}

class CreditCardStrategy implements PaymentStrategy {
  private cardNumber: string;
  private cvv: string;

  constructor(cardNumber: string, cvv: string) {
    this.cardNumber = cardNumber;
    this.cvv = cvv;
  }

  pay(amount: number) {
    console.log(`신용카드(${this.cardNumber})로 ${amount}원을 결제합니다.`);
  }
}

class PayPalStrategy implements PaymentStrategy {
  private email: string;
  private password: string;

  constructor(email: string, password: string) {
    this.email = email;
    this.password = password;
  }

  pay(amount: number) {
    console.log(`PayPal(${this.email})로 ${amount}원을 결제합니다.`);
  }
}

class PaymentContext {
  private strategy: PaymentStrategy;

  constructor(strategy: PaymentStrategy) {
    this.strategy = strategy;
  }

  pay(amount: number) {
    this.strategy.pay(amount);
  }
}

// 사용 예시
const cardStrategy = new CreditCardStrategy("1234567890123456", "123");
const paypalStrategy = new PayPalStrategy("email@example.com", "password123");

const paymentContext = new PaymentContext(cardStrategy);
paymentContext.pay(10000);
// "신용카드(1234567890123456)로 10000원을 결제합니다." 출력

paymentContext.strategy = paypalStrategy;
paymentContext.pay(5000);
// "PayPal(email@example.com)로 5000원을 결제합니다." 출력

Prototype 패턴

Prototype 패턴은 객체를 복제하여 생성하는 디자인 패턴으로, 객체를 생성하는 비용이 많이 들거나 복잡한 객체를 생성해야 할 때 유용합니다. Prototype 패턴은 원본 객체(프로토타입)을 복제하여 새로운 객체를 생성하는 방식으로 동작합니다. 이를 통해 객체의 생성 과정을 단순화하고, 새로운 객체들이 원본 객체의 초기 속성을 공유할 수 있습니다.

Prototype 패턴은 원본 객체가 되는 프로토타입을 정의하고, 복제(clone) 메서드를 구현합니다. 이렇게 구현된 복제 메서드를 사용하여 새로운 객체를 생성합니다. 복제된 객체는 원본 객체의 속성을 복사하고, 필요에 따라 속성의 값을 변경할 수 있습니다.


// 프로토타입 객체
class Prototype {
  private name: string;

  constructor(name: string) {
    this.name = name;
  }

  public clone(): Prototype {
    return new Prototype(this.name);
  }
}

// 사용 예시
const prototype = new Prototype("원본 객체");

const clone1 = prototype.clone();
// 프로토타입의 name 속성 값인 "원본 객체"를 가진 새로운 객체 생성

const clone2 = prototype.clone();
// 프로토타입의 name 속성 값인 "원본 객체"를 가진 새로운 객체 생성

Prototype 패턴 예시

Prototype 패턴을 이용하여 랜덤한 도형을 생성하는 시스템을 구현하는 예시입니다. Shape은 추상 클래스로, 도형의 클론을 생성하는 clone 메서드를 정의하고, 실제 도형들은 이를 구현합니다. ShapeCache는 도형들을 저장하고 복제하여 반환하는 역할을 합니다.


abstract class Shape {
  protected id: string;
  protected type: string;

  constructor() {
    this.id = "";
    this.type = "";
  }

  getId(): string {
    return this.id;
  }

  getType(): string {
    return this.type;
  }

  abstract clone(): Shape;
}

class Circle extends Shape {
  constructor() {
    super();
    this.type = "Circle";
  }

  clone(): Shape {
    const clone = new Circle();
    clone.id = this.id;
    return clone;
  }
}

class Rectangle extends Shape {
  constructor() {
    super();
    this.type = "Rectangle";
  }

  clone(): Shape {
    const clone = new Rectangle();
    clone.id = this.id;
    return clone;
  }
}

class ShapeCache {
  private static shapeMap: { [key: string]: Shape } = {};

  static getShape(id: string): Shape {
    const shape = ShapeCache.shapeMap[id];
    return shape.clone();
  }

  static loadCache(): void {
    const circle = new Circle();
    circle.getId(); // 랜덤한 ID 값 설정
    ShapeCache.shapeMap[circle.getId()] = circle;

    const rectangle = new Rectangle();
    rectangle.getId(); // 랜덤한 ID 값 설정
    ShapeCache.shapeMap[rectangle.getId()] = rectangle;
  }
}

// 사용 예시
ShapeCache.loadCache();

const circle = ShapeCache.getShape(Object.keys(ShapeCache.shapeMap)[0]);
console.log(circle.getType()); // "Circle" 출력

const rectangle = ShapeCache.getShape(Object.keys(ShapeCache.shapeMap)[1]);
console.log(rectangle.getType()); // "Rectangle" 출력

Decorator 패턴

Decorator 패턴은 객체에 동적으로 기능을 추가하기 위해 사용하는 디자인 패턴입니다. Decorator 패턴은 기존 객체의 수정 없이 새로운 기능을 추가하거나 수정할 수 있도록 합니다. 이를 통해 객체의 유연성을 높이고, 객체들 간의 결합도를 낮출 수 있습니다.

Decorator 패턴은 객체에 대한 인터페이스를 구현하는 구체적인 컴포넌트(Component) 클래스를 정의하고, 이에 대한 데코레이터(Decorator) 클래스들을 작성합니다. 데코레이터 클래스는 같은 인터페이스를 구현하면서, 실제 컴포넌트 객체를 감싸고 필요한 기능을 추가합니다. 그리고 나서 클라이언트에서는 필요한 데코레이터들을 순서대로 연결하여 원하는 기능을 적용합니다.


// 컴포넌트 인터페이스
interface Component {
  operation(): void;
}

// 구체적인 컴포넌트 클래스
class ConcreteComponent implements Component {
  operation() {
    console.log("기본 동작을 실행합니다.");
  }
}

// 데코레이터 클래스
class Decorator implements Component {
  private component: Component;

  constructor(component: Component) {
    this.component = component;
  }

  operation() {
    console.log("추가적인 기능을 수행합니다.");
    this.component.operation();
  }
}

// 사용 예시
const component = new ConcreteComponent();
const decorator = new Decorator(component);

decorator.operation();
// "추가적인 기능을 수행합니다."
// "기본 동작을 실행합니다."

Decorator 패턴 예시

Decorator 패턴을 이용하여 커피 주문 시스템을 구현하는 예시입니다. Beverage는 컴포넌트 인터페이스로, 구체적인 컴포넌트들(커피 종류)은 이를 구현합니다. CondimentDecorator는 데코레이터 클래스로, 필요한 추가 기능(설탕, 우유 등)을 데코레이터로써 갖습니다. 그리고 나서 클라이언트에서는 필요한 커피에 데코레이터를 연결하여 주문을 완성합니다.


// 컴포넌트 인터페이스
interface Beverage {
  getDescription(): string;
  cost(): number;
}

// 구체적인 컴포넌트 클래스
class Espresso implements Beverage {
  getDescription() {
    return "에스프레소";
  }

  cost() {
    return 1000;
  }
}

// 데코레이터 클래스
abstract class CondimentDecorator implements Beverage {
  protected beverage: Beverage;

  constructor(beverage: Beverage) {
    this.beverage = beverage;
  }

  getDescription(): string {
    return this.beverage.getDescription();
  }

  abstract cost(): number;
}

class Milk extends CondimentDecorator {
  cost() {
    return this.beverage.cost() + 500;
  }
}

class Sugar extends CondimentDecorator {
  cost() {
    return this.beverage.cost() + 200;
  }
}

// 사용 예시
const espresso = new Espresso();
console.log(`${espresso.getDescription()} ${espresso.cost()}원`); 
// "에스프레소 1000원" 출력

const latte = new Milk(new Espresso());
console.log(`${latte.getDescription()} ${latte.cost()}원`);
// "에스프레소, 우유 1500원" 출력

const sweetLatte = new Sugar(new Milk(new Espresso()));
console.log(`${sweetLatte.getDescription()} ${sweetLatte.cost()}원`);
// "에스프레소, 우유, 설탕 1700원" 출력

Proxy 패턴

Proxy 패턴은 접근 제어, 객체의 생성 및 소멸을 관리하거나 객체에 대한 추가적인 기능을 제공하는 역할을 하는 대리자(Proxy) 객체를 사용하여 객체를 제어하는 디자인 패턴입니다. Proxy 패턴은 클라이언트와 실제 객체 사이에서 중간자 역할을 수행하며, 실제 객체에 대한 접근을 제어하여 보안성을 강화하거나 객체의 생성과 소멸을 조절할 수 있습니다.

Proxy 패턴은 실제 객체와 Proxy 객체가 동일한 인터페이스를 구현하여 클라이언트에게 동일한 방법으로 접근되도록 합니다. Proxy 객체는 실제 객체를 생성하거나 소멸시키는 등의 추가적인 작업을 수행할 수 있습니다. 또한 Proxy 객체는 실제 객체에 대한 접근을 제어하여 클라이언트에게 보다 안전한 방법으로 접근할 수 있게 해줍니다.


// 인터페이스
interface Subject {
  request(): void;
}

// 실제 객체
class RealSubject implements Subject {
  request() {
    console.log("RealSubject의 request 메서드를 호출합니다.");
  }
}

// Proxy 객체
class Proxy implements Subject {
  private realSubject: RealSubject;

  request() {
    if (!this.realSubject) {
      this.realSubject = new RealSubject();
    }
    console.log("Proxy의 request 메서드를 호출합니다.");
    this.realSubject.request();
  }
}

// 사용 예시
const proxy = new Proxy();
proxy.request();
// "Proxy의 request 메서드를 호출합니다."
// "RealSubject의 request 메서드를 호출합니다."

Proxy 패턴 예시

Proxy 패턴을 이용하여 이미지 로딩 시스템을 구현하는 예시입니다. Image 인터페이스는 이미지를 표시하는 display 메서드를 정의하고, RealImage는 Image를 구현하여 실제 이미지 로딩을 수행합니다. ProxyImage는 Proxy로서 이미지 로딩 전에 로딩 중임을 나타내는 메시지를 표시하고, 로딩이 완료되면 실제 이미지를 표시합니다.


// 인터페이스
interface Image {
  display(): void;
}

// 실제 이미지
class RealImage implements Image {
  private filename: string;

  constructor(filename: string) {
    this.filename = filename;
    this.loadFromDisk();
  }

  private loadFromDisk(): void {
    console.log(`실제 이미지(${this.filename})를 로딩합니다.`);
  }

  display(): void {
    console.log(`실제 이미지(${this.filename})를 표시합니다.`);
  }
}

// Proxy 이미지
class ProxyImage implements Image {
  private filename: string;
  private realImage: RealImage | null;

  constructor(filename: string) {
    this.filename = filename;
    this.realImage = null;
  }

  display(): void {
    if (!this.realImage) {
      this.realImage = new RealImage(this.filename);
    }
    console.log(`로딩 중인 이미지(${this.filename})를 표시합니다.`);
    this.realImage.display();
  }
}

// 사용 예시
const image = new ProxyImage("image.jpg");
image.display();
// "로딩 중인 이미지(image.jpg)를 표시합니다."
// "실제 이미지(image.jpg)를 로딩합니다."
// "실제 이미지(image.jpg)를 표시합니다."

image.display();
// "로딩 중인 이미지(image.jpg)를 표시합니다."
// "실제 이미지(image.jpg)를 표시합니다." (로딩은 한 번만 수행)

Command 패턴

Command 패턴은 요청을 객체 자체로 캡슐화하여 요청을 매개변수화하고, 이력을 만들거나 작업을 지연, 취소할 수 있는 메서드를 지원하기 위해 사용하는 디자인 패턴입니다. Command 패턴은 요청 발송자(Sender), 수신자(Receiver), 명령(Command), 클라이언트(Client)로 구성됩니다. 이 패턴을 사용하면 클라이언트는 수신자에 대한 세부 정보 없이도 다양한 요청을 보낼 수 있습니다.

Command 패턴은 클라이언트가 실제 수신자 대신 Command 객체를 통해 요청을 전달하는 방식으로 동작합니다. Command 객체는 수신자의 메서드를 호출하기 위한 인터페이스를 정의하고, 실제 요청을 처리하는 수신자 객체와 연결됩니다. 클라이언트는 Command 객체를 생성하고, 해당 객체를 실행하거나 필요에 따라 취소할 수 있습니다.

Command 패턴은 많은 기능을 추상화할 수 있으며, 이력을 관리하기 위한 Undo/Redo, 작업 대기열 및 스케줄링, 트랜잭션 처리 등 다양한 기능을 구현할 수 있습니다.


// 수신자 (Receiver)
class Light {
  on() {
    console.log("조명을 켭니다.");
  }

  off() {
    console.log("조명을 끕니다.");
  }
}

// Command 인터페이스
interface Command {
  execute(): void;
}

// LightOnCommand: 조명 켜기 명령
class LightOnCommand implements Command {
  private light: Light;

  constructor(light: Light) {
    this.light = light;
  }

  execute() {
    this.light.on();
  }
}

// LightOffCommand: 조명 끄기 명령
class LightOffCommand implements Command {
  private light: Light;

  constructor(light: Light) {
    this.light = light;
  }

  execute() {
    this.light.off();
  }
}

// Invoker (클라이언트)
class RemoteControl {
  private onCommand: Command;
  private offCommand: Command;

  setOnCommand(command: Command) {
    this.onCommand = command;
  }

  setOffCommand(command: Command) {
    this.offCommand = command;
  }

  turnOn() {
    this.onCommand.execute();
  }

  turnOff() {
    this.offCommand.execute();
  }
}

// 클라이언트에서 사용 예시
const light = new Light();
const lightOnCommand = new LightOnCommand(light);
const lightOffCommand = new LightOffCommand(light);

const remoteControl = new RemoteControl();
remoteControl.setOnCommand(lightOnCommand);
remoteControl.setOffCommand(lightOffCommand);

remoteControl.turnOn();
// "조명을 켭니다."

remoteControl.turnOff();
// "조명을 끕니다."

Command 패턴 예시

Command 패턴을 사용하여 파일 처리 시스템을 구현하는 예시입니다. Command 인터페이스는 파일 작업을 수행하는 execute 메서드를 정의하고, 실제 파일 작업을 처리하는 파일 시스템 객체와 연결됩니다. 클라이언트는 Command 객체를 생성하고, 해당 객체를 실행하거나 필요에 따라 롤백할 수 있습니다.


interface Command {
  execute(): void;
  rollback(): void;
}

class FileSystem {
  private fileName: string;

  constructor(fileName: string) {
    this.fileName = fileName;
  }

  createFile() {
    console.log(`[${this.fileName}] 파일을 생성합니다.`);
  }

  deleteFile() {
    console.log(`[${this.fileName}] 파일을 삭제합니다.`);
  }
}

class CreateFileCommand implements Command {
  private fileSystem: FileSystem;

  constructor(fileSystem: FileSystem) {
    this.fileSystem = fileSystem;
  }

  execute() {
    this.fileSystem.createFile();
  }

  rollback() {
    this.fileSystem.deleteFile();
  }
}

class DeleteFileCommand implements Command {
  private fileSystem: FileSystem;

  constructor(fileSystem: FileSystem) {
    this.fileSystem = fileSystem;
  }

  execute() {
    this.fileSystem.deleteFile();
  }
  
  rollback() {
    this.fileSystem.createFile();
  }
}

class FileInvoker {
  private commands: Command[] = [];

  addCommand(command: Command) {
    this.commands.push(command);
  }

  executeCommands() {
    this.commands.forEach((command) => command.execute());
    this.commands = [];
  }

  rollbackCommands() {
    this.commands.reverse().forEach((command) => command.rollback());
    this.commands = [];
  }
}

// 클라이언트에서 사용 예시
const fileSystem = new FileSystem("file.txt");

const createCommand = new CreateFileCommand(fileSystem);
const deleteCommand = new DeleteFileCommand(fileSystem);

const fileInvoker = new FileInvoker();
fileInvoker.addCommand(createCommand);
fileInvoker.addCommand(deleteCommand);

fileInvoker.executeCommands();
// "[file.txt] 파일을 생성합니다."
// "[file.txt] 파일을 삭제합니다."

fileInvoker.rollbackCommands();
// "[file.txt] 파일을 생성합니다."

Template Method 패턴

Template Method 패턴은 알고리즘의 구조를 메서드에 정의하고, 일부 단계를 서브클래스에서 구현할 수 있게 하는 디자인 패턴입니다. Template Method 패턴을 사용하면 알고리즘의 개요는 공통으로 유지되면서 각 단계의 구체적인 구현은 서브클래스에서 제공될 수 있습니다.

Template Method 패턴은 상위 클래스에 알고리즘의 골격을 정의한 후, 다른 단계들을 추상 메서드로 선언하거나 기본 구현을 제공합니다. 이렇게 정의된 클래스는 클라이언트에게 알고리즘의 구조와 일부 단계의 구현을 제공하면서, 나머지 단계를 서브클래스가 구현하도록 합니다.

Template Method 패턴은 알고리즘의 일부를 재사용하고, 일부 단계의 동작을 다양하게 구현하고 싶을 때 유용합니다. 또한 서브클래스에서 구체적인 구현을 제공할 수 있기 때문에 유연성과 확장성을 제공합니다.


// 상위 클래스
abstract class AbstractClass {
  // 알고리즘의 구조를 정의하는 Template Method
  templateMethod() {
    this.primitiveOperation1();
    this.primitiveOperation2();
  }

  // 하위 클래스에서 구현해야 하는 추상 메서드
  abstract primitiveOperation1(): void;
  
  abstract primitiveOperation2(): void;
}

// 하위 클래스
class ConcreteClass extends AbstractClass {
  primitiveOperation1() {
    console.log("ConcreteClass의 primitiveOperation1 메서드를 실행합니다.");
  }

  primitiveOperation2() {
    console.log("ConcreteClass의 primitiveOperation2 메서드를 실행합니다.");
  }
}

// 클라이언트에서 사용 예시
const concreteClass = new ConcreteClass();
concreteClass.templateMethod();
// "ConcreteClass의 primitiveOperation1 메서드를 실행합니다."
// "ConcreteClass의 primitiveOperation2 메서드를 실행합니다."

Template Method 패턴 예시

Template Method 패턴을 사용하여 음료 제조 프로세스를 구현하는 예시입니다. 상위 클래스에는 음료 제조 프로세스의 골격이 정의되어 있고, 하위 클래스에서는 각 음료의 특정 단계를 구체적으로 구현합니다.


// 상위 클래스
abstract class Beverage {
  prepareRecipe() {
    this.boilWater();
    this.brew();
    this.pourInCup();
    this.addCondiments();
  }

  abstract brew(): void;
  abstract addCondiments(): void;

  boilWater() {
    console.log("물을 끓입니다.");
  }

  pourInCup() {
    console.log("컵에 따릅니다.");
  }
}

// 하위 클래스 - 커피
class Coffee extends Beverage {
  brew() {
    console.log("커피를 내립니다.");
  }

  addCondiments() {
    console.log("설탕과 우유를 추가합니다.");
  }
}

// 하위 클래스 - 차
class Tea extends Beverage {
  brew() {
    console.log("차를 우립니다.");
  }

  addCondiments() {
    console.log("레몬을 추가합니다.");
  }
}

// 클라이언트에서 사용 예시
const coffee = new Coffee();
coffee.prepareRecipe();
// "물을 끓입니다."
// "커피를 내립니다."
// "컵에 따릅니다."
// "설탕과 우유를 추가합니다."

const tea = new Tea();
tea.prepareRecipe();
// "물을 끓입니다."
// "차를 우립니다."
// "컵에 따릅니다."
// "레몬을 추가합니다."

Mediator 패턴

Mediator 패턴은 객체들 간의 상호작용을 캡슐화하여 객체 간의 결합도를 낮추는 디자인 패턴입니다. Mediator 패턴은 객체들이 직접 통신하는 대신 중개자 객체를 통해 통신하도록 하는 방식으로 동작합니다. 이를 통해 객체들은 다양한 형태로 상호작용할 수 있으며, 변경이 발생할 때도 중개자 객체만 수정하면 되기 때문에 유연성과 확장성을 제공합니다.

Mediator 패턴은 중개자 객체를 통해 객체들 사이의 통신을 관리하고, 객체들이 직접적으로 통신하지 않도록 합니다. 중개자 객체는 객체들의 상태를 관리하고 변경 시에 다른 객체들에게 알리며, 객체들 간의 복잡한 상호작용을 관리합니다. 이를 통해 객체들은 다른 객체의 존재를 알 필요 없이 중개자 객체와만 상호작용하면 되기 때문에 결합도가 낮아집니다.

Mediator 패턴은 복잡한 상호작용을 관리하는 경우나 애플리케이션의 확장성을 고려할 때 유용합니다. 중개자 객체를 사용하여 객체들 간의 결합도를 낮추고, 객체들 간의 상호작용을 한 곳에서 관리함으로써 코드의 복잡성을 줄일 수 있습니다.


// Mediator 인터페이스
interface Mediator {
  send(message: string, colleague: Colleague): void;
}

// Colleague 추상 클래스
abstract class Colleague {
  protected mediator: Mediator;

  constructor(mediator: Mediator) {
    this.mediator = mediator;
  }

  abstract receive(message: string): void;
}

// ConcreteColleague 클래스
class ConcreteColleague extends Colleague {
  receive(message: string) {
    console.log(`ConcreteColleague가 메시지를 수신했습니다: ${message}`);
  }

  send(message: string) {
    this.mediator.send(message, this);
  }
}

// ConcreteMediator 클래스
class ConcreteMediator implements Mediator {
  private colleague1: Colleague;
  private colleague2: Colleague;

  setColleague1(colleague1: Colleague) {
    this.colleague1 = colleague1;
  }

  setColleague2(colleague2: Colleague) {
    this.colleague2 = colleague2;
  }

  send(message: string, colleague: Colleague) {
    if (colleague === this.colleague1) {
      this.colleague2.receive(message);
    } else {
      this.colleague1.receive(message);
    }
  }
}

// 클라이언트에서 사용 예시
const mediator = new ConcreteMediator();

const colleague1 = new ConcreteColleague(mediator);
const colleague2 = new ConcreteColleague(mediator);

mediator.setColleague1(colleague1);
mediator.setColleague2(colleague2);

colleague1.send("안녕하세요!");
// "ConcreteColleague가 메시지를 수신했습니다: 안녕하세요!"

colleague2.send("반갑습니다!");
// "ConcreteColleague가 메시지를 수신했습니다: 반갑습니다!"

Mediator 패턴 예시

Mediator 패턴을 사용하여 채팅방 시스템을 구현하는 예시입니다. Mediator 인터페이스는 채팅방에서 사용되는 메시지를 보내고 받는 기능을 정의하고, ConcreteMediator 클래스는 실제로 메시지를 중개하는 역할을 합니다. Colleague 추상 클래스는 채팅 방에서 사용되는 사용자를 나타내며, ConcreteColleague 클래스는 실제 사용자를 구현합니다.


// Mediator 인터페이스
interface ChatMediator {
sendMessage(message: string, sender: ChatUser): void;
}

// Colleague 추상 클래스
abstract class ChatUser {
protected mediator: ChatMediator;
protected name: string;

constructor(mediator: ChatMediator, name: string) {
this.m

Leave a Comment