전략 패턴 Strategy Pattern

jaycee·2023년 1월 27일
0

목적

내가 생각하는 전략 패턴의 목적

동일 계열의 알고리즘(행위)들을 정의하고 캡슐화하여 유지보수를 용이하게 한다.
전략 패턴 구현 시
1. 클라이언트 클래스에서 의존성을 주입받고
2. 클라이언트 클래스에서는 인터페이스들을 이용해 알고리즘만 구현한다.
그래서 외부(스프링이라면 컨테이너)에서 입력받은 인스턴스에 따라 클라이언트 클래스에서 다른 알고리즘을 사용할 수 있다.
이런 방식은 새로운 동작이 추가되더라도 기존 구현체를 수정할 필요없이 구현체만 추가해주면 되기 때문에, 변경에는 닫혀있고 확장에는 열려있는 형태로 유지보수할 수 있다는 것을 의미한다(OCP).
또한 런타임 환경에서 실시간으로 동작을 변경할 수도 있다.

헤드퍼스트 디자인패턴에서 정의한 전략 패턴

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

중요한 이유

이 패턴을 활용하면 레고 블럭을 갈아 끼우듯 관심있는 부분에 대해서만 수정 및 테스트가 가능한 형태로 설계가 되기때문에 유지보수에 이점이 있다. 무엇보다도 전략 패턴은 많은 디자인 패턴의 원리이며, 자바 API를 비롯하여 스프링에서도 많이 사용된다.

어떤 경우에 사용할까?

  • 소리를 내는 기능과 나는 기능을 구현한 오리 객체 시뮬레이션에서 기존 천둥 오리에서 장난감 오리, 나무 오리를 추가하는 경우에 고려할 수 있다.
    • 일반오리는 소리를 내고 날 수 있고, 장난감 오리는 소리는 내지만 날 수 없고, 나무 오리는 소리를 못내고 날 수도 없다.

      💡 애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않은 부분과 분리하는 디자인 원리.

    • 난다, 소리를 낸다 추상화된 역할이 있는 인터페이스를 생성한다. 클라이언트 코드에서는 이 추상화된 역할만을 생각하여 개발한다.
    • 각 역할 인터페이스를 구현하고 각각의 오리 클래스에서 적합한 행위 구현 클래스를 작성한다. 클라이언트 클래스에서 역할의 실행 주체(인터페이스)가 실행되도록 위임한다. 클라이언트 클래스 코드 상에서 보면 어떤 구현체를 실행할지는 런타임 시에만 알 수 있고, 레고 블록을 조립하듯 다르게 정의할 수 있다.
    • 만약 위 기능들을 상속으로 구현한다면 어떨까. 제품이 늘어날 때마다 서브 클래스를 생성하고 이에 대해서 일일이 코드로 구현해야해서 코드의 중복이 발생한다. 변경에 따른 수정 범위가 커지므로 전략패턴을 사용하는 것이 훨씬 유리하다.

      💡 상속보다는 구성을 활용하는 디자인 원리.

  • RPG 게임 캐릭터의 직업에 따라 마우스 오른쪽 버튼의 동작을 다르게 해야한다면?
    • 직업에 따른 동작 배정
      • 전사는 칼을 휘두른다
      • 궁수는 활을 쏜다.
      • 도둑은 물건을 훔친다.
    • 직업이 변경되면 동작 배정도 변경해야한다.
  • 주문한 상품에 쿠폰을 적용할 때 쿠폰 종류에 따라
    • 정액 쿠폰을 사용할지, 정률 쿠폰을 사용할지, 무료 쿠폰을 사용할지
    • 한 상품에만 적용할지, 전체 상품에 적용을 할지
    • 금액 제한 적용을 둘지, 무제한 적용일지
  • 압축을 푸는 알고리즘을 구현할 때, 입력받은 파일의 확장자(zip, tar, rar 등)에 따라 다른 알고리즘을 적용한다.
  • 이하 자바 API에서 사용하는 전략 패턴이다.
    • java.util.Comparator#compare(), executed by among others Collections#sort().
    • javax.servlet.http.HttpServlet, the service() and all doXXX() methods take HttpServletRequest and HttpServletResponse and the implementor has to process them (and not to get hold of them as instance variables!).
    • javax.servlet.Filter#doFilter()

예시 구현

구현 시 고려사항

변하는 것과 변하지 않는 것을 분류한다.

  • 변하는 것: 작은 단위로 나누고 캡슐화한다. 오리 시뮬레이션의 경우 소리를 내는 행위나는 행위가 변화 대상이다.
  • 변하지 않는 것: 상속을 메소드를 구현하여 해결하거나 상속이 마음에 들지 않으면 구현체가 하나만 있는 인터페이스로 구성해도 된다. 오리의 경우 자기소개를 하는 것이 변하지 않는 대상이다.
    출처:헤드퍼스트 디자인패턴

추상 클래스를 만든다. 여기서 핵심은 각 행위(또는 알고리즘)을 위임하는 것이다.

public abstract class Duck {
    FlyBehavior flyBehavior;
    QuackBehavior quackBehavior;
    String name;

    public Duck(){
    }

   //오리가 난다.
   //나는 행위를 실행하도록 위임한다. 
   //그러므로 클라이언트에서 런타임에서 생성한 인스턴스 종류에 따라 다를 것이다. 
   public void performFly(){
        flyBehavior.fly();
    }

    //오리가 소리를 낸다.
    public void performQuack(){
    //소리내는 행위를 실행하도록 위임한다.
        quackBehavior.quack();
    }

    //오리가 자기소개를 한다.
    public void sayMyName(){
        System.out.println("============================");
        System.out.println("내 이름은 "+ this.name +"입니다.");
    };
}

천둥오리와 러버덕을 Duck객체 상속을 통해 만든다. 어떤 행위를 할지는 나는 행위와 소리내는 행위의 인터페이스 구현체를 주입한다.

public class MalladDuck extends Duck{

    public MalladDuck(){
        this.flyBehavior = new FlyWithWings();
        this.quackBehavior = new QuackLoudly();
        this.name = "천둥오리";
    }
}

public class RubberDuck extends Duck{

    public RubberDuck(){
        this.quackBehavior = new QuackLoudly();
        this.flyBehavior = new FlyNoWay();
        this.name = "러버덕";
    }

}

행위 or 알고리즘을 인터페이스로 만들고

public interface FlyBehavior {
    void fly();
}

public interface QuackBehavior {
    void quack();
}

인터페이스를 구현한다.

public class QuackLoudly implements QuackBehavior{

    @Override
    public void quack() {
        System.out.println("꽥!!!!!!");
    }
}

public class MuteQuack implements QuackBehavior{

    @Override
    public void quack() {
        System.out.println("...(아무런 소리도 나지 않는다.)");
    }
}

public class FlyNoWay implements FlyBehavior{

    @Override
    public void fly() {
        System.out.println("난 날 수가 없어......");
    }
}

public class FlyWithWings implements FlyBehavior{

    @Override
    public void fly() {
        System.out.println("날개를 펼쳐 하늘을 날고 있습니다.");
    }
}

테스트 코드

public class DuckTest {

    public static void main(String[] args){
        Duck malladDuck = new MalladDuck();
        malladDuck.sayMyName();
        malladDuck.performQuack();
        malladDuck.performFly();

        Duck rubberDuck = new RubberDuck();
        rubberDuck.sayMyName();
        rubberDuck.performQuack();
        rubberDuck.performFly();

        Duck woodDuck = new WoodDuck();
        woodDuck.sayMyName();
        woodDuck.performQuack();
        woodDuck.performFly();

    }

}

테스트 결과

  • 레고 조립하듯 구현체 인스턴스를 구성을 변경하는 단 몇 줄만의 코드 수정만으로 기능(캡슐화된 알고리즘)을 변경할 수 있다.
  • 낮게 난다거나 암닭 소리를 낸다 등(...)의 구현을 추가한다면 기존 구현체 클래스를 수정하지 않고 인터페이스 구현체 클래스를 추가 생성하여 간단하게 기능을 추가할 수 있다.
  • setter 메소드를 추가하면 런타임 상에서 중간에 행위를 변경할 수도 있다.

예제 소스

https://github.com/jayceepark/javaDesignPatternStudy/tree/master/out/production/javaDesignPatternStudy/DesignPatterns/StrategyPattern

행위군이 추가되면 어떨까?

난다, 소리를 낸다 외에 먹이를 먹는다 등의 새로운 행위군을 추가해야한다면 인터페이스도 수정해야해하고, 인터페이스 구현체를 수정하고 각 오리의 구성에도 추가해줘야한다.
행위가 추가될 때 구현체에 일일이 추가해야 하는 것은 어쩔 수 없는 선택이다. IDE의 힘을 이용하여 해결하자. 상속을 이용할 수도 있겠지만 상속에는 다중 상속 문제와 상속 구조를 파악하기 어려울 수 있는 문제가 있으므로 인터페이스를 이용하는 것이 좋다.

profile
오늘도 하나 배웠다.

0개의 댓글