전략 패턴(Strategy Pattern)

wannabeking·2022년 10월 3일
0

디자인 패턴

목록 보기
2/14
post-thumbnail

상속의 문제점

전략 패턴을 사용하면 상속(Inheritance, is-a)에서 발생하는 문제점을 해결할 수 있다.

슈퍼 클래스 A가 존재하고 상속 받는 B, C, D...가 존재한다고 가정하자.

모든 객체에서 동일한 행위를 A에서 구현하고 공통되지 않은 행위는 인터페이스를 지정하여 상속 받을 것이다.

그렇다면 B와 C가 동일한 행동을 하기 때문에 동일한 인터페이스를 상속 받아 동일하게 구현한다면, 이 것은 캡슐화인가?

예시를 들어보자.

코숏 고양이, 페르시안 고양이와 모형 고양이가 존재한다.
코숏과 페르시안 고양이는 "야옹~!"하고 울지만 모형 고양이는 울 수 없다.
따라서 코숏과 페르시안은 Meowable을 상속 받아 구현한다.

@Override
public void meow() {
	System.out.println("야옹~!");
}

만약 샴 고양이가 추가된다면 코드를 복붙하여 똑같이 구현해야할 것이다.
고양이 종류 100마리가 추가된다면...?

따라서 우리는 상속대신 다른 방법이 필요하다.

그 방법은 바로 구성(Composition, has-a)이다.

앞서 말생한 문제를 구성을 사용한 전략 패턴으로 해결해보자.



전략 패턴

고양이를 예시로 한번 사용했으니 다음은 오리로 넘어가자.


public abstract class Duck {

    protected FlyBehavior flyBehavior;

    public Duck() {
    }

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

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

    public void swim() {
        System.out.println("모든 오리는 수영할 수 있습니다.");
    }

    public abstract void display();
}

오리의 생김새는 종마다 모두 다르기 때문에 display()는 abstract이다.

모든 종류의 오리는(모형 오리라도)물에 뜰 수 있다고 가정하여 swim()은 구현되어있다.

우리는 FlyBehavior에 주목해야한다.
상속 받지 않고 멤버 변수에 선언되어 있다.

그렇다면 이것이 어떻게 상속의 문제점을 해결하는 것일까?

전략 패턴을 구현하는 방식에는 지금 설명하는 것처럼 필드로 주입 받아 사용하는 방법도 있지만, 메소드 파라미터로 전달 받아 사용하는 방법도 있다.
예를 들어 performFly(FlyBehavior flyBehavior)
템플릿 콜백 패턴이라고 부르기도 한다.


public interface FlyBehavior {

    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 class FlyWithRocket implements FlyBehavior {

    @Override
    public void fly() {
        System.out.println("로켓을 타고 날아다녀요.");
    }
}

정답은 위와 같다.

상속 받은 클래스에서 구현 하여 행동을 캡슐화했다.

해당 클래스를 가져다 멤버 변수로 사용하기 때문에 새로운 오리가 추가되더라도 이미 구현된 FlyWithWings 등은 새롭게 구현할 필요 없이 가져다 쓰면 된다.

그렇다면 이제 두 종류의 오리를 구현하고 실행 결과를 확인하자.


public class MallardDuck extends Duck {

    public MallardDuck() {
        this.flyBehavior = new FlyWithWings();
    }

    @Override
    public void display() {
        System.out.println("저는 청둥오리입니다.");
    }
}
public class ModelDuck extends Duck {

    public ModelDuck() {
        this.flyBehavior = new FlyNoWay();
    }

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

위와 같은 청둥 오리, 모형 오리를 구현했다.


public static void main(String[] args) {
    Duck mallardDuck = new MallardDuck();
    mallardDuck.swim();
    mallardDuck.display();
    mallardDuck.performFly();
    
    Duck modelDuck = new ModelDuck();
    mallardDuck.swim();
    modelDuck.display();
    modelDuck.performFly();
    modelDuck.setFlyBehavior(new FlyWithRocket());
    modelDuck.performFly();
}

실행 결과는 다음과 같다.

오리는 performFly()를 통해 FlyBehaviorfly()를 단순히 호출하기만 하므로 둘은 독립되었다고 말할 수 있다.

또한 modelDuck은 날 수 없었지만 새롭게 FlyWithRocket으로 지정해주어 로켓을 타고 날 수 있었다.
동적으로 행동을 지정해줄 수 있다는점 또한 전략 패턴의 특징이다.


마지막으로 정리하자면, 전략 패턴의 특징은 다음과 같다.

  • 전략 패턴은 알고리즘(전략)군을 정의하고 캡슐화한다.
  • 전략 패턴을 사용하면 클라이언트로부터 알고리즘을 분리하여 독립적으로 운용할 수 있다.
  • 동적으로 전략을 지정할 수 있다.

모든 소스코드는 여기에서 확인할 수 있다.



profile
내일은 개발왕 😎

0개의 댓글