실행(런타임) 중에 알고리즘 전략을 선택해 객체 동작을 실시간으로 바뀌도록 할 수 있게 하는 행위 디자인 패턴
여기에서 '전략'이란 일종의 알고리즘이 될 수 도 있고, 기능이나 동작이 될 수도 있는 특정한 목표를 수행하기 위한 행동 계획을 말한다.
즉, 어떤 일을 수행하는 알고리즘이 여러가지 일 때, 동작들을 미리 전략으로 정의해 손쉽게 전략을 교체할 수 있는, 알고리즘 변형이 빈번하게 필요한 경우에 적합한 패턴이다.
💠 전략 알고리즘 객체들 : 알고리즘, 행위, 동작을 객체로 정의한 구현체
💠 전략 인터페이스 : 모든 전략 구현체에 대한 공용 인터페이스
💠 컨텍스트 : 알고리즘을 실행해야 할 때마다 해당 알고리즘
💠 클라이언트 : 특정 전략 객체를 컨텍스트에 전달 함으로써 전략을 등록하거나 변경하여 전략 알고리즘을 실행한 결과를 누린다.
💡 프로그래밍에서의 컨텍스트란 콘텐츠를 담는 무언가를 뜻하며, 어떤 객체를 핸들링 하기 위한 접근 수단이다.
즉, 물컴에 물이 담겨있으면 물은 콘텐츠가 되고, 물컵은 컨텍스트가 되며, 물을 핸들링하기 위한 접근 수단이 된다.
// 전략(추상화된 알고리즘)
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();
}
}
💠 전략 알고리즘의 여러 버전 또는 변형이 필요할 때 클래스화를 통해 관리
💠 알고리즘 코드가 노출되어서는 안되는 데이터에 액세스 하거나 데이터를 활용할 때 (캡슐화)
💠 알고리즘의 동작이 런타임에 실시간으로 교체 되어야 할 때
💠 알고리즘이 많아질수록 관리해야 할 객체의 수가 늘어난다는 단점이 있다.
💠 만일 어플리케이션 특성이 알고리즘이 많지 않고 자주 변경되지 않는다면, 새로운 클래스와 인터페이스를 만들어 프로그램을 복잡하게 만들 이유가 없다.
💠 개발자는 적절한 전략을 선택하기 위해 전략 간의 차이점을 파악하고 있어야 한다. (복잡도 증가)
class TakeWeapon {
public static final int SWORD = 0;
public static final int SHIELD = 1;
public static final int CROSSBOW = 2;
private int state;
void setWeapon(int state) {
this.state = state;
}
void attack() {
if (state == SWORD) {
System.out.println("칼을 휘두르다");
} else if (state == SHIELD) {
System.out.println("방패로 밀친다");
} else if (state == CROSSBOW) {
System.out.println("석궁을 발사하다");
}
}
}
class User {
public static void main(String[] args) {
// 플레이어 손에 무기 착용 전략을 설정
TakeWeapon hand = new TakeWeapon();
// 플레이어가 검을 들도록 전략 설정
hand.setWeapon(TakeWeapon.SWORD);
hand.attack(); // "칼을 휘두르다"
// 플레이어가 방패를 들도록 전략 설정
hand.setWeapon(TakeWeapon.SHIELD);
hand.attack(); // "방패로 밀친다"
}
}
위 코드를 살펴보면 state
매개변수의 값에 따라 간접적으로 attack()
함수의 동작을 제어하도록 되어 있다.
적이 오면 상수를 메서드에 넘겨 조건문으로 일일히 필터링해 적절한 전략을 실행하는 방식이다.
하지만 이 코드처럼 상태 변수를 통해 행위를 분기문으로 나누는 행위는 좋지 않은 코드이다.
잘못하면 if else 지옥에 빠질 수도 있다.
// 전략 - 추상화된 알고리즘
interface Weapon {
void offensive();
}
class Sword implements Weapon {
@Override
public void offensive() {
System.out.println("칼을 휘두르다");
}
}
class Shield implements Weapon {
@Override
public void offensive() {
System.out.println("방패로 밀친다");
}
}
class CrossBow implements Weapon {
@Override
public void offensive() {
System.out.println("석궁을 발사하다");
}
}
// 컨텍스트 - 전략을 등록하고 실행
class TakeWeaponStrategy {
Weapon wp;
void setWeapon(Weapon wp) {
this.wp = wp;
}
void attack() {
wp.offensive();
}
}
// 클라이언트 - 전략 제공/설정
class User {
public static void main(String[] args) {
// 플레이어 손에 무기 착용 전략을 설정
TakeWeaponStrategy hand = new TakeWeaponStrategy();
// 플레이어가 검을 들도록 전략 설정
hand.setWeapon(new Sword());
hand.attack(); // "칼을 휘두르다"
// 플레이어가 방패를 들도록 전략 변경
hand.setWeapon(new Shield());
hand.attack(); // "방패로 밀친다"
// 플레이어가 석궁을 들도록 전략 변경
hand.setWeapon(new Crossbow());
hand.attack(); // "석궁을 발사하다"
}
}
위의 전략패턴에서는 상수 값을 넘겨주는 대신, 인스턴스를 넣어 알고리즘을 수행하도록 했다.
이런식으로 구성하면 나중에 칼이나 방패 외에 도끼나 창과 같은 전략 무기들을 추가로 등록할 때, 코드의 수정없이 빠르게 기능을 확장할 수 있다는 장점이 있다. (class를 추가하고 implements를 해주면 끝)