전략 패턴
전략 패턴 (Strategy Pattern)
전략 패턴은 행동(Behavioral) 패턴 중 하나임.
알고리즘 군을 정의하고, 각각을 캡슐화하여 상호 교체 가능하게 만드는 패턴임.
즉, 특정한 동작 방식을 코드 내부에 고정하지 않고 외부에서 주입함으로써 유연하게 변경할 수 있게 함.
목적
알고리즘을 직접 코드에 하드코딩하지 않고 전략 객체(Strategy) 로 분리함.
런타임에 필요한 전략을 선택하여 동적으로 행위를 변경할 수 있게 함.
조건문(if/switch)으로 분기하던 코드를 객체 지향적으로 대체
구조

위의 클래스 다이어그램 구조를 살펴보자.
Context - 전략을 사용하는 객체. 실행 시점에 전략을 교체한다. 추상클래스인 Character와 이 것의 서브클래스들(King, Queen, Knight)이 이에 속함.
Strategy (Interface) - 알고리즘의 공통 인터페이스를 정의. WeaponBehavior 인터페이스가 이에 속함.
Concrete Strategy - Strategy 인터페이스를 구현한 실제 알고리즘 클래스. WeaponBehavior 를 구현하는 클래스들이 이에 속함(SwordBehavior, AxeBehavior, BowBehavior).
Client - 어떤 전략을 사용할지 결정하고 Context에 주입 (Service 레이어 등 사용처)
먼저 전략 패턴을 사용하지 않고 상속으로만 디자인 해본다고 생각해보자.
행동 인터페이스인 WeaponBehavior를 구성 하지않고 캐릭터 클래스에서 attack() 메서드 대신useWeapon() 메서드를 캐릭터 클래스 내에서 사용할 수 있다고 생각할 수 있다.
그렇게 하게되면 상속받은 킹, 퀸, 나이트 클래스에서 useWeapon() 메서드를 오버라이딩하여 알고리즘을 재정의 할 것이다.
이 때, 특정 캐릭터의 공격 방식을 변경해야 한다면 그 해당 클래스에 들어가 오버라이딩 한 useWeapon() 메서드의 코드를 일일이 수정해야하는 번거로움이 생긴다.
반면에, useWeapon()메서드를 행동으로 분리하고 인터페이스로 캡슐화하여 캐릭터 클래스에서 attack 메서드 내부에서 weapon.useWeapon() 으로 위임을 한다면,
해당 객체들을 사용하는 클라이언트 쪽에서 Context의 내부 코드를 수정하지 않고, setWeapon()을 통해 런타임에 행동을 교체할 수 있게 된다.
이와 동시에 SOLID원칙인 OCP와 DIP를 준수하게 되는 것이다.
핵심은 상속보다는 구성을 사용하는 것이다.
동작 흐름
Context는Strategy인터페이스를 참조- 클라이언트는 실행할 전략(Concrete Strategy)을
Context에 주입함.Context는 자신의 로직 중 일부를Strategy에 위임- 새로운 전략이 필요하면 새로운 클래스만 추가하면 됨. 기존 코드 수정은 불필요
전략패턴을 적용하지 않고 상속을 적용했을 때와 전략패턴을 적용했을 때의 예제코드를 살펴보자.
예제 코드
상속만 적용했을 때
public abstract class Character {
abstract void attack();
}
public class King extends Character {
@Override
public void attack() {
System.out.println("검으로 공격");
}
// Client
public class Run {
public static void main(String[] args) {
Character king = new King();
king.attack(); // "검으로 공격"
}
}
편의상 필드는 정의하지 않겠다.
Character 추상 클래스를 정의하고 Character를 확장하는 King 클래스에서 attack 메서드를 오버라이딩하여 재정의 했다.
클라이언트부에서 사용 시 왕의 행동(알고리즘)을 어떻게 변경할 것인가?
King 클래스로 접근하여 오버라이딩된 메서드 내용을 직접 수정하는 수 밖에 없다.
전략패턴을 적용했을 때
// Context
public abstract class Character {
WeaponBehavior weaponBehavior;
public void attack() {
weaponBehavior.useWeapon();
}
public void setWeaponBehavior(WeaponBehavior weaponBehavior) {
this.weaponBehavior = weaponBehavior;
}
}
// Context
public class King extends Character {
public King() {
weaponBehavior = new SwordBehavior();
}
}
// Context
public class Knight extends Character {
public Knight() {
weaponBehavior = new AxeBehavior();
}
}
// Context
public class Queen extends Character {
public Queen() {
weaponBehavior = new BowBehavior();
}
}
// Strategy Interface
public interface WeaponBehavior {
void useWeapon();
}
// Concrete Strategy
public class SwordBehavior implements WeaponBehavior {
@Override
public void useWeapon() {
System.out.println("검으로 공격");
}
}
// Concrete Strategy
public class BowBehavior implements WeaponBehavior {
@Override
public void useWeapon() {
System.out.println("활로 공격");
}
}
// Concrete Strategy
public class AxeBehavior implements WeaponBehavior {
@Override
public void useWeapon() {
System.out.println("도끼로 공격");
}
}
// Client
public class Run {
public static void main(String[] args) {
Character king = new King();
Character queen = new Queen();
Character knight = new Knight();
king.attack();
queen.attack();
knight.attack();
king.setWeaponBehavior(new BowBehavior());
king.attack();
}
}
캐릭터의 attack() 메서드에서 weaponBehavior.useWeapon() 이라는 행동을 전략으로 분리하고 attack() 은 Context가 그대로 보유한 채 전략에 위임한다.
그리고 각 인터페이스를 구현하는 클래스를 작성하여 특정 역할에 맞게 알고리즘을 구현한다.
또한, 런타임에 알고리즘을 교체할 수 있도록 setWeaponBehavior() 메서드를 작성해주었다.
각 컨텍스트 객체들이 생성자에서 특정 행동을 주입받도록 한다.
클라이언트부에서는 인스턴스를 생성하여 attack() 메서드를 실행해보면 처음 주입된 행동이 작동할 것이다.
setWeaponBehavior() 메서드를 통해 런타임에 알고리즘을 변경하여 “활로 공격"이 출력되는 것을 볼 수 있다.
Spring 통합
@Service
public class WeaponService {
private final Map<String, WeaponBehavior> strategies;
public WeaponService(List<WeaponBehavior> behaviors) {
strategies = behaviors.stream()
.collect(Collectors.toMap(WeaponBehavior::getType, Function.identity()));
}
public void attack(String type) {
WeaponBehavior behavior = strategies.get(type);
if (behavior != null) behavior.useWeapon();
}
}
여러 무기 전략 (WeaponBehavior)을 스프링 컨테이너에서 자동으로 주입받아, 인터페이스에 String getType() 같이 구현체의 type을 반환하는 특정 메서드가 있다는 가정하에 type을 key로 지정하고 문자열 key로 런타임에 적절한 전략을 찾아서 실행시키는 코드이다.
애플리케이션 구동 시 빈으로 등록된 모든 WeaponBehavior 인터페이스의 구현체를 자동으로 찾아 생성자의 파라미터 List<WeaponBehavior>에 주입해준다.
요약
전략 패턴은 “변하지 않는 것과 변하는 것을 분리한다”는 객체지향의 본질을 가장 잘 보여주는 패턴이다.
상속 기반 설계보다 더 유연하고, 런타임 교체가 필요한 상황에서도 안정적으로 대응할 수 있다.
단, 무조건 적용하기보다는 로직 변경이 잦거나 알고리즘 교체 가능성이 높은 부분에 한정하여 사용하는 것이 좋다.