전략 패턴
여러 알고리즘을 캡슐화하고 상호 교환 가능하게 만드는 패턴. 알고리즘을 사용하는 클라이언트와 상관 없이 독립적으로 알고리즘을 다양하게 변경할 수 있다.
구조
- Strategy - 제공하는 모든 알고리즘에 대한 공통 연산들을 인터페이스로 정의한다. Context 클래스는 ConcreteStrategy 클래스에서 정의한 인터페이스를 통해 실제 알고리즘을 사용한다.
- ConcreteStrategy - Strategy의 인터페이스를 실제 알고리즘으로 구현한다.
- Context - ConcreteStrategy 객체를 통해 구성된다. Strategy 객체에 대한 참조를 관리하고, 실제로는 Strategy 서브클래스(ConcreteStrategy)의 인스턴스를 갖고 있음으로써 구체화한다. 또한 Strategy 객체가 자료에 접근하는 데 필요한 인터페이스를 정의한다.
기본 인터페이스
// 전략 인터페이스
public interface IStrategy
{
void Execute();
}
// 전략 1: 구체적인 알고리즘 구현
public class ConcreteStrategy1 : IStrategy
{
public void Execute()
{
Console.WriteLine("전략 1을 실행합니다.");
}
}
// 전략 2: 다른 구체적인 알고리즘 구현
public class ConcreteStrategy2 : IStrategy
{
public void Execute()
{
Console.WriteLine("전략 2를 실행합니다.");
}
}
// 컨텍스트 클래스
public class Context
{
private IStrategy _strategy;
public Context(IStrategy strategy)
{
_strategy = strategy;
}
// 전략 교체 메서드
public void SetStrategy(IStrategy strategy)
{
_strategy = strategy;
}
// 전략 실행 메서드
public void ExecuteStrategy()
{
_strategy.Execute();
}
}
class Program
{
static void Main(string[] args)
{
// 전략 생성
IStrategy strategy1 = new ConcreteStrategy1();
IStrategy strategy2 = new ConcreteStrategy2();
// 컨텍스트 생성
Context context = new Context(strategy1);
// 처음에는 전략 1을 실행
context.ExecuteStrategy();
// 전략 변경 후 실행
context.SetStrategy(strategy2);
context.ExecuteStrategy();
/*
* 실행 결과
* 전략 1을 실행합니다.
* 전략 2를 실행합니다.
*/
}
}
왜 사용해야 할까?
전략 패턴의 핵심은 알고리즘의 사용 방식과 구현을 분리하는 것이다. 전략 패턴은 알고리즘을 각각 별도의 클래스로 정의하고, 이들을 전략 인터페이스를 통해 통합한다. 이 인터페이스는 실행 시에 교체될 수 있는 알고리즘을 정의하는 방법을 제공한다.
게임을 예시로 한 가지 예시를 들어보겠다. RPG 게임에서 캐릭터의 조작 방식을 구현한다고 할 때, 기본적인 기능으론 공격, 이동 기능이 존재할 것이다.
// 캐릭터 클래스
public class Character
{
public void Attack()
{
// Logic
}
public void Move()
{
// Logic
}
}
개발자는 캐릭터의 공격 기능에 특색을 추가하고 싶었다. 그래서 소울라이크계의 유명한 게임 중 하나인 엘든링의 공격 시스템을 따라 구현하려고 한다.
엘든링에서는 무기가 변경되면 캐릭터의 공격 방식이 무기에 맞게 변경되는 시스템이 존재한다. 플레이 중 장비할 수 있는 무기 종류가 수십개가 넘고, 이에 맞춰 각각의 공격 모션을 취할 수 있기 때문에 다양한 플레이 방식을 제공할 수 있다.
개발자는 이를 구현하기 위해 현재 무기에 따라 다른 공격 방식을 실행하도록 조건문을 사용해 작성하려고 했다. 그러나 조건문을 사용하는 접근 방식은 여러 문제를 야기한다.
먼저, 무기의 종류가 많아질수록 조건문이 거대해지며, 이는 코드의 복잡성을 증가시킨다. 새로운 무기를 추가할 때마다 새로운 분기를 작성해야 하며, 공격 방식이 변경될 때는 Character 클래스의 코드를 직접 수정해야 한다. 이는 개방-폐쇄 원칙을 위배하는 것으로, 새로운 기능 추가 시 기존 코드를 자주 변경해야 하는 문제를 발생시킨다.
또한, Character 클래스가 여러 책임을 담당하게 되어 단일 책임 원칙도 위배한다. 이러한 방식은 장기적으로 유지보수와 확장성 측면에서 비효율적이다.
public class Character
{
public string Weapon { get; set; }
public Character(string weapon)
{
Weapon = weapon;
}
public void Attack()
{
if (Weapon == "GreatSword")
{
Console.WriteLine("Attacking with a great sword.");
}
else if (Weapon == "DualDaggers")
{
Console.WriteLine("Attacking with dual daggers.");
}
else if (Weapon == "LongBow")
{
Console.WriteLine("Attacking with a long bow.");
}
//...
}
}
전략 패턴을 사용하여 Character 클래스를 구현할 경우 무기에 따른 공격 모션을 유연하게 바꿀 수 있다. 각 무기에 해당하는 공격 전략을 별도의 클래스로 구현할 수 있다.
Strategy
// 대검 공격 전략
public class SwordAttack : IAttackStrategy
{
public void Attack()
{
Console.WriteLine("Attacking with a sword!");
}
}
// 단검 공격 전략
public class DaggerAttack : IAttackStrategy
{
public void Attack()
{
Console.WriteLine("Attacking with a dagger!");
}
}
IAttackStrategy 인터페이스는 모든 공격 전략이 따라야 할 규약을 정의한다. 여기에는 Attack 메서드가 포함된다.
SwordAttack, DaggerAttack 클래스는 IAttackStrategy 인터페이스를 구현한다. 각 클래스는 무기에 맞는 고유한 공격 방식을 정의한다. SwordAttack에서는 검으로 공격하는 방식을, DaggerAttack에서는 단검으로 공격하는 방식을 구현한다.
Weapon
public class Weapon
{
private IAttackStrategy attackStrategy;
public Weapon(IAttackStrategy strategy)
{
attackStrategy = strategy;
}
public void Attack()
{
attackStrategy.Attack();
}
// 원하는 공격 전략으로 변경할 수 있다.
public void SetAttackStrategy(IAttackStrategy strategy)
{
attackStrategy = strategy;
}
}
각 무기의 전략은 무기별로 설정되어 있다. 그렇기 때문에 Weapon 클래스는 IAttackStrategy를 참조를 관리하고, 실제 전략 인스턴스를 저장한다. 무기의 종류는 다양하되, 같은 종류의 무기는 같은 방식으로 공격한다면 같은 전략을 Weapon에 저장함으로써 코드의 재사용성을 높일 수 있다.
또한, 이후 공격 전략이 변경된다고 하더라도 해당 전략을 사용하는 코드들의 수정 없이 전략만 수정해도 되기 때문에 기능의 확장성을 보장할 수 있다.
Character
public class Character
{
private Weapon weapon;
public Character(Weapon weapon)
{
this.weapon = weapon;
}
public void SetWeapon(Weapon weapon)
{
this.weapon = weapon;
}
public void Attack()
{
weapon.Attack();
}
}
Character 클래스는 weapon을 사용하여 공격하는 방식을 구현하고 있다. Character 클래스 내에서 Attack 메서드를 호출할 때, 현재 설정된 weapon (즉, 공격 전략)을 사용하여 실제 공격을 수행한다. 이 구조를 통해, Character 클래스는 다양한 종류의 무기 (예: 검, 단검, 활)를 간단하게 교체할 수 있으며, 각 무기에 맞는 고유한 공격 방식을 적용할 수 있다.
전략 패턴을 적용한 결과 캐릭터의 행동을 변경하기 위해 캐릭터 클래스의 코드를 수정할 필요 없이, 단순히 다른 공격 전략 객체를 주입함으로써 다양한 행동을 쉽게 구현할 수 있다. 이는 코드의 유연성과 확장성을 크게 향상시키며, 새로운 공격 방식이 추가될 때 기존 코드에 영향을 주지 않고 쉽게 확장할 수 있는 구조를 제공한다(OCP 보장).
각 클래스가 하나의 기능(여기서는 특정 무기의 공격 방식)에만 집중하도록 함으로써, 코드의 단일 책임 원칙(SRP)을 준수한다. 이는 코드의 복잡성을 줄이고 각 부분의 책임을 명확히 한다.
이는 곧 코드의 가독성과 유지보수도 향상시킨다. 각 전략이 독립된 클래스로 존재하기 때문에, 해당 전략과 관련된 코드를 찾고 수정하기가 더 쉬워진다. 게임의 복잡한 로직 중에서 특정 부분만 변경하고 싶을 때, 관련 전략 클래스를 찾아 수정하기만 하면 된다.
장점
- 새로운 전략을 추가하더라도 기존 코드를 변경하지 않는다.
- 런타임 도중 전략을 변경할 수 있다.
- OCP와 SRP를 보장한다.
단점
- 코드 복잡도가 증가한다.
- 클라이언트 코드가 구체적인 전략을 알아야 한다.
전체 예시 코드
참고자료
'프로그래밍 이론 > 디자인 패턴' 카테고리의 다른 글
24. [Design Pattern] 방문자 패턴(Visitor) (0) | 2024.02.29 |
---|---|
23. [Design Pattern] 템플릿 메소드 패턴(Templete Method) (0) | 2024.02.28 |
21. [Design Pattern] 옵저버 패턴(Observer) (1) | 2024.02.26 |
20. [Design Pattern] 상태 패턴(State) (0) | 2024.02.20 |
19. [Design Pattern] 메멘토(Memento) 패턴 (0) | 2024.02.19 |