전략(Strategy) 패턴은 프로그램이 실행되는 도중에 알고리즘을 선택해서 적용하는 디자인 패턴이다. 어떤 기능이 동작할 때 다양한 로직으로 실행이 가능하면서 이를 구분해야하는 경우에 주로 사용한다. 전략 패턴은 다음과 같은 큰 특징이 있다.
각각의 알고리즘(전략)을 별도의 클래스로 캡슐화하면 새로운 알고리즘 방식을 추가할 때 기존의 코드를 변경하지 않고 새로운 클래스를 추가하여 구현하게 된다.
프로그램 실행 중에 특정 상황에 따라 필요한 전략을 선택해 적용할 수 있다.
인터페이스를 만들고 이를 구현함으로써 알고리즘을 정의하게 되는데 덕분에 각각의 전략을 서로 치환할 수 있다.
따라서 전략 패턴은 SOLID 5원칙을 만족하는데 큰 도움을 주는 디자인 패턴이다.
예를 들어서 살펴보자. 게임에서 아이템을 저장하는 리스트가 있다. 이 리스트는 아이템의 이름부터 소지 갯수 등 다양한 방법으로 정렬할 수 있을 것이다.
class Item
{
public int Id;
public string Name;
public int SellPrice;
public int Count;
}
class Inventory
{
public List<Item> Slots;
public Inventory()
{
Slots = new List<Item>();
Slots.Add(new Item() { Name = "빨간 포션", SellPrice = 20, Count = 100 });
Slots.Add(new Item() { Name = "파란 포션", SellPrice = 40, Count = 200 });
Slots.Add(new Item() { Name = "주황 포션", SellPrice = 80, Count = 50 });
Slots.Add(new Item() { Name = "하얀 포션", SellPrice = 150, Count = 25 });
}
public void SortByName()
{
Slots.Sort((a, b) => (a.Name.CompareTo(b.Name)));
}
public void SortByCount()
{
Slots.Sort((a, b) => (a.Count.CompareTo(b.Count)));
}
public void ShowInventory()
{
foreach (Item item in Slots)
{
Console.WriteLine($"{item.Name} x{item.Count}개 | 판매가 {item.SellPrice}");
}
Console.WriteLine("==========");
}
}
class Program
{
static void Main(string[] args)
{
Inventory inventory = new Inventory();
inventory.ShowInventory();
inventory.SortByCount();
inventory.ShowInventory();
}
}

현재의 코드는 Inventory클래스에서 Item의 리스트인 Slots을 관리하며 출력과 정렬까지 모두 담당한다. 이는 SRP(단일책임원칙)을 위배할 여지가 있으므로 정렬을 담당하는 InventorySorter 클래스를 따로 두고자한다. 그리고 Inventory 클래스는 Item의 리스트에 아이템을 넣는것과 출력만 담당할 것이다.
class Item
{
public int Id;
public string Name;
public int SellPrice;
public int Count;
}
class Inventory
{
private List<Item> slots;
private InventorySorter sorter;
public Inventory()
{
slots = new List<Item>();
sorter = new InventorySorter();
}
public void SortInventory(int sortType)
{
switch (sortType)
{
case 0:
sorter.SortByName(slots);
break;
case 1:
sorter.SortByCount(slots);
break;
}
}
public void AddItem(Item item)
{
slots.Add(item);
}
public void ShowInventory()
{
foreach (Item item in slots)
{
Console.WriteLine($"{item.Name} x{item.Count}개 | 판매가 {item.SellPrice}");
}
Console.WriteLine("==========");
}
}
class InventorySorter
{
public void SortByName(List<Item> slots)
{
slots.Sort((a, b) => (a.Name.CompareTo(b.Name)));
}
public void SortByCount(List<Item> slots)
{
slots.Sort((a, b) => (b.Count.CompareTo(a.Count)));
}
}
class Program
{
static void Main(string[] args)
{
Inventory inventory = new Inventory();
inventory.AddItem(new Item() { Name = "빨간 포션", SellPrice = 20, Count = 100 });
inventory.AddItem(new Item() { Name = "파란 포션", SellPrice = 40, Count = 200 });
inventory.AddItem(new Item() { Name = "주황 포션", SellPrice = 80, Count = 50 });
inventory.AddItem(new Item() { Name = "하얀 포션", SellPrice = 150, Count = 25 });
inventory.ShowInventory();
inventory.SortInventory(1);
inventory.ShowInventory();
}
}
이번 코드는 새로운 정렬 알고리즘이 필요한 상황에는 InventorySorter와 Inventory클래스를 고쳐야한다. 그리고 런타임에서 정렬 전략(방식)을 동적으로 변경할 수 없어서 그때그때 switch문 등 분기를 나누어 다른 메서드를 불러주어야 하므로 이것은 SRP(단일책임원칙)와 OCP(개방폐쇄원칙)를 위반한다. 이 문제를 고치기 위해 ISortStrategy 인터페이스와 그 세부 구현들을 만들어 전략 패턴을 적용해보자.
interface ISortStrategy
{
void Sort(List<Item> slots);
}
class SortByNameStrategy : ISortStrategy
{
public void Sort(List<Item> slots)
{
slots.Sort((a, b) => (a.Name.CompareTo(b.Name)));
}
}
class SortByCountStrategy : ISortStrategy
{
public void Sort(List<Item> slots)
{
slots.Sort((a, b) => (b.Count.CompareTo(a.Count)));
}
}
이렇게 ISortStrategy 인터페이스를 설계하고 정렬방식에 따라 내부 로직을 다르게 한 SortByNameStrategy와 SortByCountStrategy를 구현한다.
추후 새로운 정렬전략이 필요하다면 SortByPriceStrategy 등을 추가로 구현하면되어 기존에 잘 동작하던 다른 클래스를 수정할 필요가 없어져 OCP(개방폐쇄원칙)를 만족한다.
class Item
{
public int Id;
public string Name;
public int SellPrice;
public int Count;
}
class Inventory
{
private List<Item> slots;
private ISortStrategy sortStrategy;
public Inventory()
{
slots = new List<Item>();
}
public void SetSortStrategy(ISortStrategy strategy)
{
sortStrategy = strategy;
}
public void SortInventory()
{
sortStrategy.Sort(slots);
}
public void AddItem(Item item)
{
slots.Add(item);
}
public void ShowInventory()
{
foreach (Item item in slots)
{
Console.WriteLine($"{item.Name} x{item.Count}개 | 판매가 {item.SellPrice}");
}
Console.WriteLine("==========");
}
}
class Program
{
static void Main(string[] args)
{
Inventory inventory = new Inventory();
inventory.AddItem(new Item() { Name = "빨간 포션", SellPrice = 20, Count = 100 });
inventory.AddItem(new Item() { Name = "파란 포션", SellPrice = 40, Count = 200 });
inventory.AddItem(new Item() { Name = "주황 포션", SellPrice = 80, Count = 50 });
inventory.AddItem(new Item() { Name = "하얀 포션", SellPrice = 150, Count = 25 });
inventory.ShowInventory();
inventory.SetSortStrategy(new SortByNameStrategy());
inventory.SortInventory();
inventory.ShowInventory();
inventory.SetSortStrategy(new SortByCountStrategy());
inventory.SortInventory();
inventory.ShowInventory();
}
}
모든 정렬 알고리즘을 담고있던 InventorySorter는 더이상 필요가 없어졌다.
inventory.SetSortStrategy()메서드에 새로운 ISortStrategy 구현체를 만들어넣어 새로운 전략을 동적으로 할당하고 Sort()메서드를 호출해 해당 구현체의 알고리즘을 수행하게 된다.
기존 코드의 수정없이 새로운 알고리즘을 만들고 인벤토리의 정렬 방식을 실행중에 동적으로 선정할 수 있다는 장점이 있지만 전략 패턴에 익숙하지 않은 개발자에게는 코드가 더 복잡해보일 수 있다. 또, 클래스의 개수가 늘어나 간단한 기능을 구현할 때는 과도한 설계로 간주될 가능성도 충분하다. 복잡성을 굳이 증가시키지 않도록 상황에 맞게 적용시키는 것이 중요할 것 같다.