본문 바로가기
디자인 패턴

3. Strategy Pattern

by spaul 2023. 10. 29.

 첫 번째로 알아볼 디자인 패턴은 행위 패턴의 일종인 Strategy Pattern(스트래티지 패턴)입니다.

▣ What is Strategy Pattern?

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it

 

 스트래티지 패턴은 같은 종류의 작업을 하는 알고리즘을 정의하고, 각 알고리즘을 캡슐화하며, 각 알고리즘들을 서로 바꿔 사용할 수 있도록 하는 디자인 패턴입니다. 다시 말해서, 알고리즘을 사용하는 클라이언트로부터 독립적으로 알고리즘을 바꿔서 적용시킬 수 있도록 합니다.

 

 위와 같이 써놓으니 무슨 말인지 하나도 모르겠네요. 좀 더 쉽게 설명해보겠습니다. 앞선 포스팅인 SOLID에서도 설명 드렸듯이, 우리가 코드를 작성할 때 하나의 Java 클래스 파일에 필요한 모든 내용을 쑤셔넣은 다음 컴파일하여 겨우겨우 프로그램이 동작되도록 했다고 가정해봅시다.

 

 그런데 무엇인가 새로운 기능을 추가해야된다거나, 기존에 있던 내용들을 변경해야되는 경우가 생긴다면 어떨까요? 수 많은 코드 속에서 어떤 부분을 수정하거나 추가해야되는지도 찾기 어려울 뿐더러, 수정해야할 부분을 찾아서 수정했는데 예상치 못한 오류가 발생할 수도 있고, 만약 이런 문제가 전혀 없었더라도 클래스 파일을 처음부터 끝까지 다시 컴파일해야하는 문제가 발생합니다.

 

 스트래티지 패턴은 특히 위와 같은 문제점들을 해결하기 위해 코드의 기능 단위 분리, 즉 캡슐화를 목적으로하는 패턴입니다. 클라이언트 코드에 모든 알고리즘을 포함시키는 것은 유지 보수를 매우 어렵게하는 원인이 되기 때문에, 비슷한 종류의 작업을 하는 코드들을 분리한 다음 실행 시점에 적합한 알고리즘을 선택해서 적용하는 패턴입니다.

 

 우리가 DuckSimulation이라는 프로그램을 만든다고 가정해보겠습니다. 그러면 아래와 같은 클래스 다이어그램과 프로그램 코드를 작성할 수 있습니다.

그림 출처 : 조용주, 고급객체지향프로그래밍

 

class Duck {
    void quack() {
        System.out.println("quack");
    }
    void swim() {
        System.out.println("swimming");
    }
    void display() {
        System.out.println("Duck");
    }
}

class MallardDuck extends Duck {
    @Override
    void display() {
        System.out.println("MallardDuck");
    }
}

class RedheadDuck extends Duck {
    @Override
    void display() {
        System.out.println("RedheadDuck");
    }
}

public class Main {
    public static void main(String[] args) {
        Duck d1 = new Duck();
        Duck d2 = new MallardDuck();
        Duck d3 = new RedheadDuck();
        d1.display();
        d2.display();
        d3.display();
        d1.quack();
        d2.quack();
        d3.quack();
    }
}

 MallardDuck과 RedheadDuck에서 각각 Duck이라는 클래스를 상속받았고 display()를 오버라이드 했습니다. 만약 우리의 시뮬레이터가 여기서 더 기능을 추가할 일이 없다면, 여기서 프로그램 작성을 끝내도 무방합니다. 그런데 우리가 여기서 오리를 날 수 있게하는 기능을 추가하고 싶어서,  간단하게 Duck 클래스에 fly()라는 메서드를 추가한다고 생각해봅시다. 

class Duck {
    void quack() {
        System.out.println("quack");
    }
    void swim() {
        System.out.println("swimming");
    }
    void display() {
        System.out.println("Duck");
    }
    void fly() {
        System.out.println("flying");
    }
}

 Duck() 클래스로부터 상속받는 모든 오리 클래스들이 날 수 있다면, 프로그램이 문제 없이 동작할 것입니다. 그런데 우리가 여기서 날 수 있는 기능이 없는, RubberDuck이라는 장난감 오리 클래스를 추가하고 Duck으로부터 상속받는다고하면, RubberDuck은 날 수 없음에도 fly()라는 메서드를 가지게 됩니다. 이를 해결하려면 RubberDuck 클래스에서 fly()를 오버라이드하여 날 수 없도록 수정할 수 있겠지만, 만약 클래스의 종류가 수 백가지가 된다면 일일이 그렇게 수정하는것은 매우 힘든 작업이 될 것입니다.

 

 따라서 우리는 Duck 클래스로부터 직접 클래스를 상속받도록 하지말고, 중간에 인터페이스를 두어 변경되는 부분을 최소화 하는 것이 좋을 것 같습니다.

 위의 클래스 다이어그램처럼, 날 수 있는 오리들은 Flyable이라는 interface를 구현하고, 소리를 낼 수 있는 오리들은 Quackable이라는 인터페이스를 구현했습니다. 그리고 모든 오리들이 공통적으로 가질 수 있는 메서드인 swim()과 display()는 그대로 Duck클래스에서 상속받도록 해도 될 것 같군요. 

 

 그런데 위의 그림처럼 설계해도 문제인 것이, 만약에 오리마다 울음소리가 달라서 Quackable을 구현하는 모든 클래스를 만들때마다 오버라이드 해줘야된다면, 이 또한 아까와 마찬가지의 문제가 발생합니다. 그래서 차라리 QuackBehavior이라는 인터페이스와 FlyBehavior이라는 인터페이스를 따로 만들어서 이 두 인터페이스를 이용해 새로운 오리 객체를 생성할 때 이용한다면, 우리가 모든 오리 클래스들을 수정해줄 필요가 없지 않을까요?

 

  오리마다 울음소리가 다르거나 아예 울지않는 오리(DecoyDuck)들을 구현하는 QuackBehavior이라는 인터페이스를 하나 만들고, 그 인터페이스를 구현하는 울음소리 클래스들을 만들어서, 오리 객체를 생성할 때 해당 QuackBehavior를 가진 객체를 인수로 넘겨주는것입니다. FlyBehavior도 마찬가지고요. 그래서 우리는 이와 같은 캡슐화를 통해, 아래 그림과 같은 Duck클래스를 만들어 줄 수 있습니다.

 말로만 설명드려서 어려울 수 있는데, 코드를 통해 이해하면 더 쉽게 이해하실 수 있을겁니다. 

public interface QuackBehavior {
    void quack();
}

  우선 QuackBehavior이라는 인터페이스를 만들고

public class Quack implements QuackBehavior {
    @Override
    public void quack() {
        System.out.println("quack");
    }
}

 

public class Squeak implements QuackBehavior {
    @Override
    public void quack() {
        System.out.println("Squeak");
    }
}

 

public class MuteQuack implements QuackBehavior {
    @Override
    public void quack() {
        System.out.println("Mute");
    }
}

Quackable을 구현하는 Quack, Squeak, MuteQuack이라는 클래스를 만들어줍시다.

 

public class Duck {
    private QuackBehavior quackBehavior;

    public Duck(QuackBehavior quackBehavior) {
        this.quackBehavior = quackBehavior;
    }

    public void display() {
        System.out.println("Duck");
    }

    public void swim() {
        System.out.println("Swimming");
    }

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

 

public class MallardDuck extends Duck {
    public MallardDuck(QuackBehavior quackBehavior) {
        super(quackBehavior);
    }

    @Override
    public void display() {
        System.out.println("Mallard Duck");
    }

}

 

public class DecoyDuck extends Duck {
    public DecoyDuck(QuackBehavior quackBehavior) {
        super(quackBehavior);
    }

    @Override
    public void display() {
        System.out.println("Decoy Duck");
    }

}

 그런 다음 공통적인 오리의 속성과 기능을 가지는 것들을 Duck 클래스에서 정의한 다음, MallardDuck과 DecoyDuck에서 Duck을 오버라이드하여 달라지는 부분들을 구현해줍니다. 

public class DuckSimulation {
    public static void main(String[] args) {
        QuackBehavior q1 = new Quack();
        QuackBehavior q2 = new Squeak();
        QuackBehavior q3 = new MuteQuack();

        Duck d1 = new Duck(q1);
        Duck d2 = new MallardDuck(q2);
        Duck d3 = new DecoyDuck(q3);

        d1.display();
        d1.performQuack();

        d2.display();
        d2.performQuack();

        d3.display();
        d3.performQuack();
    }
}

 테스트를 위해 간단한 main() 함수를 작성해봤습니다. 아래 결과를 보시면

 

 

 이처럼 예상했던 결과가 나오네요. 코드가 너무 길어질 것 같아서 일부러 클래스다이어그램에서 생략한 부분들이 많습니다. 직접 한 번 따라해보시고, 이해가 안되시거나 궁금하신 것들은 댓글로 남겨주시면 감사하겠습니다.

 

 이 정도면 스트레티지 패턴의 핵심적인 부분들은 전부 설명드린 것 같네요. 다시 한 번 스트래티지 패턴을 그림과 함께 요약해보겠습니다.

 

- Context : 캡슐화된 알고리즘(Strategy)을 멤버 변수로 포함하며, 캡슐화된 알고리즘을 교환해서 적용시킬 수 있습니다.

 

- Strategy : 컴파일 시점에서 사용하는 캡슐화된 알고리즘을 나타내며, 실제 구현은 하위(자식) 클래스에 위임합니다.

 

- StrategyN : 실행 시점에 적용될 알고리즘을 캡슐화하여, Context에서 실행될 알고리즘을 구현합니다.

 

References

[1] 조용주, 고급객체지향프로그래밍

[2] Eric Freeman, Head Frist Design Patterns

'디자인 패턴' 카테고리의 다른 글

5. Singleton Pattern  (0) 2023.11.13
4. Observer Pattern  (0) 2023.11.07
2-2. SOLID(S.O.L.I.D)  (0) 2023.10.18
2-1. SOLID(S.O.L.I.D.)  (0) 2023.10.16
1. Software Design Pattern  (0) 2023.10.06