앤디가이 블로그

Unity와 C#을 통한 전략 패턴(Strategy Pattern) 사용 방법에 대해 알아보자.

 

1. 전략 패턴이란?

행동을 정의하고 캡슐화해서 각각의 행동이 추가될 때 유연하고 독립적으로 변경하여 사용할 수 있게 도와주는 패턴

특정 상황에 따라 행동을 바꾸고 싶을 때 적용하면 유용한 패턴

ex) 캐릭터가 전투 상황에 따라 무기를 교체할 때

 

2. 사용 예제 - 문제 상황

필자가 좋아하는 자동차로 상황을 만들어 보겠다.

자동차 회사들의 자동차를 시뮬레이션하는 프로그램을 제작한다고 가정해보자.

H사의 - HCar 자동차 모델이 있고, T회사의 TCar 자동차 모델이 있다. 

모든 자동차는 Display() : 자동차 외관을 보여주는 함수, Move() : 자동차가 굴러가면서 움직이는 함수

두 가지 함수를 가지고 있다고 가정해보자.

 

일반적으로 Car라는 부모 추상 클래스를 만들고, 이 Car를 상속받는 H회사의 HCar와 T회사의 TCar라는 확장 클래스를 만들 수 있을 것이다.

자동차 클래스 구조도
자동차 클래스 구조도

여기서 문제가 생겼다. 새로 생긴 자동차 회사인 G사에서 하늘을 날 수 있는 자동차가 출시된 것이다.

이럴 경우 코드를 어떻게 수정하면 좋을까?

 

3. 사용 예제 - 문제 해결 고민(상속)

위와 같은 요구사항 추가 발생시 1차적으로 생각해 볼 수 있는 방법은 부모 추상 클래스인 Car Class에 Fly() 추상 함수를 추가하고, 하위 클래스에서 Fly() 함수를 재구현 하는 방법이 있을 수 있다.

 

Fly() 함수 재구현
상속으로 구현

 

 

이럴 경우 HCar와 TCar 클래스의 Fly() 재구현 함수에서는 날지 않고, GCar에서만 하늘을 날 수 있게 재구현 해줄 수 있다.

하지만 이런 요구 사항들이 계속 늘어난다면 어떨까?

서브 클래스마다 중복되는 코드가 발생될 것이고, 재구현이 필요없는 상황에서 무조건 코드를 추가해줘야 하는 상황이 발생할 것이다.

상속을 계속 활용한다면 규격이 바뀔 때마다 프로그램에 추가했던 Fly() 메서드를 계속 살펴보며 오버라이드 해줘야 할 것이다.

이렇게 행동이 바뀌는 요구 사항들이 많아지는 경우 상속으로만 해결하기에는 상당히 지저분해 보인다.

 

4. 사용 예제 - 문제 해결 고민(인터페이스)

좀 더 나은 방법으로 고민을 해보자.

HCar, TCar 자동차는 하늘을 날 수 없으므로 Fly() 함수는 필요 없는 함수이다. 

그럼 Fly()함수를 부모 클래스인 Car에서 삭제하고 IFlyable 인터페이스를 만든 후 GCar에서만 IFlyable 인터페이스를 상속받아 구현하면 어떨까?

구조도를 보면 아래와 같을 것이다.

인터페이스 사용
인터페이스 사용

상속을 사용했을 때보다 좀 더 깔끔해진 모습이다.

하지만 인터페이스는 상속받은 클래스에서 다 구현을 해줘야 하기에 코드 재사용성이 떨어진다.

코드 재사용성이 떨어진다는건 그만큼 사유 코드 유지보수에 문제가 있다는 의미이다.

한 가지 행동이 바뀔 때마다 그 행동이 정의된 다른 서브 클래스를 전부 찾아서 코드를 일일이 고쳐야 한다.

 

5. 전략 패턴(Strategy Pattern) 적용

디자인 원칙 중에 프로그램에서 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리해야 한다.

달라지는 부분을 찾아서 나머지 코드에 영향을 주지 않도록 '캡슐화' 해야한다.

전략 패턴의 핵심인 바뀌는 부분을 캡슐화하면, 나중에 바뀌지 않는 부분에는 영향을 미치치 않고 그 부분만 고치면서 확장해 나갈 수 있다.

 

위 예제에서 바뀌는 행동 Fly는 인터페이스를 활용하고 행동 클래스를 정의해서 해당 클래스를 구현해주면 된다.

IFlayBehaviour 인터페이스를 만들고, 해당 인터페이스를 상속받는 행동 클래스를 정의한다.

행동 인터페이스 정의
행동 인터페이스 정의

유니티 코드를 보면 다음과 같이 작성해 볼 수 있다.

부모 클래스인 Car 클래스는 다음과 같이 작성해볼 수 있다. 바뀌는 행동을 위해 IFlyBehaviour 인터페이스를 멤버로 가진다.

using UnityEngine;

public abstract class Car : MonoBehaviour
{
    IFlyBehaviour flyBehaviour;

    public abstract void Move();
    public abstract void Display();

    public void StartFly()
    {
        flyBehaviour.Fly();
    }

    public void SetFlyBehaviour(IFlyBehaviour newBehaviour)
    {
        flyBehaviour = newBehaviour;
    }
}

Car 클래스를 상속받는 TCar, GCar 자식 클래스는 다음과 같다.

public class GCar : Car
{
    public override void Display()
    {
        Debug.Log("GCar Display");
    }

    public override void Move()
    {
        Debug.Log("GCar Move");
    }
}

public class TCar : Car
{
    public override void Display()
    {
        Debug.Log("TCar Display");
    }

    public override void Move()
    {
        Debug.Log("TCar Move");
    }
}

 

행동 인터페이스 구성은 다음과 같이 작성해볼 수 있다.

public interface IFlyBehaviour
{
    void Fly();
}

행동 인터페이스를 상속받은 행동 클래스는 다음과 같이 작성해 볼 수 있다.

public class Flyable : MonoBehaviour, IFlyBehaviour
{
    public void Fly()
    {
        //Fly 행동 구현.
        Debug.Log("날아올라~");
    }
}
using UnityEngine;

public class FlyDisable : MonoBehaviour, IFlyBehaviour
{
    public void Fly()
    {
        //행동 구현.
        Debug.Log("날지 못해~!~");
    }
}

 

호출 테스트를 위해 Simulator.cs C# 스크립트를 아래와 같이 만든 후 Scene의 GameObject에 Attatch 한 후 실행해본다.

Simulator.cs 

using UnityEngine;

public class Simulator : MonoBehaviour
{
    void Start()
    {
        Car gCar = new GCar();
        gCar.SetFlyBehaviour(new Flyable());
        gCar.StartFly();

        Car tCar = new TCar();
        gCar.SetFlyBehaviour(new FlyDisable());
        gCar.StartFly();
    }

}

 

StartFly() 함수의 결과 로그

Simulator 결과 화면
Simulator 결과 화면

 

6. 정리

위 예제에서 보는 것처럼 Fly 행동의 추가 요구사항이 들어오면, IFlayBehaviour를 상속받은 행동 클래스(Flyable, FlyDisable 등)를 계속 확장해 개발하면, Car 클래스의 몸체를 수정하지 않고도 계속 확장이 가능하다. 또한 행동에 대한 수정이 필요할 경우 해당 행동 클래스만 수정하면 되기 때문에 유지보수에 유용하다.

각 객체마다 행동이 바뀌는 경우, 전략 패턴을 잘 활용하면 재사용성이 뛰어난 코드를 만들 수 있을 것 같다.

공유하기

facebook twitter kakaoTalk kakaostory naver band