BackEnd/Design Patterens

[Design Patterns] Strategy Pattern : 스트래티지 패턴

샤아이인 2022. 1. 12.

Head First Design Patterns 책을 읽으며 정리한 내용 입니다. 문제가 될시 글을 내리도록 하겠습니다!

 

 

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

 

이말이 무슨 의미인지 다음 단락부터 설명해 보겠습니다!


디자인 패턴 소개

 

다음과 같이 오리를 표현하는 class가 하나 있다고 해봅시다.

(ps. 간단하게 만 작성한 class 입니다. 실제 코드가 아닙니다!!)

Duck{
    quack() { ... }
    swim() { ... }
    abstract display();
}
 

이제 이러한 Duck 을 상속 받는 여러 파생의 오리들을 만들수 있게 되었다!!

MallardDuck extends Duck{
    display() {
      ...
    }
}

RedheadDuck extends Duck{
    display() {
      ...
    }
}
 

 

문제는 오리들이 날아다닐 수 있도록 하고싶다는 점 입니다!!

단순하게 생각하면 그냥 Duck class에 fly()메소드를 추가해주면 끝날 것 같습니다(?). 다음과 같이 말이죠

Duck{
    quack() { ... }
    swim() { ... }
    abstract display();
    fly(); // 나는 기능을 추가해줌. 모든 child class 들이 상속받게됨
}
 

이로 인하여 Duck 을 상속받은 모든 서브클래스 가 날수있게 되어버렸습니다.

문제는 다음과 같이 "고무"오리 또한 날아다니는 상황이 되버린 것 입니다.

Duck{
    quack() { ... }
    swim() { ... }
    abstract display();
    fly(); // 나는 기능을 추가해줌. 모든 child class 들이 상속받게됨
}

고무Duck extends Duck{
    quack() {
        고무 오리는 고무소리가 나도록 오버라이딩 해줌!
    }
}
 

모든 서브 클래스가 날아다니거나, 꽥꽥 거리는 기능이 있어야 하는 것 이 아니므로 상속을 사용하는 것 이 올바른 해결책이 아닌것 같습니다.

 

그럼 인터페이스를 추가하여 이를 구현하도록 하면? 해결이 될까요? (개인적으로 저는 이러면 끝나는 줄....) 다음과 같이 말이죠!

Duck{
    swim() { ... }
    abstract display();
}

interface Flyable {
    fly();
}

interface Quackable {
    quack();
}

MallardDuck extends Duck implements Flyable, Quackable {
    display()
    fly()
    quack()
}

RedheadDuck extends Duck implements Flyable, Quackable {
    display() 
    fly()
    quack()
}

고무Duck extends Duck implements Flyable, Quackable {
    display() 
    quack()    
}
 

문제는 자바의 interface에는 구현된 코드가 존재하지 않기 때문에 코드를 재사용할수 가 없습니다.

해당 interface를 구현하는 class들 마다 모두 직접 구현하여 사용해야 하는 불편함이 존재합니다.

따라서 interface를 일부 변경하면 이를 구현한 모든 class를 수정해줘야 하는 불상사가 생기게 됩니다.

 

 

이때 필요한 디자인의 원칙이 있습니다!

어플리케이션에서 달라지는 부분을 찾아
달라지지 않는 나머지로부터 분리시킨다.

바뀌는 부분만 따로 캡슐화해두면, 바뀌지 않는 나머지 부분에 영향을 미치지 않은채로 그 부분만 고치거나 확장할 수 있게됩니다.

 

fly() 와 quack()은 Duck class에서 오리마다 달라지는 부분입니다. 따라서 이 부분을 끄집어내서 (Duck class와는 완전 별개의) 각각의 집합을 만들어야 합니다.

 

fly 집합에는 나는 다양한 방식의 클래스 들이 구현되어 있고, quack 집합에는 소리내는 다양한 방식의 클래스 들이 구현되어 있습니다.

예를 들어 quack 집합에는 꽥꽥 소리내는 클래스삑삑 소리내는 클래스무소음 클래스 가 있을 수 있습니다.

 

이들 두 클래스 집합은 어떻게 디자인해야 할까요?

 

 

다음과 같이 두번째 디자인 원칙을 살펴봅시다.

구현이 아닌 인터페이스 에 맞춰 프로그래밍 하자.

 

각 행동은 interface로 표현하고, 이러한 행동을 구현할때 interface를 구현하도록 하자.

 

여기서 말하는 interface는 java문법의 인터페이스를 의미하는 것 이 아니라, 상위형식(supertype)에 맞춰 프로그래밍 하라는 의미 입니다.

상위 형식에 맞춰 작성하라는 말은 변수를 선언할때 추상클래스나 인터페이스 같은 상위 형식으로 선언해야 이를 구체적으로 구현한 어떤 객체도 변수에 집어넣을 수 있기 때문입니다. 한마디로 다형성을 활용하자는 의미이죠!

 

따라서 더이상 날거나, 소리내는 행동은 Duck class 내부에 구현할필요가 없습니다. 대신 이러한 특정 행동만을 목적으로 하는 클래스 집합을 만들어야 겠습니다.

 

Duck의 행동은 (특정 행동 인터페이스, 예를 들어 FlyBehavior, QuackBehavior 를 구현한) 별도의 클래스 들 입니다.

따라서 Duck class에서는 행동을 구체적으로 구현하는 방식에 대하여 알필요가 없게됩니다!

 

다음은 이러한 인터페이스와 이를 구현한 클래스 들 입니다.

 
FlyBehavior 라는 인터페이스와 QuackBehavior를 구현한 클래를 확인할 수 있습니다.

따라서 날 수 있는 클래스에서는 앞으로 무조건 FlyBehavior 인터페이스를 구현해야 합니다. 또한 날수 있는 클래스를 새로 만들때는 무조건 fly()를 구현해줘야 합니다.

 

이런식으로 디자인 해 두면 나는 행동과 소리내는 행동을 다른 객체에서 재사용할 수 있게됩니다.

또한 기존의 Duck class를 전혀 건드리지 않고도 새로운 행동을 추가해줄 수 있게되었습니다!


이제부터 구체적인 코드로 어떻게 사용하는지 확인해 봅시다!

1) 우선 Duck class에 2개의 인터페이스 형식의 레퍼런스 변수를 선언해 둡니다.

 

각 오리 객체에서는 실행시 이 변수에 특정 행동을 구현한 클래스를 전달함으로써 다형적으로 사용할 수 있게됩니다.

class Duck{
    FlyBehavior flyBehavior;
    QuackBehavior quackBehavior;

    public void performQuack(){
        quackBehavior.quack(); // quackBehavior로 참조되는 객체에 그 행동을 위임함
    }

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

    public void swim(){}
    public void display(){}
}
 

레퍼런스 변수에는 실행시 특정 행동에 대한 레퍼런스가 저장됩니다.

이후 performQuack() 에서는 꽥꽥 거리는 행동을 직접 처리하는 대신, quackBehavior로 참조되는 객체에 행동을 위임합니다!

 

2) FlyBehavior 와 QuackBehavior 인스턴스 변수를 설정해 보자!

public class MallardDuck extends Duck{
    public MallardDuck(){
        quackBehavior = new Quack();
        flyBehavior = new FlyWithWings();
    }

    public void display(){
        System.out.println("물오리 입니다!");
    }
}
 

mallardDuck에서 소리를 처리할때 Quack class의 인스턴스를 사용하기 때문에 performQuack()를 호출시 꽥꽥거리는 행동은 Quack 객체에게 위임 됩니다.

 

또한 quackBehavior는 레퍼런스 변수이기 때문에 실행시에 동적으로 QuackBehavior을 구현한 다른 클래스를 할당할 수 있습니다.

 

3) 동적으로 행동을 지정해 봅시다!

오리의 행동을 생성자에서 인스턴스 만들어 레퍼런스 변수로 받아 사용하는 것 이 아닌!, Duck의 서브클래스 에서 setter를 사용하여 동적으로 행동을 변경해줄 수 있습니다!

 

우선 기존의 Duck class에 나는 행동과 소리내는 행동에 대한 setter를 새로 추가해 줍니다.

class Duck{
    FlyBehavior flyBehavior;
    QuackBehavior quackBehavior;

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

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

    public void setFlyBehavior(FlyBehavior fb){ // 추가!!
        flyBehavior = fb;
    }

    public void setQuackBehavior(QuackBehavior qb){ // 추가!!
        quackBehavior = qb;
    }

    public void swim(){}
    public void display(){}
}
 

이제 이를 활용하여 나는 중간 동적으로 변화시켜 날지 못하도록 변경해 봅시다!

public class Main {
    public static void main(String[] args) {
        Duck mallard = new MallardDuck();
        mallard.performQuack();
        mallard.performFly();
        mallard.setFlyBehavior(new FlyNoWay());
        mallard.performFly();
    }
}
 

실행결과는 다음과 같습니다!

 

Fly로 날고있던 오리가 중간에 날수 없게 되어버렸습니다.


 

클래스 사이의 관계도를 확인해 보고 이들 사이에 어떤 관계가 있는지 생각해 봅시다.

 

"A는 B이다" 보다는 "A에는 B가 있다" 가 더 좋을 수 있습니다!

 

각 오리에는 FlyBehavior와 QuackBehavior이 있으며, 각각 행동과 꽥꽥거리는 행동을 위임받게 됩니다.

이렇게 두 클래스를 합치는 방식을 구성(composition)이라 말합니다.

 

오리클래스에서 행동을 직접적으로 구현하고 이를 서브 클래스들에게 상속해주는 대신,

올바르게 구현된 행동(알고리즘군)을 객체로 전달함으로써 동작할 수 있게됩니다.

상속보다는 구성을 활용하자!

구성 요소로 사용하는 객체에서 올바른 행동 인터페이스를 구현하기만 한다면 실행시간에 동적으로 행동을 변화시켜 줄 수 있습니다!

 

 

 

댓글