유니티 스트래티지 패턴 - yuniti seuteulaetiji paeteon

3. 유니티 디자인 패턴 - 스트래티지 패턴(전략 패턴) 이해하기

  • 2020. 5. 10. 15:52
  • 공부기록

스트래티지 패턴이란 다양한 객체들을 하나의 개념으로 추상화하여 하나의 인터페이스를 만들고 이를 접근점으로 활용하는 패턴입니다.

구체적으로 어떻게 사용될 수 있는지 현실에서의 예를 들어 살펴보겠습니다. "어느 날 한 어린이는 사자와 호랑이를 직접 보러가고 싶었습니다. 그래서 가족에게 동물원에 가자고 말했습니다" 여기서 어린이는 사자와 호랑이를 동물이라는 개념으로 추상화했고, 사자와 호랑이라는 객체를 동물이라는 개념으로 접근한 것입니다.  어린이는 사자와 호랑이를 직접적으로 말할 필요도 없이 동물이라는 것만 알고 있어도 되는 것이죠. 이것이 인터페이스가 됩니다. 

그럼 유니티 내에서 어떻게 사용할수 있을지도 살펴보겠습니다.

유니티 스트래티지 패턴 - yuniti seuteulaetiji paeteon

위와같이 발사기를 바꾸는데 스트래티지 패턴을 활용할 수 있습니다. 이 예제에선 슈터 3개가 종류별로 있습니다. 이 3개의 슈터는 총알을 발사한다는 기본적인 유사점이 있습니다. 이를 추상화하여 IShooter 라는 인터페이스를 만들어 접근점으로 활용할 수 있습니다.

유니티 스트래티지 패턴 - yuniti seuteulaetiji paeteon

위와 같이 접근점을 두어 발사기에 접근할 수 있는 것의 장점은 클래스의 작동방식, 알고리즘, 즉 어떻게에 해당하는 부분을 모두 다르게 코딩할 수 있다는 점입니다.

어떤 발사기는 샷건처럼 발사시킬수도 있고, 어떤 건 레이저로 나가게 할수도 있고, 어떤 건 미사일이 나갈수도 있습니다. 이렇게 발사한다는 기본적인 개념은 같으나 발사한다는 것에 대한 방법은 모두 다를 수 있습니다.

코드까지 소개해드리면 조금 복잡해질듯하여 간단한 예를 들어 보겠습니다.

만약 세 개의 슈터를 번갈아가면서 쓰고 싶은 경우에서, 스트래티지 패턴을 사용하지 않으면 제각각 세 가지 슈터를 인자로 받아서 활용해야하는 반면, 스트래티지 패턴을 활용하면 IShooter shooter 라는 인자로 모든 슈터의 정보를 쉽게 받아올수 있게됩니다.

이상입니다.

한 줄 요약 : 다양한 객체에 접근이 필요할 때 다양한 객체의 공통분모를 추상화하여 인터페이스 클래스를 만들어 이를 접근점으로 활용하는 패턴

1. 전략 패턴이란?

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

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

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

2. 사용 예제 - 문제 상황

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

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

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

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

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

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

유니티 스트래티지 패턴 - yuniti seuteulaetiji paeteon
자동차 클래스 구조도

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

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

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

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

유니티 스트래티지 패턴 - yuniti seuteulaetiji paeteon
상속으로 구현

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

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

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

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

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

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

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

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

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

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

유니티 스트래티지 패턴 - yuniti seuteulaetiji paeteon
인터페이스 사용

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

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

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

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

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

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

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

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

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

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

유니티 스트래티지 패턴 - yuniti seuteulaetiji paeteon
행동 인터페이스 정의

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

부모 클래스인 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() 함수의 결과 로그

유니티 스트래티지 패턴 - yuniti seuteulaetiji paeteon
Simulator 결과 화면

6. 정리

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

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