[Design Pattern] Strategy Pattern

Loopy·2022년 4월 12일
0

디자인패턴

목록 보기
1/9
post-thumbnail
post-custom-banner

☁️ 전략 패턴이란?

행위 패턴의 한 종류로, 알고리즘을 사용하는 클라이언트로부터, 독립적으로 알고리즘을 바꿔서 적용시킬 수 있도록 하는 패턴이다.

🔖 행위 패턴
클래스와 객체들이 상호작용 하는 방법과 책임을 분산시키는 패턴

즉, 서로 다른 알고리즘들이 존재하고, 실행 중 적합한 알고리즘 선택해서 적용한다. 아래 두 가지 규칙을 모두 만족할 수 있도록, 점차 클래스를 발전시켜나가 보면서 전략 패턴이 왜 필요한지 알아보도록 하자.

  1. 새로운 알고리즘을 추가해도, 기존 코드를 수정하지 않아야 한다.
  2. 코드가 중복되지 않고 재사용이 가능해야 한다.

☁️ 전략 패턴 예시

기존 오리 클래스에서 새롭게 "나는" 기능을 추가하고자 할때, 우선 상속을 생각해볼 수 있다.

1. 상속 사용

하지만 부모 클래스에 기능을 추가하게 되면 다음과 같은 단점이 존재한다.

  1. 관련이 없는 자식 클래스들은, 필요가 없는데도 코드가 중복되는 문제가 발생한다.
  2. 각 오리들의 행동을 정확하게 알 수 없다. 즉, 밀접하게 연관되어 있어 상속받은 자식 클래스들에게 영향이 가게 된다.

2. 인터페이스 사용

변경되지 않는 부분은 클래스로, 변경 여지가 있는 부분은 인터페이스로 분리하였다. 하지만 이 역시 치명적인 단점이 존재한다.

인터페이스는 코드 구현이 불가능하므로, 코드 재사용이 안돼 코드 관리에 문제가 생긴다.(같은 코드를 반복해서 구현하게 된다.)

class MallardDuck extends Duck implements Flyable, Quackable {
    void display() {
        System.out.println("MallardDuck");
    }
    public void quack() {
        System.out.println("quack");
    }
    public void fly() {
        System.out.println("flying");
    }
}

class RedheadDuck extends Duck implements Flyable, Quackable {
    void display() {
        System.out.println("RedheadDuck");
    }
    public void quack() {
        System.out.println("quack");
    }
    public void fly() {
        System.out.println("flying");
}

만약 날아가는 동작을 조금만 바꾸려고 해도, 잘못하면 날아다니는 기능이 포함된 자식 클래스들 50개를 모두 고쳐야 하게 되기 때문이다.

소프트웨어는 항상 변화가 일어난다. 따라서, 변화에 대비해 기존 코드에 미치는 영향을 최소한으로 줄이는 것이 중요하다.

3. Default 메소드 도입

Default 메서드를 도입한다면 중복 코드 문제를 해결 할 수는 있으나, Duck d2 = new MallardDuck() 와 같이 업캐스팅이 불가능한 문제 발생한다.

좋은 설계는 새로운 기능이 추가되도, 다형성에 의해 기존 main 코드를 변경하지 않아야한다. 하지만, 지금은 재사용이 불가능해 잘못된 설계이다.

interface Flyable {
  default void fly() {
    System.out.println("flying");
  }
}

interface Quackable {
  default void quack() {
    System.out.println("quack");
  }
}
public class Main {
  public static void main(String[] args) {
      Duck d1 = new Duck();
      MallardDuck d2 = new MallardDuck();  //업캐스팅 불가능 
      RedheadDuck d3 = new RedheadDuck();
      RubberDuck d4 = new RubberDuck();
      d1.display();
      d2.display();
      d3.display();
      d4.display();
      //d1.quack();   //아예 메소드가 존재 X
      d2.quack();
      d3.quack();
      d4.quack();
      //d1.fly();
      d2.fly();
      d3.fly();
  }
}

4. 전략 패턴 도입

핵심은 변화하는 부분과 그대로 있는 부분을 분리하는 것이다. 따라서 동적으로 바뀌는 부분을 인터페이스로 만들고, 구체적인 특정 행동들은 해당 인터페이스를 상속받도록 한다.

이후 변화하지 않는 클래스인 Duck 에 주입만 해주면, Duck 입장에서는 상위 인터페이스로 받고 있으니 구체적인 알고리즘을 알 필요가 없다. 즉, 서브 클래스들에 국한되지 않아 변경이 전파되지 않는다.

단 이때 실행 시에 쓰이는 객체가, 코드에 고정되지 않도록 추상 클래스나 인터페이스와 같은 상위 타입에 맞추는 것이 중요하다.

public abstract class Duck {
    QuackBehavior quackBehavior;
    FlyBehavior flyBehavior;
    
    public abstract void display();

    public void performQuack() {
       quackBehavior.quack();
    }

    public void performFly() {
       flyBehavior.fly();
    }
    
    public setFlyBehavior(FlyBehavior behavior) { // 동적으로 주입
    	this.flyBehavior = behavior;
    }
    
    public setQuackBehavior(QuackBehavior behavior) {	
        this.quackBehavior = behavior;
    }
    ...
}
public class MallardDuck extends Duck {

	public MallardDuck() {
    	quackBeahavior = new Quack();  // 실행 시점에 주입
        flyBehavior = new FlyWithWings();
    }
}
public static void main(String[] args) {
	Duck mallard = new MallardDuck();
    mallard.performQuack();
    mallard.setQuackBehavior(new NewQuack()); // 다른 거로 변경 가능
    mallard.performQuack();

이처럼 실행 시에 동적으로 다른 구체 클래스들을 할당할 수 있어, 유연하다. 특히 세터 메서드는 상속시에 할당된 행동들 말고 동적으로 다른 행동을 주입할 수 있어 더욱 유연하게 바꿀 수 있다.

A 클래스 안에 B가 있다라고 하는 것을 구성(composition) 이라고 한다.

☁️ 전략 패턴 실제 사용된 예시: Comparator

비교하는 알고리즘은 매우 다양하다. 따라서, 비교 행위 자체를 인터페이스로 다음과 같이 만들고 서로 다른 비교 방법을 구현하면 된다.

@FunctionalInterface
public interface Comparator<T> {  // 알고리즘 캡슐화
    int compare(T o1, T o2);
    ...
}
class MyComparator<T> implements Comparator<T> {

  @Override
  public int compare(T o1, T o2) {
      if (o1 == o2) return 0;
      if (o1 == null) return -1;
      if (o2 == null) return 1;

      return compareNonNull(o1, o2);
  }
}

그리고, Collections.sort() 에서는 Comparator 인터페이스를 파라미터로 받으면서, 실행 시점에 객체가 각기 다른 방법으로 정렬할 수 있도록 보장하고 있다.

public class Collections {
    public static <T> void sort(
    	List<T> list, 
        Comparator<? super T> c  // 동적으로 알고리즘 주입
    ) {
        list.sort(c);
    }
}

☁️ 패턴 정리

패턴이 필요한 경우

여러 알고리즘이 존재하고, 실행 시점(Run-time)에 사용할 알고리즘이 결정되어져서, 조건문 등을 이용해 선택해야 하는 경우에 사용하면 좋다.

중복을 공통화시키고, 상속과 인터페이스 활용해 실행 시점에 맞는 알고리즘을 호출하도록 해결하면 된다.(OCP를 지킬 수 있다.)

profile
개인용으로 공부하는 공간입니다. 잘못된 부분은 피드백 부탁드립니다!
post-custom-banner

0개의 댓글