[DesignPattern] Strategy Pattern

suhan0304·2024년 9월 5일

Design Pattern

목록 보기
3/16
post-thumbnail

Strategy

전략 패턴은 실행(런타임) 중에 알고리즘 전략을 선택하여 객체 동작을 실시간으로 바꿔 사용하도록 할 수 있게 하는 행위(Behaviour) 패턴이다.

Strategy(전략)이란 일종의 알고리즘일 수도 있고, 기능이나 동작이 될 수도 있는 특정한 목표를 수행하기 위한 행동 계획을 전략이라고 칭한다.


Why

어떤 일을 수행하는 알고리즘이 여러 가지 일 때, 동작들을 미리 전략으로 정의함으로써 손쉽게 전략을 교체할 수 있는, 알고리즘 변형이 빈번하게 필요한 경우에 적합한 패턴이다.


Structure

  • 전략 알고리즘 객체들 : 알고리즘, 행위, 동작을 객체로 정의한 구현체
  • 전략 인터페이스 : 모든 전략 구현제에 대한 공용 인터페이스 
  • 컨텍스트(Context) : 알고리즘을 실행해야 할 때마다 해당 알고리즘과 연결된 전략 객체의 메소드를 호출.
  • 클라이언트 : 특정 전략 객체를 컨텍스트에 전달 함으로써 전략을 등록하거나 변경하여 전략 알고리즘을 실행한 결과를 누린다.

전략 패턴은 OOP의 집합체이기도 하다.

  1. 동일 계열의 알고리즘군을 정의하고 
  2. 각각의 알고리즘을 캡슐화하여 
  3. 이들을 상호 교환이 가능하도록 만든다.
  4. 알고리즘을 사용하는 클라이언트와 상관없이 독립적으로
  5. 알고리즘을 다양하게 변경할 수 있게 한다. 


When

전략 패턴의 사용 시기와 주의점을 정확히 알고 나서 사용하는 것이 좋다. 저번 싱글톤에서도 다뤘지만 모든 디자인 패턴은 각각의 장단점이 있기 때문에 장점을 극대화 할 수 있는 사용 시기에만 사용하는게 좋다.

  • 전략 알고리즘의 여러 버전 또는 변형이 필요할 때 클래스화를 통해 관리
  • 알고리즘 코드가 노출되어서는 안 되는 데이터에 엑세스 하거나 데이터를 활용할 때 (캡슐화)
  • 알고리즘의 동작이 런타임에 실시간으로 교체 되어야 할 때

But

  1. 알고리즘이 많아질수록 관리해야할 객체의 수가 늘어난다.
  2. 알고리즘의 변경이 자주 일어나지 않는다면, 새로운 클래스와 인터페이스를 굳이 만들어서 프로그램을 복잡하게 만들 이유가 없다.

How

// 전략(추상화된 알고리즘)
interface IStrategy {
    void doSomething();
}

// 전략 알고리즘 A
class ConcreteStrateyA implements IStrategy {
    public void doSomething() {}
}

// 전략 알고리즘 B
class ConcreteStrateyB implements IStrategy {
    public void doSomething() {}
}
// 컨텍스트(전략 등록/실행)
class Context {
    IStrategy Strategy; // 전략 인터페이스를 합성(composition)
	
    // 전략 교체 메소드
    void setStrategy(IStrategy Strategy) {
        this.Strategy = Strategy;
    }
	
    // 전략 실행 메소드
    void doSomething() {
        this.Strategy.doSomething();
    }
}

// 클라이언트(전략 교체/전략 실행한 결과를 얻음)
class Client {
    public static void main(String[] args) {
        // 1. 컨텍스트 생성
        Context c = new Context();

        // 2. 전략 설정
        c.setStrategy(new ConcreteStrateyA());

        // 3. 전략 실행
        c.doSomething();

        // 4. 다른 전략 설정
        c.setStrategy(new ConcreteStrateyB());

        // 5. 다른 전략 시행
        c.doSomething();
    }
}

Example

이렇게만 설명하면 구현하는데 이해가 어려울 수도 있어 예시를 들어보자.

Moving Sample

Move 메서드를 가진 탈 것이라는 인터페이스가 있다. 이 탈 것을 implements해서 TrainBus 클래스를 생성했다고 해보자. 각 탈 것 클래스에는 Move 메서드가 Override 되어있다.

public interface Movable {
    public void move();
}
public class Train implements Movable{
    public void move(){
        System.out.println("선로를 통해 이동");
    }
}
public class Bus implements Movable{
    public void move(){
        System.out.println("도로를 통해 이동");
    }
}

만약에 선로를 따라 움직이는 버스가 생기면 Busmove 메서드를 수정하면 된다.

public void move(){
    System.out.println("선로를 따라 이동");
}

하지만 이렇게 되면 SOLID의 원칙 중 개방 폐쇄의 원칙(OCP)에 위배되게 된다. OCP에 의하면 기존 move()를 수정하지 않으면서 행위가 수정되어야 하지만, 지금 Bus의 move() 메서드를 직접 수정했다. 또한 지금과 같은 방식의 변경은 시스템이 확장됐을때 유지보수가 어렵다. 도로를 따라 움직이는 택시, 자가용, 오토바이 등이 추가된다고 했을 때, 버스처럼 선로를 따라 움직일 수 있게 된다면 클래스의 move() 메서드를 일일이 수정해야 될 뿐더러 같은 메서드를 여러 클래스에서 똑같이 정의하고 있으므로 메서드의 중복이 발생해버린다.

이를 해결하기 위해 전략을 생성해보자. 현재 운송 수단은 두 가지 방식으로 움직인다. 움직이는 두 방식(Rail, Load)에 대해 Strategy 클래스를 생성한다. 그리고 두 클래스에 move 메서드를 구현하여, 어떤 경로로 움직이는지에 대해 구현한다. 또한 두 전략 클래스를 캡슐화 하기 위해 MoveableStrategy 인터페이스를 생성해서 운송 수단에 대한 전략 뿐만 아니라, 다른 전략들이 추가적으로 확장되는 경우를 고려하여 설계한다.

public interface MovableStrategy {
    public void move();
}
public class RailLoadStrategy implements MovableStrategy{
    public void move(){
        System.out.println("선로를 통해 이동");
    }
}
public class LoadStrategy implements MovableStrategy{
    public void move() {
        System.out.println("도로를 통해 이동");
    }
}

운송 수단 클래스를 정의해보자. 기차와 버스 같은 운송 수단은 move 메서드를 통해 움직일 수 있다. 하지만 이 때 직접 이동 방식을 메서드로 구현하지 않고, 어떻게 움직일 것인지에 대한 전략을 설정, 그 전략의 움직임 방식을 사용하여 움직이도록 하는 것이다.

public class Moving {
    private MovableStrategy movableStrategy;

    public void move(){
        movableStrategy.move();
    }

    public void setMovableStrategy(MovableStrategy movableStrategy){
        this.movableStrategy = movableStrategy;
    }
}
public class Bus extends Moving{

}
public class Train extends Moving{

}

이제 이 운송 수단은 아래와 같이 사용할 수 있다. 이 때 각기 다른 운송 수단 객체를 생성한 후에 각 운송 수단이 어떻게 움직이는지 설정하기 위해 setMoveableStrategy 메서드를 호출한다.

public class Client {
    public static void main(String args[]){
        Moving train = new Train();
        Moving bus = new Bus();

        /*
            기존의 기차와 버스의 이동 방식
            1) 기차 - 선로
            2) 버스 - 도로
         */
        train.setMovableStrategy(new RailLoadStrategy());
        bus.setMovableStrategy(new LoadStrategy());

        train.move();
        bus.move();

        /*
            선로를 따라 움직이는 버스가 개발
         */
        bus.setMovableStrategy(new RailLoadStrategy());
        bus.move();
    }
}

위에서 보다시피 선로를 따라 움직이는 버스가 개발되었을 때 쉽게 이동 방식 전략을 수정할 수 있는 것은 확인할 수 있다.

Game Sample

만약 RPG 게임을 개발하고 있다고 생각해보자. state라는 매개변수로 무기의 종류를 구별해서 attack 함수의 동작을 제어하도록 아래와 같이 작성했다고 해보자. 적이 오면 상수를 메서드에 넘겨 조건문으로 일일이 필터링하여 적절한 전략을 실행하였다.

using UnityEngine;

class TakeWeapon {
    public static readonly int SWORD = 0;
    public static readonly int SHIELD = 1;
    public static readonly int CROSSBOW = 2;

    private int state;

    void setWeapon(int state) {
        this.state = state;
    }

    void attack() {
        if (state == SWORD) {
            Debug.Log("칼을 휘두르다");
        } else if (state == SHIELD) {
           Debug.Log("방패로 밀친다");
        } else if (state == CROSSBOW) {
            Debug.Log("석궁을 발사하다");
        }
    }
}

그 다음 유저의 무기를 설정하고 공격하는 코드가 아래와 같이 작성 되어있다고 해보자.


class User {
    void Process() {
        // 플레이어 손에 무기 착용 전략을 설정
        TakeWeapon hand = new TakeWeapon();

        // 플레이어가 검을 들도록 전략 설정
        hand.setWeapon(TakeWeapon.SWORD);
        hand.attack(); // "칼을 휘두르다"

        // 플레이어가 방패를 들도록 전략 설정
        hand.setWeapon(TakeWeapon.SHIELD);
        hand.attack(); // "방패로 밀친다"
    }
}

물론 정상적으로 작동은 한다. 하지만 위에서 언급했던 문제점들이 그대로 발생한다.

  1. 특정 무기의 동작이 바뀌면 attack 함수를 직접 수정해야 하는 문제
  2. 무기가 추가 될 때마다 유지보수가 어렵다는 문제
  3. if-else 지옥에 빠질 수 있는 문제

그래서 이를 전략 패턴을 적용시켜보면 여러 무기들을 객체 구현체로 정의하고 이들을 Weapon 인터페이스로 묶는다. 인터페이스를 컨텍스트 클래스에 합성시키고, setWeapon 메서드를 통해 전략 인터페이스 객체의 상태를 바로바로 변경할 수 있도록 구성해보자.

using UnityEngine;

public interface Weapon {
    void offensive();
}

public class Sword : Weapon {
    public void offensive() {
        Debug.Log("칼을 휘두르다.");
    }
}

public class Shield : Weapon {
    public void offensive() {
        Debug.Log("방패로 밀치다.");
    }
}

public class Crossbow : Weapon {
    public void offensive() {
        Debug.Log("석궁을 발사하다.");
    }
}
// 컨텍스트 - 전략을 등록하고 실행
public class TakeWeaponStrategy 
{
    public Weapon wp;

    public void setWeapon(Weapon wp) {
        this.wp = wp;
    }

    public void attack() {
        wp.offensive();
    }
}
// 클라이언트(유저) - 전략 제공/설정
public class Player : MonoBehaviour
{
    void Start() {
        // 플레이어 손에 무기 착용 전략을 설정
        TakeWeaponStrategy hand = new TakeWeaponStrategy();

        // 플레이어가 검을 들도록 전략 설정
        hand.setWeapon(new Sword());
        hand.attack(); // "칼을 휘두르다"

        // 플레이어가 방패를 들도록 전략 변경
        hand.setWeapon(new Shield());
        hand.attack(); // "방패로 밀친다"
        
        // 플레이어가 석궁을 들도록 전략 변경
        hand.setWeapon(new Crossbow());
        hand.attack(); // "석궁을 발사하다"
    }
}

클린하지 않는 기존의 코드에서는 메서드에 상수값을 넘겨주었지만, 전략 패턴에선 인스턴스를 넣어 알고리즘을 수행하도록 한 것이다. 이런식으로 구성하면 좋은 점은 나중에 칼이나 방패외에 도끼나 창과 같은 무기들을 추가로 등록할 때, 코드의 수정없이 빠르게 기능을 확장할 수 있다.

새로운 무기를 추가하고 싶으면 클래스를 추가하고 implements 해주면 끝난다.

결국 객체 지향 프로그래밍의 핵심인 유지보수를 용이하게 하기 위해, 약간 복잡하더라도 이러한 패턴을 적용하는 것이 장기적으로 봤을때 좋다는 것을 잊지 말자.

profile
Be Honest, Be Harder, Be Stronger

0개의 댓글