디자인 패턴

1. 전략 패턴 (Strategy Pattern)

EF DEV 2020. 12. 16. 01:43
알고리즘 군을 정의하고 각각 하나의 클래스로 캡슐화한 다음, 필요할 때 서로 교환해서 사용할 수 있게 해 준다.
- 위키백과 -

가장 먼저 알아볼 건 전략 패턴이다.

위 설명만 봐서는 무슨 소리인지 이해하기 어려울 수 있는데

이해하기 쉬운 게임에 비유해서 왜 써야 하는지부터 하나씩 알아가 보도록 하겠다.

 

 

이름 : 평범한 철 검

공격력 : 10

특수 효과 : 없음

 

위와 같은 검을 구현해본다고 생각해보자.

그러면 대략 아래와 같은 코드가 나올 것이다.

 

public enum SpecialEffect
{
    NONE,
    FIRE,
    ICE
}

public abstract class SwordBase
{
    protected string name;
    protected float damage;
    protected SpecialEffect effect;

    public abstract void UseEffect();
    public virtual void Attack() { /* 기본 공격 구현 */ }
}

public class SwordIron : SwordBase
{
    public SwordIron()
    {
        name = "평범한 철 검";
        damage = 10;
        effect = SpecialEffect.NONE;
    }
    
    public override void UseEffect() { /* 기능 구현 */ }
}

 

SwordBase 라는 추상 클래스를 기반으로 이후에 생길 특수 효과를 대비하여

상속하는 형태로 무기별로 구현하는 것이 가장 단순한 방법이다.

또는 아래와 같이 SpecialEffect 값에 따라 UseEffect 함수 내에서 분기로 처리할 수도 있다.

 

public class SwordBase
{
    /* 기타 기능 생략 (위와 동일) */
    
    public virtual void UseEffect()
    {
    	switch (effect)
        {
            case SpecialEffect.NONE:
                break;
            case SpecialEffect.FIRE:
                break;
            case SpecialEffect.ICE:
                break;
        }
    }
}

public class SwordIron : SwordBase
{
    public SwordIron()
    {
        name = "평범한 철 검";
        damage = 10;
        effect = SpecialEffect.NONE;
    }
}

여기까지 봤을 땐 큰 문제를 느끼지 못할 수도 있다.

그런데 만약 게임에 검이 아닌 활이 생겼다고 생각해보자.

 

이름 : 평범한 나무 활

원거리 공격력 : 10

특수 효과 : 없음

 

 

원거리 공격력이라는 새로운 값이 생겼고 투사체를 날려야 하기 때문에

기존의 SwordBase 를 사용할 수 없어 새로운 클래스를 만들어야 한다.

 

public abstract class BowBase
{
    protected string name;
    protected float rangeDamage;
    protected SpecialEffect effect;

    public abstract void UseEffect();
    public virtual void Attack() { /* 기본 공격 구현 */ }
}

public class BowWood : BowBase
{
    public BowWood()
    {
        name = "평범한 나무 활";
        rangeDamage = 10;
        effect = SpecialEffect.NONE;
    }
    
    public override void UseEffect() { /* 기능 구현 */ }
}

 

SwordBase 와 rangeDamage, Attack 구현부를 제외하면 큰 차이가 없다.

무기 타입으로 구분하여 데미지를 다르게 적용하는 게 더 좋은 방법이 될 수도 있지만

가장 중요한 문제는 그게 아니다.

 

이러한 구조에서 특수 효과의 종류나 장비의 종류가 수십 개라고 생각해보자.

 

만약 추상 메소드 형태의 UseEffect 라면 모든 장비마다 효과를 구현해야 하며

중복 코드 또한 엄청나게 증가하고 재사용은 어려워지며 이는 결국 유지보수의 어려움으로 이어진다.

분기 처리한 UseEffect 또한 Base 클래스에 의존적이며 확장성은 효과나 장비의 종류의 수에 반비례될 것이다.

 

여기서 확장성이 반비례된다는 건 확장을 못한다는 게 아니라

점점 한 클래스에 기능이 몰리게 되어 관리가 어렵게 되어간다는 의미이다.

 

유지보수의 어려움에 대한 감이 안 온다면 FIRE의 효과가 불 속성 추가 데미지에서

지속 화염 데미지로 바뀌었다고 생각해보자.

 

단순히 문자열 바꾸기 기능 같은 걸로 수많은 클래스를 갈아엎을 방법이 없으며

지속 데미지라는 전혀 다른 형태이기 때문에 분기 처리한 SwordBase 라면

fireTime, iceTime 과 같은 계산을 위한 변수들이 난무할 것이다.


이제 이 암울한 코드에 전략 패턴을 적용시켜보자.

 

앞선 정의를 다시 한번 살펴보면 알고리즘 군이라는 것이 등장하는데

여기서 알고리즘 군은 특수 효과와 같은 특정 기능이라고 볼 수 있다.

 

즉, 특수 효과를 각각의 클래스로 구현하고 필요할 때 서로 교환해서 사용할 수 있게 만들면 된다.

 

우선 각각의 클래스로 구현하고 서로 교환하는 구조로 만들려면

인터페이스, 또는 추상 클래스와 같은 하나의 형태로 묶어줄 것이 하나 필요하다.

여기서는 인터페이스로 구성해보겠다.

 

public enum SpecialEffect
{
    NONE,
    FIRE,
    ICE
}

public interface ISpecialEffect
{
    void UseEffect();
    
    /* 앞선 설명의 지속 데미지 같은걸 구현한다면 추가적인
       Update 함수 등이 필요할 것이다. */
}

public class SEFire : ISpecialEffect
{
    public void UseEffect() { /* 기능 구현 */ }
}

public class SEIce : ISpecialEffect
{
    public void UseEffect() { /* 기능 구현 */ }
}

public class SwordBase
{
    protected string name;
    protected float damage;
    protected SpecialEffect effect;
    protected ISpecialEffect effectFunc;

    public SwordBase(string name, float damage, SpecialEffect effect)
    {
        this.name = name;
        this.damage = damage;
        this.effect = effect;

        switch (effect)
        {
            case SpecialEffect.NONE:
                break;
            case SpecialEffect.FIRE:
                effectFunc = new SEFire();
                break;
            case SpecialEffect.ICE:
                effectFunc = new SEIce();
                break;
        }
    }

    public void UseEffect() => effectFunc?.UseEffect();
    public virtual void Attack() { /* 기본 공격 구현 */ }
}

 

ISpecialEffect 인터페이스(알고리즘 군)를 만들고 SEFire 와 SEIce 에 이를 추가하여 각각의 클래스로 구현을 나누었다.

이후 SwordBase 에서는 ISpecialEffect 타입의 effectFunc 변수가 추가되었고

생성자에서 이름, 데미지, 효과 타입을 매개변수로 받을 수 있게 변경되었다.

 

생성자에서는 이때 받은 효과 타입에 맞춰 구현한 SEFire 또는 SEIce 등을 effectFunc 변수에 생성한다.

사용 시에는 UseEffect 로 동일하게 사용할 수 있다.

 

이를 통해 특수 효과는 SwordBase 나 BowBase 에 의존적인 것이 아닌 독립적인 객체로서 사용할 수 있게 되었고

마법 스크롤 같은 전혀 다른 곳에도 쉽게 적용할 수 있을 것이다.


이제 이 패턴을 정리해보며 마치도록 하겠다.

 

  • 목적 : 코드의 재사용성을 높이고 의존성을 낮추어 각 기능을 독립적인 객체로 다룰 수 있게 한다.
  • 방법 : 알고리즘 군을 정의하고 각각의 클래스로 캡슐화한다.
  • 사용 : 캡슐화한 클래스를 교환하여 사용한다.

얼핏 보면 간단하지만 객체지향적인 설계에 있어서 가장 기본이 되는 패턴이라고도 볼 수 있겠다.

 

더보기

추가로 위 예제 코드에 대한 의문이 하나 들 수 있다.

특수 효과는 이 전략 패턴이라는 걸 써서 해결할 수 있다면

장비의 이름이나 능력치 같은 건 마찬가지로 수많은 코드가 만들어지는데 이건 어떻게 해결할 것 인가? 이다.

 

우선 실무 환경에서는 저런 식으로 데이터가 하드코딩으로 들어가지 않고

엑셀과 같은 데이터를 기반으로 읽어서 처리하기 때문에 ~Base 클래스 만으로도 구현하는데 문제가 없다.

또한 이 패턴을 사용하는 목적은 중복되는 코드를 줄이고 재사용성을 증가시키며 의존성을 낮추는 것 에 있지

클래스 자체의 수를 줄이는 것이 목적은 아니다.

 

그래서 별도의 구현이 필요하고 캡슐화가 가능한 특수 효과에 전략 패턴을 적용하는 것이다.