본문 바로가기

Backend/디자인 패턴

Strategy Pattern (전략 패턴)

전략 패턴이란 이름 그대로 전략을 쉽게 바꿀 수 있는 패턴을 말합니다.

상호 교환 가능한 개별 전략 이라는 객체들로 추상화함으로써 어떤 로직이나, 문제를 해결하는 알고리즘등의 변형을 지원합니다.

 

이 패턴에 대해서 2가지 예시를 들도록 하겠습니다.

 

import fs from 'fs';

class Config {
  private jsonData: Record<string, any>;

  constructor() {
    this.jsonData = {};
  }

  getData(key: string): any {
    return this.jsonData[key];
  }

  readJson(file: string): void {
    this.jsonData = JSON.parse(fs.readFileSync(file, 'utf-8'));
  }
}

const config = new Config();
config.readJson('xxx.json');

 

Config 객체를 통해 앱에서 사용되는 여러 설정 파라미터들을 보관하고 이에 접근할 수 있는 메서드를 제공한다고 할 때, json 파일을 읽고 이를 제공하는 객체는 위와 같이 작성할 수 있을 것입니다.

제가 일하는 회사에선 필요한 파라미터들을 json 으로만 관리하기 때문에 사실 다른건 필요 없긴 합니다만 만약 ini 파일에 대한 부분을 추가하려면 iniData 에 대한 멤버 변수를 추가하고 관련 메서드들을 추가하는 등의 짓을 해야겠죠.

 

이러한 경우 전략 패턴을 사용해 다양한 파일 형식에 대한 전략을 취할 수 있습니다.

 

import fs from 'fs';
import ini from 'ini';

interface Strategy {
  deserialize(data: any): any;
}

class JsonStrategy implements Strategy {
  deserialize(data: string) {
    return JSON.parse(data);
  }
}

class IniStrategy implements Strategy {
  deserialize(data: string) {
    return ini.parse(data);
  }
}

class Config {
  private data: Record<string, any>;
  private strategy: Strategy;

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

  getData(key: string): any {
    return this.data[key];
  }

  read(file: string): void {
    this.data = this.strategy.deserialize(fs.readFileSync(file, 'utf-8'));
  }
}

const jsonConfig = new Config(new JsonStrategy());
jsonConfig.read('xxx.json');

const iniConfig = new Config(new IniStrategy());
iniConfig.read('xxx.ini');

 

예시는 좀 억지스러울 수 있지만, 전략 패턴을 이런식으로 사용할 수 있다는 점을 중점으로 봐주시면 좋을 것 같습니다.

결국 이런 부분은 객체간의 협력으로 풀어갈 수 있는 부분입니다. 각 파일 타입별 deserialize 하는 부분을 Strategy 로 추상화하여
Config 객체와 협력하도록 구성함으로써, 앞으로 json, ini 파일 외의 타입에도 전략만 추가해 config 를 구성할 수 있게 되었습니다.

 

또 다른 예시를 보도록 하죠.

 

class StormwindAuctionHouse {
  bid(list: Array<{ id: string; money: number }>): string {
    list.sort((x, y) => y.money - x.money);
    return list[0].id;
  }
}

class IronforgeAuctionHouse {
  bid(list: Array<{ id: string; money: number }>): string {
    list.sort((x, y) => y.money - x.money);
    return list[1].id;
  }
}

const stormwindAuctionHouse = new StormwindAuctionHouse();
const stormWinner = stormwindAuctionHouse.bid([{ id: 'a', money: 3000 }, { id: 'b', money: 5000 }, { id: 'c', money: 1000 }, { id: 'd', money: 4000 }]);
console.info(stormWinner); // b

const ironforgeAuctionHouse = new IronforgeAuctionHouse();
const ironWinner = ironforgeAuctionHouse.bid([{ id: 'a', money: 3000 }, { id: 'b', money: 5000 }, { id: 'c', money: 1000 }, { id: 'd', money: 4000 }]);
console.info(ironWinner); // d

 

스톰윈드 경매장, 아이언포지 경매장 이라는 두 경매장이 있고 각각의 경매는 스톰윈드는 가장 비싼 값을 지른 사람에게,
아이언포지는 두번째로 비싼 값을 지른 사람에게 낙찰되는 시스템을 갖고 있어서 위와 같이 만들어졌습니다.

 

단순 적은 내용대로면 아무런 문제는 없지만, 경매장의 낙찰 시스템을 바꾸고 싶으면 어떻게 해야 할까요?

불가피하게 각 경매장 클래스의 bid 메서드를 수정할 수 밖에 없습니다. 사실 실무에서 이런 경우는 부지기수이긴 합니다만..

이론적으론 이러한 경우 OCP (개방-폐쇄 원칙) 를 위반했다고 합니다.

새로운 기능을 추가할 때 기존 코드 수정 없이 작업할 수 있도록 설계되어야 하는데 그렇지가 않다는 것이죠.

그럼 이 부분을 전략 패턴을 사용해 변경해 보도록 하겠습니다.

 

interface BidStrategy {
  bid(list: Array<{ id: string; money: number }>): string;
}

class FirstBidStrategy implements BidStrategy {
  bid(list: Array<{ id: string; money: number;}>): string {
    list.sort((x, y) => y.money - x.money);
    return list[0].id;
  }
}

class SecondBidStrategy implements BidStrategy {
  bid(list: Array<{ id: string; money: number;}>): string {
    list.sort((x, y) => y.money - x.money);
    return list[1].id;
  }
}

class StormwindAuctionHouse {
  private bidStrategy: BidStrategy;

  constructor(bidStrategy: BidStrategy) {
    this.bidStrategy = bidStrategy;
  }

  bid(list: Array<{ id: string; money: number }>): string {
    return this.bidStrategy.bid(list);
  }
}

class IronforgeAuctionHouse {
  private bidStrategy: BidStrategy;

  constructor(bidStrategy: BidStrategy) {
    this.bidStrategy = bidStrategy;
  }

  bid(list: Array<{ id: string; money: number }>): string {
    return this.bidStrategy.bid(list);
  }
}

const stormwindAuctionHouse = new StormwindAuctionHouse(new FirstBidStrategy());
const stormWinner = stormwindAuctionHouse.bid([{ id: 'a', money: 3000 }, { id: 'b', money: 5000 }, { id: 'c', money: 1000 }, { id: 'd', money: 4000 }]);
console.info(stormWinner); // b

const ironforgeAuctionHouse = new IronforgeAuctionHouse(new SecondBidStrategy());
const ironWinner = ironforgeAuctionHouse.bid([{ id: 'a', money: 3000 }, { id: 'b', money: 5000 }, { id: 'c', money: 1000 }, { id: 'd', money: 4000 }]);
console.info(ironWinner); // d

 

네.. 억지로 전략 패턴을 보이기 위해 작성한 예시다보니 뭔가 더 장황해진 것 같습니다만

낙찰이라는 부분이 전략이라는 형태로 빠져나오게되어 경매장이 어떤 낙찰 시스템을 갖는지에 대해 이제 아무런 부담이 없게 되었습니다.

코드를 보면서 느끼셨겠지만 이렇게 된 이상 따로 AuctionHouse 클래스 자체를 나눌 필요도 없게 되었습니다. (간단한 예시라 그렇기도 합니다)

 

중요한 점은 똑같은 문제를 해결하는데 있어 언제든지 다른 방법으로 쉽게 변경할 수 있도록 하는 디자인 패턴이며,

디자인 패턴이라는 이름을 달고 있지만 어떤 문제를 해결하는 부분을 전략이라는 객체로 나누고 다른 객체와 협력하는 설계를 하다보면
자연스럽게 따라오는 구조로 볼 수 있을 것 같습니다.