본문 바로가기
디자인 패턴

전략 패턴

by CodingMasterLSW 2025. 4. 11.

 

전략 패턴 : 알고리즘(행위)을 각각 독립된 클래스로 캡슐화하고, 이들을 동적으로 바꿔 끼울 수 있게 설계하는 패턴

 

단순히 글로만 보면 이해가 안 된다. 아래 귀여운 오리 사진과 예제를 보며, 전략 패턴을 어떤 상황에 쓰는지 이해해 보자!


꽥꽥

 

오리가 한 마리 있다. 이 오리는 

1) 소리를 낼 수 있다.

2) 수영을 할 수 있다.

 

위의 UML을 살펴보자.

 

여러 종류의 오리들이 존재하고, 공통되는 행위를 줄이기 위해 Duck이라는 상위 클래스에 공통 메서드를 정의했다. 그런데 문득 특별한 오리를 만들어주고 싶다는 생각이 들었다. 내가 추가하고 싶은 기능은 fly()다.

 

fly() 기능을 오리에 추가했다! 모든 오리들이 날 수 있지만, 한 가지 문제점이 발생했다.

 

ModelDuck은 단순한 모형 오리다. 모형이기에 ModelDuck은 날면 안 된다. 음... 이런 문제를 어떻게 해결할 수 있을까?

 

방법 1) 추상메서드 사용

 

fly()를 추상 메서드로 만들고, 각각의 하위클래스들이 오버라이딩을 사용해서 기능을 구현해 주면 되지 않을까?

 

이 또한 좋은 방법이다. ModelDuck에서 UnsupportedOperationException을 발생시키거나, 오버라이딩해 아무 기능도 하지 않게 만들면 될 것 같다. 하지만 하위클래스가 점점 많아질수록, 중복코드를 제어하지는 못 할 것 같다. 또한 모든 오리들의 행동을 알기 힘들 것 같다. (하위 클래스를 확인해봐야 한다...!)

 

이보다 좋은 방법이 있을 것 같다.

 

 

방법 2) 날아야 하는 하위 클래스에 기능 추가하기

fly() 메서드를 사용하려면 상위 클래스에서 형변환(다운캐스팅)을 사용해야 할 것 같은데... 다형성의 장점이 사라져 좋은 방법은 아닌 것 같다. 또한, 위에서 맞닥뜨린 하위 클래스의 중복 코드에 대한 문제점은 여전히 남아있다.

 

 

기능을 분리해 볼까?

 

이 방법 또한 괜찮아 보이지만 마찬가지로 다형성을 활용할 수는 없어 보인다. 

 

 

행동을 따로 만들고 정의해 보자!

오리는 날 수 있거나, 날 수 없다. 두 가지 행동을 각각 정의를 해보자.

 

public class FlyWithWings implements FlyBehavior {

    @Override
    public void fly() {
        System.out.println("날아간다아아아아");
    }
}

 

public class FlyNoWay implements FlyBehavior{

    @Override
    public void fly() {
        System.out.println("나는 날 수 없어요...꽥꽥");
    }
}

 

FlyBehavior를 Duck에서 가지고 있다면, 어떤 일이 일어날까?

 

public class Duck{

    private final FlyBehavior flyBehavior;

    public Duck(FlyBehavior flyBehavior) {
        this.flyBehavior = flyBehavior;
    }

    public void fly() {
        flyBehavior.fly();
    }
}

 

Duck은 직접적인 구체클래스가 아닌, 인터페이스에 의존하고 있다.

인터페이스에 의존해 fly() 메서드를 호출하니, 다형성 또한 잘 지켜지고 있는 것 같다.

 

public class PrettyDuck extends Duck {

    public PrettyDuck(FlyBehavior flyBehavior) {
        super(flyBehavior);
    }
}

 

public class ModelDuck extends Duck{

    public ModelDuck(FlyBehavior flyBehavior) {
        super(flyBehavior);
    }
}

 

(보너스) 생성자에서 FlyBehavior를 주입받을 수도 있지만, 내부에서 바로 만드는 방법 또한 존재한다.

public class ModelDuck extends Duck{

    public ModelDuck() {
        super(new FlyNoWay());
    }
}

 

(아래 코드는 생성자 주입 방식을 사용했다)

public class MainDuck {

    public static void main(String[] args) {
        Duck canFlyDuck = new PrettyDuck(new FlyWithWings());
        Duck cannotFlyDuck = new ModelDuck(new FlyNoWay());
        canFlyDuck.fly();
        cannotFlyDuck.fly();
    }
}

 

 

다형성을 잘 활용하고 있는 것 같다! 또한, Duck에 대한 하위 클래스가 늘어남으로써 발생하는 중복코드제거에 대한 문제점 또한 해결됐다.

추후에 fly에 대한 요구사항이 바뀌어도 쉽게 관리할 수 있을 것 같다!

 

만약 ModelDuck에 대한 요구사항이 바뀌어 ModelDuck 또한 하늘을 날아야 한다면 어느 부분의 코드를 바꿔야 할까?

Duck modelDuck = new ModelDuck(new FlyWithWings());

 

해당 코드만 바꿔주면 ModelDuck을 날 수 있게 변경할 수 있다. 이는 OCP(개방 폐쇄 원칙)를 잘 지키며, 코드가 매우 유연해지는 것을 볼 수 있다...!

 

만약 동적으로 ModelDuck의 전략을 변경하고 싶으면 어떻게 해야 할까?

public class Duck{

    private FlyBehavior flyBehavior;

    public Duck(FlyBehavior flyBehavior) {
        this.flyBehavior = flyBehavior;
    }

    public void setFlyBehavior(FlyBehavior changeBehavior) {
        this.flyBehavior = changeBehavior; // 전략 교체
    }

    public void fly() {
        flyBehavior.fly();
    }
}
public class MainDuck {

    public static void main(String[] args) {
        Duck modelDuck = new ModelDuck(new FlyNoWay());
        modelDuck.fly();
        modelDuck.setFlyBehavior(new FlyWithWings());
        modelDuck.fly();
    }
}

// 나는 날 수 없어요...꽥꽥
// 날아간다아아아아

 

굉장히 유연하게 알고리즘을 바꿀 수 있다.

 

전략 패턴은 알고리즘을 정의하고 캡슐화해 각각의 알고리즘들을 수정해서 쓸 수 있게 해 준다. 전략 패턴을 사용하면 클라이언트로부터 알고리즘을 분리해 독립적으로 변경할 수 있다.

 

다만, 클래스가 많아져 코드가 다소 복잡해질 수 있다는 단점 또한 존재한다. 반드시 전략패턴을 고집하지는 말고, 본인 상황에 맞게 잘 사용해 보자.

 

 

참고 서적 : 헤드퍼스트 디자인패턴(한빛미디어)