[Design Pattern] Strategy Pattern이란?

Junseong·2021년 4월 15일
0
post-thumbnail

목차

  1. 등장 배경
  2. 패턴 설명
  3. 패턴 정의

본 시리즈는 'Head First Design Patterns' 책을 통해 공부한 내용을 참고 및 각색하여 작성되어졌습니다. 전체 코드는 Github 에서 확인할 수 있습니다.


등장 배경

과거 우리의 선배 개발자님들은 상속을 이용해서 코드의 재사용성을 확보했었다고 합니다.

자, 여기 오리 시뮬레이터 게임을 만드는 회사의 한 개발자의 경험을 통해 어떻게 상속을 이용해서 코드의 재사용성을 확보했는지 알아봅시다.

게임 안의 오리들을 구현해야 되는 과제를 받은 이 개발자는 자신의 객체지향 지식을 이용해 다음과 같이 설계를 하였습니다. Duck이라는 상위클래스를 만들고 이 클래스를 상속받아 다른 모든 종류의 오리를 만들었습니다.

상식적으로 모든 오리는 날 수 있으며 수영도 할 줄 알고 "꽥" 소리도 낼 줄 압니다. 이런 공통적인 동작을 Duck 클래스에 정의 하고 다른 하위 클래스들이 이를 상속받아 사용함으로써 코드를 재사용한 모습을 볼 수 있습니다.

// 상속 예제 코드

public abstract class Duck {
        public void fly() {
            System.out.println("저는 날고있어요!");
        }
        public void quack() {
            System.out.println("꽉");
        }
        public void swim() {
            System.out.println("저는 수영하고있어요!");
        }
        public abstract void display();
}

public class redheadDuck extends Duck{
    @Override
    public void display() {
        // 적당한 모양을 표시
    }
}
    
public class MallardDuck extends Duck{
    @Override
    public void display() {
        // 적당한 모양을 표시
    }
}

Duck redheadDuck = new redheadDuck();
redheadDuck.quack();    // "꽉"

Duck mallardDuck = new MallardDuck();
mallardDuck.quack();    // "꽉"

다음날, 자신의 뛰어난 객체 지향적 능력에 만족하던 이 개발자에게 일부 오리들의 울음소리를 "꽉" 말고 "꽥"으로 해달라는 요구사항이 들어왔습니다.

개발자는 요구사항을 수행하기 위해 모든 서브 클래스 오리들이 display()처럼 quack()을 오버라이드해서 소리를 다르게 내게 했습니다. 아래의 그림처럼요.

이렇게 요구사항을 해결해 한숨을 돌리던 개발자에게 이번에는 고무로 된 장난감 오리를 만들어 달라는 요구사항이 들어왔습니다. 안타깝게도 고무로 된 오리는 날지 못하기 때문에 fly() 메소드도 오버라이드하여 요구사항을 해결했습니다.

이쯤 되니 이 개발자는 무언가 잘못되고 있음을 깨닫게 되었습니다. 처음에는 분명히 상속을 통해 코드를 재사용하고 있었지만 새로운 요구사항이 생길 때마다 코드의 재사용성이 점점 떨어지고 있다는 것을 말이죠.

여기서 우리는 상속의 단점 에 대해 알 수 있습니다.

Duck과 같은 상위 클래스에서는 의미가 있던 기능들이 RubberDuck과 같은 하위 클래스에서는 의미가 없는 기능일 수도 있습니다.

그렇다면 과연 이를 어떻게 해결할 수 있을까요?

이를 해결하기 위해 고안된 디자인 패턴이 바로 Strategy Pattern 입니다.


패턴 설명

Strategy Pattern세 가지의 디자인 원칙을 적용한 패턴이라고 할 수 있습니다.

우선 우리가 앞서 등장한 오리 시뮬레이터 게임의 개발자가 되었다고 생각하고 디자인 원칙들을 하나하나 적용해나가며 Strategy Pattern에 대해 알아보도록 하겠습니다.

중간에 이해가 안되더라도 일단은 쭉 읽어보세요. 끝까지 읽어보면 왜 그랬는지 이해가 가실 수도 있을 거에요.

디자인 원칙 첫 번째

애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분으로부터 분리한다.

이는 '캡슐화한다' 라고도 표현합니다.

우리는 기존 오리 시뮬레이터 게임의 코드에서 fly() 메서드와 quack() 메서드가 하위 클래스마다 달라지고 있음을 확인했습니다.

그 외의 부분들은 자주 달라지거나 바뀌지 않습니다. (실제로는 달라질 가능성이 충분히 있지만 적어도 여기서는 그렇지 않다고 가정합시다)

그래서 우리는 fly()와 quack()을 달라지는 부분으로 결정하고 Duck으로부터 분리할 것입니다. 그런데 어떻게 분리를 시켜야 하는 걸까요?


디자인 원칙 두 번째

직접적인 구현이 아닌 인터페이스에 맞춰서 프로그래밍한다.

디자인 원칙에 따라 나는 동작과 소리를 내는 동작을 인터페이스화 시켜봅시다.

fly() 메서드를 가진 FlyBehavior 인터페이스를 정의하고 하늘을 나는 방식별로 이를 구현한 객체들을 만듭니다. quack() 메서드도 동일하게 수행합니다.

만약 fly()와 quack()을 인터페이스가 아닌 클래스로 구현하여 분리를 시킨다면 하위클래스들을 일괄적으로 처리하기 힘들어질 것입니다.

이제는 클래스로부터 분리시켰던 기능들을 합쳐 오리 시뮬레이터 게임을 완성시켜 보겠습니다.


디자인 원칙 세 번째

상속보다는 구성을 활용한다.

참고로 구성(composition)을 활용한다는 것은 두 클래스의 관계를 'redheadDuck은 오리이다' 와 같이 is-a 관계가 아니라 'redheadDuck은 날개를 가지고 있다' 와 같이 has-a 관계를 성립하게 합치는 것을 말합니다.

우리가 만든 FlyBehavior 인터페이스와 QuackBehavior 인터페이스를 Duck의 멤버 변수로 집어넣습니다.

이렇게 하면 'Duck은 하늘을 나는 행동의 집합인 FlyBehavior를 가지고 있다'라는 has-a 관계가 성립됩니다. 즉, 구성을 활용하였다는 뜻입니다.

그리고 Duck의 하위 클래스들이 각자에게 맞는 하늘을 나는 행동을 선택할 수 있게 Duck 클래스에 setFlyBehavior()메소드를 만들어 줍니다.

이처럼 setter 메소드를 통해 의존성을 주입하는 것을 setter injection 또는 method injection이라 합니다. 이 방법 말고도 다른 방법들이 존재하는데 자세히 알고 싶다면 '의존성 주입' 혹은 'Dependency Injection' 키워드로 검색해보세요.

인터페이스를 구현한 객체의 fly(), quack() 메소드를 실행시키는 performFly(), performQuack() 메서드도 만들어 줍니다.

글만으로는 이해가 잘 안 되니 다이어그램과 코드를 살펴봅시다.

public abstract class Duck {

    // 인터페이스 멤버 변수로 추가
    private FlyBehavior flyBehavior;
    private QuackBehavior quackBehavior;

    public void swim() {
        System.out.println("저는 수영하고있어요!");
    }
    public abstract void display();

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

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

    // setter를 통한 의존성 주입
    public void setFlyBehavior(FlyBehavior flyBehavior) {
        this.flyBehavior = flyBehavior;
    }

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

public class redheadDuck extends Duck{
    @Override
    public void display() {
        // 적당한 모양을 표시
    }
}

public class MallardDuck extends Duck{
    @Override
    public void display() {
        // 적당한 모양을 표시
    }
}

public class RubberDuck extends Duck{
    @Override
    public void display() {
        // 적당한 모양을 표시
    }
}

public interface FlyBehavior {
    public void 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("저는 날지 못해요!");
    }
}

public interface QuackBehavior {
    public void quack();
}

public class Quack implements QuackBehavior{
    @Override
    public void quack() {
        // 꽉
    }
}

public class Bbick implements QuackBehavior{
    @Override
    public void quack() {
        // 삑
    }
}

public class Quaeck implements QuackBehavior{
    @Override
    public void quack() {
        // 꽥
    }
}

자, 이렇게 오리 연못 시뮬레이터 게임에 Strategy Pattern을 적용시켜 보았습니다.

마지막으로 게임 속에 오리들을 풀어놔 보도록 하겠습니다. (Constructor Injection을 이용하거나 객체가 생성될때 초기화 작업을 수행해 주면 아래에서 나타나는 set메서드들을 전부 생략할 수 있습니다)

FlyBehavior flyWithWings = new FlyWithWings();
FlyBehavior flyNoWay = new FlyNoWay();
QuackBehavior quack = new Quack();
QuackBehavior quaeck = new Quaeck();
QuackBehavior bbick = new Bbick();

Duck redheadDuck = new redheadDuck();
redheadDuck.setFlyBehavior(flyWithWings);
redheadDuck.setQuackBehavior(quack);
redheadDuck.performFly();      // 저는 날고 있어요!
redheadDuck.performQuack();    // "꽉"

Duck mallardDuck = new MallardDuck();
mallardDuck.setFlyBehavior(flyWithWings);
mallardDuck.setQuackBehavior(quaeck);
mallardDuck.performFly();      // 저는 날고 있어요!
mallardDuck.performQuack();    // "꽥"

Duck rubberDuck = new RubberDuck();
rubberDuck.setFlyBehavior(flyNoWay);
rubberDuck.setQuackBehavior(bbick);
rubberDuck.performFly();      // 저는 날지 못해요!
rubberDuck.performQuack();    // "삑"

어떤가요? 언뜻보면 더 복잡해 보이는 듯 합니다만 이제는 새로운 요구사항이 오더라도 구조가 유연하다보니 쉽고 빠르면서 안전하게 코드를 재사용할 수 있습니다.

예를 들어 "로켓의 추진력으로 날아가는 오리를 만들어 주세요" 같은 요구사항도 FlyBehavior 인터페이스를 구현한 FlyWithRocket 클래스만 만들면 해결 됩니다.

아니면 "연못에 있는 오리는 무조건 "꽥" 하고 울게 해주세요" 같은 경우도 오리가 연못에 있을 경우 setQuackBehavior()메서드를 사용해 동적으로 오리의 QuackBehavior를 Quack 객체로 바꿔주면 해결 됩니다.


패턴 정의

Strategy Pattern에서는 알고리즘군을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만든다. Strategy를 활용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.

여기서 알고리즘이란 앞의 예시에서 하늘을 나는 행동, 울음소리를 내는 행동과 같이 애플리케이션에서 달라지는 부분을 뜻하며 알고리즘군은 달라지는 부분인 알고리즘에 대한 집합을 의미합니다.

또한 알고리즘을 사용하는 클라이언트는 예제에서 Duck에 해당됩니다.

profile
#취준생 #Back-end

0개의 댓글