Strategy
Strategy Pattern이란?
전략 패턴Strategy Pattern은 객체의 행동을 결정하는 알고리즘을 별도의 클래스strategy 로 캡슐화하고, 런타임에 알고리즘을 선택하여 사용할 수 있도록 하는 패턴입니다. 이 패턴을 사용하면, 동일한 작업을 수행하더라도 다양한 알고리즘을 쉽게 교체하거나 추가할 수 있으며, 코드의 재사용성과 확장성을 높일 수 있습니다.
Strategy Pattern의 필요성
프로그램이 복잡해짐에 따라, 동일한 작업을 여러 가지 방법으로 수행해야 하는 상황이 발생할 수 있습니다. 예를 들어, 도서관 관리 시스템에서 도서를 정렬하는 방식이 여러 가지 있을 수 있습니다. 이를 클래스 내부에 직접 구현하면 코드가 복잡해지고, 새로운 정렬 방식을 추가할 때마다 기존 코드를 수정해야 하므로 유지보수가 어려워집니다. 전략 패턴을 사용하면, 서로 다른 알고리즘을 별도의 클래스로 분리하고, 런타임에 필요에 따라 알고리즘을 선택할 수 있습니다. 이를 통해 코드의 유연성과 유지보수성을 크게 향상시킬 수 있습니다.
Strategy Pattern 구조
- Client → Context
- 클라이언트는
Context
에 사용할 전략Strategy을 설정합니다(Set Strategy
). Context
는 설정된 전략에 따라 작업을 실행합니다(Execute Operation
).- 클라이언트가 호출하는 것은
Context
내부에 설정된Strategy
인터페이스입니다.
- 클라이언트는
- Context → Strategy
Context
는 현재 설정된 전략을 호출하여 알고리즘을 실행합니다(Execute Algorithm
).
- Strategy ← ConcreteStrategyA / ConcreteStrategyB / ConcreteStrategyC
Strategy
는 인터페이스 또는 추상 클래스로, 다양한 전략을 정의합니다.- 각
ConcreteStrategy
는Strategy
를 구현하여 고유한 알고리즘을 제공합니다.
Strategy Pattern의 구성 요소
전략 패턴은 주로 다음과 같은 구성 요소로 이루어져 있습니다:
컨텍스트Context
클라이언트가 사용하는 인터페이스로, 전략을 통해 다양한 알고리즘을 사용할 수 있게 합니다.
전략Strategy
알고리즘을 정의하는 인터페이스입니다. 이 인터페이스는 다양한 알고리즘이 구현해야 하는 공통 메서드를 정의합니다.
구체적인 전략Concrete Strategy
전략 인터페이스를 구현하는 클래스입니다. 각 클래스는 특정 알고리즘을 구현합니다.
Strategy Pattern 적용
잘못된 알고리즘 구현 방식
전략 패턴을 사용하지 않고 여러 알고리즘을 하나의 클래스에 직접 구현하는 경우, 코드가 복잡해지고 유지보수가 어려워집니다. 아래는 도서관 관리 시스템에서 도서를 정렬하는 여러 가지 알고리즘을 하나의 클래스에 직접 구현한 잘못된 예시입니다.
public class BookSorter
{
public enum SortType
{
ByTitle,
ByAuthor,
ByYear
}
public void Sort(List<Book> books, SortType sortType)
{
if (sortType == SortType.ByTitle)
{
books.Sort((x, y) => x.Title.CompareTo(y.Title));
}
else if (sortType == SortType.ByAuthor)
{
books.Sort((x, y) => x.Author.CompareTo(y.Author));
}
else if (sortType == SortType.ByYear)
{
books.Sort((x, y) => x.Year.CompareTo(y.Year));
}
}
}
복잡성 증가
여러 알고리즘을 하나의 메서드에서 처리하려면, 조건문이 많아지고 코드가 복잡해집니다. 새로운 정렬 방식이 추가될 때마다 조건문을 수정하거나 추가해야 하므로 유지보수가 어렵습니다.
단일 책임 원칙SRP 위반
BookSorter
클래스는 정렬 방식을 선택하는 책임과 알고리즘을 구현하는 책임을 동시에 가지고 있습니다. 이는 SRP를 위반하여 클래스의 역할이 모호해집니다.
Strategy Pattern 적용 예시
전략 패턴을 사용하면, 서로 다른 알고리즘을 별도의 클래스로 분리하고, 런타임에 원하는 알고리즘을 선택할 수 있습니다.
// 전략 인터페이스
public interface IBookSortStrategy
{
void Sort(List<Book> books);
}
// 구체적인 전략 클래스: 제목으로 정렬
public class TitleSortStrategy : IBookSortStrategy
{
public void Sort(List<Book> books)
{
books.Sort((x, y) => x.Title.CompareTo(y.Title));
}
}
// 구체적인 전략 클래스: 저자명으로 정렬
public class AuthorSortStrategy : IBookSortStrategy
{
public void Sort(List<Book> books)
{
books.Sort((x, y) => x.Author.CompareTo(y.Author));
}
}
// 구체적인 전략 클래스: 출판 연도로 정렬
public class YearSortStrategy : IBookSortStrategy
{
public void Sort(List<Book> books)
{
books.Sort((x, y) => x.Year.CompareTo(y.Year));
}
}
// 컨텍스트 클래스
public class BookSorter
{
private IBookSortStrategy _sortStrategy;
public BookSorter(IBookSortStrategy sortStrategy)
{
_sortStrategy = sortStrategy;
}
public void SetSortStrategy(IBookSortStrategy sortStrategy)
{
_sortStrategy = sortStrategy;
}
public void SortBooks(List<Book> books)
{
_sortStrategy.Sort(books);
}
}
// 패턴 사용 예시
public class Program
{
public static void Main(string[] args)
{
List<Book> books = new List<Book>
{
new Book("Clean Code", "Robert C. Martin", 2008),
new Book("The Pragmatic Programmer", "Andrew Hunt", 1999),
new Book("Design Patterns", "Erich Gamma", 1994)
};
// 제목으로 정렬
BookSorter sorter = new BookSorter(new TitleSortStrategy());
sorter.SortBooks(books);
DisplayBooks(books);
// 저자명으로 정렬
sorter.SetSortStrategy(new AuthorSortStrategy());
sorter.SortBooks(books);
DisplayBooks(books);
// 출판 연도로 정렬
sorter.SetSortStrategy(new YearSortStrategy());
sorter.SortBooks(books);
DisplayBooks(books);
}
public static void DisplayBooks(List<Book> books)
{
foreach (var book in books)
{
Console.WriteLine($"{book.Title} by {book.Author} ({book.Year})");
}
Console.WriteLine();
}
}
Strategy Pattern 장단점
장점
유연성
새로운 알고리즘을 쉽게 추가할 수 있으며, 기존 코드를 수정하지 않고도 다양한 알고리즘을 사용할 수 있습니다.
단일 책임 원칙SRP 준수
각 알고리즘을 별도의 클래스에 분리하여, 각 클래스가 하나의 책임만을 가지도록 설계할 수 있습니다.
재사용성
알고리즘이 별도의 클래스로 분리되어 있어, 다양한 컨텍스트에서 쉽게 재사용할 수 있습니다.
단점
클래스 수 증가
각 알고리즘마다 별도의 클래스를 생성해야 하므로, 클래스의 수가 증가할 수 있습니다.
복잡성 증가
전략 패턴을 과도하게 사용하면 코드 구조가 복잡해질 수 있습니다. 간단한 경우에는 오히려 오버엔지니어링이 될 수 있습니다.
맺음말
전략 패턴은 다양한 알고리즘을 유연하게 관리하고, 필요에 따라 동적으로 알고리즘을 변경할 수 있는 강력한 패턴입니다. 도서관 관리 시스템과 같은 경우, 다양한 정렬 방식이나 다른 알고리즘을 사용해야 할 때 전략 패턴을 사용하면 코드의 유연성과 유지보수성을 크게 향상시킬 수 있습니다. 다만, 상황에 따라 클래스 수와 복잡성이 증가할 수 있으므로, 필요할 때 신중하게 적용하는 것이 중요합니다.
심화 학습
동적 전략 선택
일반적으로 전략 패턴에서는 Context
가 생성 시점에 전략을 설정하지만, 런타임에 전략을 동적으로 변경할 수 있습니다. 이를 통해 조건에 따라 실행 중에 알고리즘을 전환하는 것이 가능합니다.
public class Context
{
private IStrategy _strategy;
public Context(IStrategy strategy)
{
_strategy = strategy;
}
public void SetStrategy(IStrategy strategy)
{
_strategy = strategy; // 런타임에 전략 변경 가능
}
public void ExecuteStrategy()
{
_strategy.Execute();
}
}
캐싱
전략의 상태 관리
전략이 상태를 갖도록 설계할 수 있습니다. 예를 들어, 캐시된 값을 재사용하거나, 이전 실행 결과를 기반으로 다음 동작을 결정할 수 있습니다.
public class CachingStrategy : IStrategy
{
private Dictionary<int, int> _cache = new();
public void Execute()
{
if (_cache.TryGetValue(10, out int result))
{
Console.WriteLine($"Cached Result: {result}");
}
else
{
result = 10 * 10;
_cache[10] = result;
Console.WriteLine($"Computed Result: {result}");
}
}
}
알고리즘 캐싱
동일한 입력에 대해 반복적으로 계산되는 경우, 결과를 캐싱하여 불필요한 전략 실행을 줄일 수 있습니다.
전략 Factory와 DI 활용
많은 전략을 가진 경우, 팩토리 패턴과 결합하여 전략 객체를 동적으로 생성하거나 종속성 주입DI을 활용할 수 있습니다. 이를 통해 전략 생성 로직을 중앙화하고 코드 중복을 줄일 수 있습니다.
public class StrategyFactory
{
public static IStrategy GetStrategy(string type)
{
return type switch
{
"A" => new ConcreteStrategyA(),
"B" => new ConcreteStrategyB(),
_ => throw new ArgumentException("Invalid strategy type")
};
}
}
전략 패턴과 병렬 처리
전략이 독립적인 작업을 수행한다면 병렬로 실행하여 성능을 최적화할 수 있습니다. 예를 들어, 데이터 처리를 위한 여러 알고리즘을 동시에 실행한 후 최적의 결과를 선택할 수 있습니다.
public class ParallelStrategy : IStrategy
{
public void Execute()
{
Parallel.Invoke(
() => Console.WriteLine("Task 1 executed"),
() => Console.WriteLine("Task 2 executed"),
() => Console.WriteLine("Task 3 executed")
);
}
}
6. 전략 패턴의 확장
6.1 전략 패턴과 템플릿 메서드 패턴의 결합
- 전략 패턴과 템플릿 메서드 패턴을 결합하여, 기본 로직은 고정하되, 세부적인 알고리즘은 전략으로 대체 가능합니다.
6.2 상태 패턴과의 조합
- 전략 패턴을 사용하여 상태 전환 로직을 캡슐화하면, 상태 변화에 따라 전략이 변경될 수 있습니다. 코드 예시
public interface IStateStrategy
{
void Handle();
}
public class ActiveState : IStateStrategy
{
public void Handle() => Console.WriteLine("Handling Active State");
}
public class InactiveState : IStateStrategy
{
public void Handle() => Console.WriteLine("Handling Inactive State");
}