JAVA/디자인 패턴

[Strategy Pattern] 전략 패턴

dev_rosieposie 2023. 7. 6. 18:23

Goal 

  1. 전략 패턴에 대해 알아보고 이해한다
  2. 전략 패턴이 필요한 경우의 예시를 알아본다
  3. 문제를 파악하고, 디자인 원칙에 대해 알아본다
  4. 디자인 원칙에 입각한 전략패턴 이해하기
  5. 캡슐화된 행동 살펴보고 이해하기
  6. 클래스를 합치는 방법은 무엇인가?

 

전략 패턴 (Strategy Parttern) 

객체 동작을 실시간으로 바뀌도록 할 수 있게 하는 행위 디자인 패턴 이다. 여기서 '전략'이란 일종의 알고리즘이 될 수 도 있으며, 기능이나 동작이 될 수도 있는 특정한 목표를 수행하기 위한 행동 계획을 말한다.

 

 

왜 or 언제 전략 패턴을 사용하는가?

어떤 일을 수행하는 알고리즘이 여러가지 일때, 동작들을 미리 전략으로 정의함으로써 손쉽게 전략을 교체할 수 있는, 알고리즘 변형이 빈번하게 필요한 경우에 적합한 패턴이기 때문이다

 

 

전략 패턴 사용 시 장점

  • 전략 패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있다 
  • 달라지는 부분을 찾아 나머지 코드에 영향을 주지 않도록 캡슐화한다 => OCP 개방폐쇄의 원칙 
  • 의도치 않게 발생하는 일을 줄이며 시스템 유연성 향상

 

as - is 오리 시뮬레이션 게임 관점에서 생각해보기

 

상황 : 오리 시뮬레이션 게임을 만드는 회사가 있다. 이 게임에는 1.헤엄도 치고, 2.꽥꽥 소리도 내는 등 매우 다양한 오리가 등장한다.

이 시스템을 처음 디자인 한 사람은 표준 객체지향 기법을 사용하여 Duck이라는 슈퍼클래스를 만들고, 그 클래스를 확장해서 서로 다른 종류의 오리를 만들었다.

 

요구 사항 : 오리들이 날 수 있도록 하라

 

해결 방안 1 : Duck 클래스에 fly() 메소드만 추가해서 모든 오리가 그 메소드를 상속 받게 하기 

 

문제점 : Duck이라는 슈퍼클래스에 fly() 메소드를 추가하면서 일부 서브 클래스에 적합하지 않은 행동이 추가됨 

=> 일부만 고쳤는데 전체에 고무오리가 날아다니는 오류 발생 => 코드를 재사용했지만 유지보수 관점에서는 좋지 않음

 

 

해결 방안 2 : 상속을 하여 아무것도 하지 않도록 오버라이드 하기

문제점 : 제품이 자주 업데이트가 되면 규격이 계속 바뀔 것이므로 상속을 활용하면 규격이 바뀔때마다 Duck의 서브클래스 fly()와 quack()메소드를 일일이 살펴보고 상황에 따라 오버라이드 해야함

 

해결 방안 3 : 인터페이스를 활용하여 행위를 따로 분리하기

문제점 : 모든 서브클래스에 날거나 꽥꽥거리는 기능이 있어야 하는 것은 아니므로 상속이 올바른 방법은 아니다. 서브클래스에서 Flyable, Quackable을 구현해서 (고무 오리가 날아다니는 것과 같은) 일부 문제점은 해결할 수 있지만, 코드를 재사용하지 않으므로 코드관리에 문제가 발생하며, 날 수 있는 오리 중에서도 날아다니는 방식이 서로 다를 수 있다면 서브클래스 모두 고쳐야하는 번거로움이 존재한다.

 

문제를 명확하게 파악하기

해결 방안으로 제시된 위의 3가지 경우는 문제점이 여전히 존재해보인다. 하지만 이 상황에서 어울릴만한 디자인 원칙이 있다.

 

디자인 원칙

01. 애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리한다.
02. 바뀌는 부분은 따로 뽑아서 캡슐화한다. 그러면 나중에 바뀌지 않는 부분에는 영향을 미치지 않고 그 부분만 고치거나 확장할 수 있다

=> OCP 개방폐쇄의 원칙

 

디자인원칙을 준수하는 4단계 

1. 바뀌는 부분과 그렇지 않은 부분 분리하기

  • 그대로 있는 부분 : Duck 클래스
  • 바뀌는 부분 : fly(), quack()
    • 클래스 집합(set)을 2개 생성 (fly관련, quack관련)
    • 각 클래스 집합에는 각각의 행동을 구현한 것을 넣는다

2.오리의 행동을 디자인하기

  • 목표 정하기 
    • Duck의 인스턴스 할당
    • 특정 형식의 나는 행동으로 초기화하는 방법
    • 오리의 행동을 동적으로 바꾸기

즉, Duck클래스에 행동과 관련된 setter 메소드를 포함해서 프로그램 실행중에도 MallardDuck의 나는 행동을 바꾸기

  • 각 행동을 인터페이스로 사용하여 구현 
    • FlyBehavior 인터페이스의 fly()
    • FlyBehavior를 구현한 FlyWithWings 클래스
    • FlyBehavior를 구현한 FlyNoWay 클래스

이제부터 Duck의 행동은 별도의 클래스 안에 들어있다. 그러면 Duck 클래스에서는 그 행동을 구체적으로 구현할 필요가 없다.

즉, 실제 행동 구현(FlyBehavior, QuackBehavior를 구현하는 클래스에 코딩되어있는 구체적인 행동)은  Duck 서브클래스에 국한되지 않는다.

  •  

3.오리의 행동을 구현하기

  • FlyBehavior, QuackBehavior 2개의 인터페이스에 맞춰서 구체적인 행동을 구현하는 클래스 만들기

위와 같이 디자인하면 다른 형식의 객체에서도 나는 형식과 꽥꽥거리는 행동을 재사용할 수 있다. 더 이상 Duck 클래스 안에 숨겨져 있지 않기 때문이다. 그리고 기존의 행동 클래스를 수정하거나 날아다니는 행동을 사용하는 Duck 클래스를 수정 없이 새로운 행동을 추가할 수 있다. => 상속을 사용시 부담을 떨쳐버리고 재사용의 장점을 누릴 수 있다.

4.오리의 행동 통합하기

가장 중요한 점은 나는 행동과 꽥꽥거리는 행동을 Duck클래스(또는 그 서브클래스)에서 정의한 메소드를 써서 구현하지 않고 다른 클래스에 위임하는 것이다.

public abstract class Duck {
    FlyBehavior flyBehavior;
    QuackBehavior quackBehavior;

    public Duck() {}

    public abstract void display();

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

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

    public void swim(){
        System.out.println("모든 오리는 물에 뜹니다. 가짜 오리도 뜨죠");
    }
}
  • flyBehavior와 quackBehavior라는 인터페이스 형식의 인스턴스 변수를 추가
  • 각 오리 객체에서는 실행 시 이 변수에 특정 행동 형식(FlyWithWings, Squack 등)의 레퍼런스를 다형적으로 설정
  • 나는 행동과 꽥꽥거리는 행동은 인터페이스로 분리했으므로, Duck클래스와 모든 서브 클래스에서 fly(), quack()메소드 제거
  • Duck클래스에 performFly(), performQuack()이라는 메소드 추가
  • quackBehavior.quack();
    • 꽥꽥거리는 행동을 직접 처리하는 대신, quackBehavior로 참조되는 객체에 그 행동을 위임한다
public class MaralldDuck extends Duck {

    public MaralldDuck() {
        quackBehavior = new Quack();
        flyBehavior = new FlyWithWings();
    }

    @Override
    public void display() {
        System.out.println("저는 물오리 인데용");
    }
}
  • MallardDuck은 Duck클래스에서 quackBehavior과 flyBehavior 인스턴스를 상속 받는다
  • MallardDuck이 꽥꽥거리는 행동을 처리할 때는 Quack클래스를 사용하므로 performQauck()이 호출되면 꽥꽥거리는 행동은 Quack객체에게 위임된다. 결과적으로 꽥꽥 소리를 들을 수 있음, 그리고 FlyBehavior 형식으로는 FlyWithWings를 사용한다.

 

오리 코드 테스트 

Duck 클래스

public abstract class Duck {
    FlyBehavior flyBehavior;
    QuackBehavior quackBehavior;

    public Duck() {}

    public abstract void display();

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

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

    public void swim(){
        System.out.println("모든 오리는 물에 뜹니다. 가짜 오리도 뜨죠");
    }
}

FlyBehavior 인터페이스

public interface FlyBehavior {
    public void fly();
}

FlyWithWings, FlyNoWay 행동 구현 클래스

public class FlyWithWings implements FlyBehavior{

    @Override
    public void fly() {
        System.out.println("날고 있어요");
    }
}
public class FlyNoway implements FlyBehavior{

    @Override
    public void fly() {
        System.out.println("저는 못 날아요");
    }
}

QuackBehavior 인터페이스

public interface QuackBehavior {
    public void quack();
}

Quack, MuteQuack, Squeak 행동 구현 클래스

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 Squack implements QuackBehavior{
    @Override
    public void quack() {
        System.out.println("삑");
    }
}

테스트 클래스

public class MiniDuckSimulator {

    public static void main(String[] args){
        Duck mallard = new MaralldDuck();
        mallard.performQuack();
        mallard.performFly();
    }
}

결과


날고 있어요

 

동적으로 행동 지정하기 

1. Duck 클래스에 아래 메소드 추가 - 언제든지 오리의 행동을 즉석에서 바꿀 수 있도록!

public void setFlyBehavior(FlyBehavior fb) {
    this.flyBehavior = fb;
}

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

 

2. Duck의 서브클래스(ModelDuck.java)를 새로 만든다

public class ModelDuck extends Duck{

    public ModelDuck() {
        flyBehavior = new FlyNoway();
        quackBehavior = new Quack();
    }

    @Override
    public void display() {
        System.out.println("나는 모형 오리");
    }
}

-> 날지 못하는 오리

 

3. FlyBehavior 형식의 클래스(FlyRocketPowerd.java)를 새로 만든다

public class FlyRocketPowered implements FlyBehavior{
    @Override
    public void fly() {
        System.out.println("로켓 추진으로 날아갑니다");
    }
}

4. 테스트 클래스 ModelDuck 추가하고 로켓 추진 기능 부여

public class MiniDuckSimulator {

    public static void main(String[] args){
        Duck mallard = new MaralldDuck();
        mallard.performQuack();
        mallard.performFly();

        Duck model = new ModelDuck();
        model.performFly();
        model.setFlyBehavior(new FlyRocketPowered());
        model.performFly();
    }
}

결과

날고 있어요
저는 못 날아요
로켓 추진으로 날아갑니다

 

=> 실행 중에 오리의 행동을 바꾸고 싶으면 원하는 행동에 해당하는 Duck의 세타 메소드를 호출한다

 

캡슐화된 행동 살펴보기

클라이언트에서는 나는 행동과 꽥꽥거리는 행동을 캡슐화된 알고리즘으로 구현한다

 

두 클래스를 합치는 방법 

"A에는 B가 있다". 각 오리에는 FlyBehavior와 QuackBehavior가 있으며, 각각 나는 행동과 꽥꽥거리는 행동을 위임받는다.

이런 식으로 두 클래스를 합치는 것을 구성(composition)을 이용한다라고 한다.

=> 오리 클래스 에서는 행동을 상속받는 대신, 올바른 행동 객체로 구성되어 행동을 부여받는다.

 

디자인 원칙

03. 상속보다는 구성을 사용한다

 

 

 

 

참고

헤드퍼스트 디자인 패턴

 


후 다이어그램 그리느라 시간이 더 소요됬지만 만족스럽다 ! 

이제 구성은 완벽히 이해가 되었다 히히 

ㅎㅎㅎ