1. 전략 패턴 (Strategy Pattern)

Kim Dong Kyun·2023년 6월 21일
10

Design Pattern

목록 보기
1/5
post-thumbnail

이미지 출처

전략 패턴의 정의

전략 패턴 (Strategy patter)은

1.알고리즘군을 정의하고
2. 캡슐화해서
각각의 알고리즘군을 수정해서 쓸 수 있게 해줍니다.

전략 패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경 할 수 있습니다.

  • By 헤드퍼스트 디자인패턴

사건의 발단 - 모든 오리는 꽥꽥거리는가?

예를 들어, 다음과 같은 상황이 있다고 하자.

당신은 "DuckSimulator"의 개발자이다. 이 프로그램은 세상의 모든 오리의 행동을 구현해낸 유일한 프로그램이며, 세상의 모든 오리는 매우 다양하므로 당신은 Duck이라는 슈퍼클래스 아래에 모든 오리를 구현하겠다고 결심했다.

  • 그래서, 다음과 같은 코드를 작성하기에 이른다.
public abstract class FirstDuck {
    public void fly(){
        System.out.println("날고있어요!");
    }

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

    public void swim(){
        System.out.println("모든 오리는 물에 뜬다!");
    }

    public FirstDuck(){} // 생성자

    public abstract void display(); // "내가 어떤 오리인가"를 보여주는 매서드
}
  • 위와 같이 추상 클래스를 작성하고, 여러 구현체들이 위를 상속받았다

그런데, 오리 시뮬레이터에 이상한 일이 생겼다!

  • RubberDuck (고무오리) 가 날아다니면서, 꽥꽥 소리를 내기 시작 한 것이었다!!!

정말 기괴하지 않을 수 없었다. 따라서 당신은 황급히 매서드를 오버라이드 해서 재정의하기로 한다.

public class RubberDuck extends FirstDuck{
    @Override
    public void display() {
        System.out.println("저는 고무오리입니다");
    }
    @Override
    public void quack(){
        System.out.println("삑!"); // 고무오리는 삑!
    }
    
    @Override
    public void fly(){
        System.out.println("<아무일도 일어나지 않았다>"); // 못날아요!
    }
}

역시 당신은 슈퍼 개발자! 한번에 문제를 해결했다.

하지만, "모든" 오리가 존재하는 DuckSimulator 프로그램에서 RubberDuck과 같은 구현체는 너무너무 많았다.

당신은 그 모든 구현체 (20152개)에 @Override 해야하는 비극적인 상황을 마주하게 된 것이다!


당신은 슈퍼개발자 이므로

이런 식의 반복은 오한이 들고 정신이 아득해질 것이다. 그렇다면 당신은 어떻게 이 부분을 해결해야 할까?

"오리" 슈퍼클래스가 가지는 알고리즘을 인터페이스로 먼저 정의하고, 그 알고리즘의 구현체를 적절히 캡슐화해서 사용 할 수 있지 않을까? 라는 생각에 이르게 된다.

1. Quack! (꽥!)

public interface QuackBehavior {
    public void quack();
}
  • 당신은 위와 같이 오리가 가진 알고리즘 중 하나, 꽥! 소리를 내는 행동의 추상을 정의했다.
  • 모든 오리는 꽥! 비슷한 것을 할 것이다. 그것은 꽥! 삑! 혹은 "아무일도 일어나지 않음" 등으로 정의가 가능하다.
public class Quack implements QuackBehavior{
    @Override
    public void quack() {
        System.out.println("꽥!");
    }
}
public class MuteQuack implements QuackBehavior{
    @Override
    public void quack() {
        System.out.println("<아무일도 일어나지 않았다>");
    }
}
public class Squeak implements QuackBehavior{
    @Override
    public void quack() {
        System.out.println("삑!");
    }
}

위와 같이 세 개의 적절한 구현체를 정의해서 사용이 가능하다!

  • 더해서, 오리는 날 수 있거나, 날 수 없을 것이다. 따라서 오리는 "fly"알고리즘 + 적절한 구현체가 필요하다

2. Fly

public interface FlyBehavior {
    public void fly();
}
public class CanFly implements FlyBehavior{
    @Override
    public void fly() {
        System.out.println("날 수 있다말이야!");
    }
}
public class CantFly implements FlyBehavior{
    @Override
    public void fly() {
        System.out.println("못 난다 말이야!");
    }
}

그렇다면, 우리의 오리들은 이 알고리즘들을 어떻게 사용 할 수 있을까?


FirstDuck은 이제 사라지고, "진짜" 슈퍼클래스인 Duck 클래스를 정의했다.

슈퍼클래스 "Duck"

public abstract class Duck {
    FlyBehavior flyBehavior;
    QuackBehavior quackBehavior;

    public Duck(){}

    public abstract void display();
    public void performFly(){
        flyBehavior.fly();
        // flyBehavior 에는 1. 난다, 2. 못난다 조정. (고무오리, 청동오리 ...)
    }

    public void performQuack(){
        quackBehavior.quack();
        // quackBehavior 는 1. 꽥! (청둥오리) 2. 삑! (고무오리==러버덕) 3. 무음 (나무오리)
    }

    public void swim(){
        System.out.println("모든 오리는 물에 뜬다!");
    }
  • 위와 같이 performAlgorithm 형태로 매서드를 재정의했다.

이제 오리의 구현체를 손봐주러 가자

public class RealDuck extends Duck{
    @Override
    public void display() {
        System.out.println("판사님 저는 진짜 오리입니다");
    }

    public RealDuck() {
        quackBehavior = new Quack();
        flyBehavior = new CanFly();
    }
}
  • 이 RealDuck 클래스는 생성자 매서드를 통해서 이 녀석이 스스로 어떤 알고리즘을 가지고 있는지를 정의할 수 있다.

  • 따라서, 이 녀석의 모든 인스턴스는 이미 정의된 알고리즘의 구현체를 적절히 선택해서 태어난다

와!! 이러면 상속의 이점 + 코드의 중복을 줄일 수 있겠다.

  • 그런데, 당신은 문득 불안해졌다.

"재미있는 이벤트를 준비했는데, 러버덕 같은 고무오리들도 날 수 있게 해보자! 사용자들이 좋아할 것 같은데?"


유연성을 위해!

  • 어지럼증을 느끼던 당신은, 간단한 해결책을 떠올렸다

public abstract class Duck {
    FlyBehavior flyBehavior;
    QuackBehavior quackBehavior;
	
    ...
    
    public void setFlyBehavior(FlyBehavior flyBehavior){
        this.flyBehavior = flyBehavior;
    }

    public void setQuackBehavior(QuackBehavior quackBehavior){
        this.quackBehavior = quackBehavior;
    }
}

위와 같이 알고리즘의 구현체를 선택 가능하게 만드는 setter 매서드를 Duck 슈퍼클래스에 내장해놓는 방법이었다!

  • 우리는 위와 같은 "전략 패턴"을 통해서 해당 객체가 가지는 알고리즘의 변경 가능성이 높을 때, 혹은 알고리즘 하나에 여러 구현 클래스가 필요 할 때 (오리의 꽥꽥은 1. 꽥 2. 무음 3. 삑...) 언제든 편하게 확장 + 유연하게 변경 할 수 있게 되었다.

정리 : 디자인패턴의 장단점!?

장점

  • 클래스가 가지는 알고리즘의 구현체의 추가가 아주 자유롭다
  • 오리가 아닌 예를 들어 보자면,
    : 할인 정책은 지속적 추가/삭제가 이루어질 가능성이 높다.
    그러나 전략 패턴을 사용해서 관리하면, 추가 삭제의 코스트가 매우 낮을 것이다.

단점

  • 때에 따라서 더 읽기 힘들 수 있다.

  • 예를 들어, 오리의 flyable 인터페이스의 경우 if/else 구문으로 처리하는 게 더 가독성 측면에서는 유리할 것이다 (왜냐면, YES/NO 두 가지 알고리즘이니까)

즉, 전략 패턴을 어디에나 적용 할 수 있는 것은 아니다. 이 점이 매우 중요하다

모든 디자인 패턴은 상황에 따라 적절히 선택되어야 한다.


위 내용은, "헤드퍼스트 디자인패턴"의 실습 내용입니다.


모든 코드는 깃허브에 있습니다.

4개의 댓글

comment-user-thumbnail
2023년 6월 21일

섹시균

1개의 답글
comment-user-thumbnail
2023년 6월 28일

지존 섹시

1개의 답글