Computer Science/디자인 패턴

전략 패턴(Strategy Pattern)

TwinParadox 2021. 12. 26. 23:51
728x90

Strategy Pattern이란?

교환이 가능한 객체를 정의하고, 런타임에 설정하고 변환하는 패턴

같은 문제를 해결하는 여러 알고리즘을 클래스별로 캡슐화하고 필요에 따라 교체할 수 있는 설계로, 이를 런타임에 수행할 수 있게 하려는 패턴이다. 예시 그림과 예시 그림에 있는 용어들에 대해서 정리하고 가자.

 

 

Strategy Pattern UML

 

Strategy

인터페이스나 추상 클래스로 외부에서 동일한 방법으로 알고리즘을 호출하는 방법을 명시

 

ConcreteStrategyA, B, C

앞서 설계한 추상 클래스 혹은 인터페이스를 필요 기능에 따라 실제 구현한 클래스

 

Context

전략 패턴을 이용하는 역할을 수행한다.

필요에 따라서, 동적으로 구체적인 전략을 바꿀 수 있는 setter를 제공한다.

사용하는 쪽에서는 Strategy만 바꿔주면 코드의 변경 없이 세부 구현 로직이 바뀐다.

 

결국 전략 패턴은 Contextsetter를 통해서 변경 대상인 Strategy를 동적으로 변경이 가능한 구조를 갖는다.

 

 

 

구현 예시

어떤 온라인 게임에 두 개의 직업이 존재한다고 가정하자.

초기 설계

Infighter 클래스와 Berserker 클래스는 딜러(Dealer) 인터페이스를 구현한 클래스다.

Dealer 인터페이스에는 attack(), skill()이라는 메서드가 존재하며 다음과 같이 구현되어 있다.

 

public interface Dealer {
	void attack();
	void skill();
}
public class Berserker implements Dealer {
	@Override
	public void attack() {
		System.out.println("버서커는 베기가 기본 공격입니다.");
	}

	@Override
	public void skill() {
		System.out.println("공격 타입 : 백어택");
	}
}
public class Infighter implements Dealer {
	@Override
	public void attack() {
		System.out.println("인파이터는 주먹이 기본 공격입니다.");
	}

	@Override
	public void skill() {
		System.out.println("공격 타입 : 백어택");
	}
}

 

만약 여기서, 개발자가 리메이크 작업에 들어가면서 Infighter 클래스의 skill()에 변경을 준다고 생각해보자.

가장 쉬운 건 Infighter 클래스에 대해서 skill() 메서드만 변경해주면 된다.

public void skill() {
	// AS-IS
	// System.out.println("공격 타입 : 백어택");

	// TO-BE
	System.out.println("공격 타입 : 헤드 어택");
}

 

이렇게 하면 문제가 있다.

Infighter 클래스의 skill() 메서드의 구현 내용이 변경이 발생하게 됐다. 기획 상으로 보면, 해당 메서드를 변경해주면 끝나는 부분이지만, SOLID의 OCP(Open-Closed Princinple)을 위배한다. OCP에 근거한 수정 방식은 기존의 메서드는 수정하지 않으면서, 행위를 수정해야 하는데 기존 메서드를 변경하게 된 셈이다.

 

OCP 위반뿐 아니라, 이러한 유지보수 방식은 그 대상 메서드가 늘어날수록 규모가 커진다.

지금은 Infighter, Berserker 두 개의 클래스만 존재했지만 콘텐츠 추가로 게임에 여러 직업이 추가되면 클래스가 그만큼 늘어나게 된다. 변경 사항이 발생하면 그때마다 해당 클래스를 변경을 해줘야 한다. 커지는 규모만큼 메서드 중복 문제까지 발생해서 유지보수 문제가 더 크게 느껴진다.

 

 

Strategy Pattern을 적용하자.

현재 skill() 메서드는 공격 타입에 따라 헤드 어택과 백어택 두 가지 방식으로 구성되어 있다.

skill의 공격 타입에 따라서 전략이 생성되도록(Strategy)하고, 이를 캡슐화하기 위한 SkillStrategy() 인터페이스를 생성한다. 캡슐화를 통해서 추후 skill에 공격 타입 전략 외에 자원 소모 전략(마나 또는 기력) 등의 확장에 대해 고려한 설계를 가져갈 수 있다.

SkillStrategy

public interface SkillStrategy {
	void skill();
}
public class HeadAttackSkillStrategy implements SkillStrategy {
	@Override
	public void skill() {
		System.out.println("공격 타입 : 헤드 어택");
	}
}


public class BackAttackSkillStrategy implements SkillStrategy {
	@Override
	public void skill() {
		System.out.println("공격 타입 : 백어택");
	}
}

 

두 클래스는 이제 skill() 메서드를 통해 스킬 사용이 가능한데, 공격 타입에 대한 부분을 직접 구현하는 것이 아니라 전략을 설정하여 그 전략에 맞게끔 공격 타입이 설정되도록 한다. 이를 위한 setter인 setSkillStrategy()를 만든다. 이제 전략의 교체는 skill() 메서드를 직접 변경하지 않고 setter를 통해서 진행할 수 있다.

Dealer 클래스

public class Dealer {
   private SkillStrategy skillStrategy;

   public void attack() {
      System.out.println("기본 공격");
   }

   public void skill() {
      skillStrategy.skill();
   }

   public void setSkillStrategy(SkillStrategy skillStrategy) {
      this.skillStrategy = skillStrategy;
   }
}
public class Berserker extends Dealer {
	@Override
	public void attack() {
		System.out.println("버서커는 베기가 기본 공격입니다.");
	}
}

public class Infighter extends Dealer {
   @Override
   public void attack() {
      System.out.println("인파이터는 주먹이 기본 공격입니다.");
   }
}

 

어떻게 사용할까?

이 설계를 사용하는 쪽에서는 어떻게 사용할까?

Infighter, Berserker 클래스의 skill()에 대한 구현은 앞서 선언한 setSkillStrategy() 호출을 통해 설정한다.

public static void strategy() {
   Dealer infighter = new Infighter();
   Dealer berserker = new Berserker();

   // AS-IS
   // Infighter의 스킬 타입 : 백어택
   // Berserker의 스킬 타입 : 백어택
   infighter.setSkillStrategy(new BackAttackSkillStrategy());
   berserker.setSkillStrategy(new BackAttackSkillStrategy());

   infighter.skill();
   berserker.skill();

   // TO-BE
   // Infighter : 헤드어택
   // Berserker : 백어택
   infighter.setSkillStrategy(new HeadAttackSkillStrategy());

   infighter.skill();
   berserker.skill();
}

지금은 skill만을 예시로 들었지만, attack에 대해서도 Strategy Pattern을 적용해볼 수 있을 것이다.

 

 

장점

코드 중복을 제거한다.

런타임에 타겟 메서드 변경한다.

확장성 및 알고리즘 변경이 용이하다.

 

단점

구조가 조금 더 복잡해질 수 있다.

setter를 사용하려면 결국 구현된 Strategy를 모두 파악해야 한다.

 

 

언제 사용할 것인가?

구현된 알고리즘은 달라도, 목적은 동일한 경우에 적용을 생각해볼 수 있다.

참조하는 클래스의 변경 및 제거 등 다양한 상황이 발생한 경우, 사용 시점에 따라 알고리즘 변경이 필요할 때 Strategy Pattern을 사용하면 setter로 새로운 알고리즘으로 교체하기 용이할 것이다.

 

 

 

Reference

GoF의 디자인 패턴

https://www.geeksforgeeks.org/strategy-pattern-set-1/

https://www.baeldung.com/java-strategy-pattern

https://www.researchgate.net/publication/249885094_A_Quality_Model_for_Design_Patterns

 

728x90