본문 바로가기
Architecture & Engineering/Design Pattern

[GoF Design Pattern] 1. 전략(Strategy) 패턴

by 신숭이 2023. 5. 14.

[GoF Design Pattern] 1. 전략(Strategy) 패턴

 

 

흔히 디자인 패턴이라고 말하면, GoF(Gang of Four)라 불리는 소프트웨어 공학자 4명이 구체화한 23개의 디자인 패턴을 말한다. 디자인 패턴은 모듈 단위가 아닌 클래스 레벨에서 설계를 하는 기교이며 다음과 같이 목적(클래스, 객체), 범위(생성, 구조, 행위)에 따라 구분하기도 한다.

https://4z7l.github.io/2020/12/25/design_pattern_GoF.html

  • 생성 패턴(Creational Pattern) : 객체를 생성하는 방법에 대한 패턴들이다. 생성 로직을 숨기면서, 사용 목적에 따라 어떻게 객체를 생성할 지에 대한 유연성을 제공한다. 특히 객체의 생성과 조합을 캡슐화하여 객체의 생성 또는 변경과정에서 프로그램 구조가 영향받지 않도록한다. 
    ex) 팩토리 메서드(Factory Method) 패턴, 추상 팩토리(Abstract Factory) 패턴, 싱글톤(Singleton) 패턴 등

  • 구조 패턴(Structural Pattern) : 클래스와 객체를 구성하는 방법에 대한 패턴들이다. 상속과 같은 컨셉을 이용해 인터페이스를 구성하고 객체가 새로운 기능을 가지는 방법에 대해 정의한다. 
    주로 조금 더 큰 구조를 만들 때 쓰인다. 서로 다른 인터페이스를 가진 두 클래스를 묶어 하나의 단일 인터페이스를 만들거나 여러 객체를 묶어 새로운 기능을 제공하기도 한다.
    ex) 어댑터(Adapter) 패턴, 데코레이터(Decorator) 패턴, 퍼사드(Facade) 패턴 등

  • 행위 패턴(Behavioral Pattern) : 객체 간 커뮤니케이션에 관한 패턴들이다. 객체나 클래스 사이의 알고리즘이나 책임의 분배해 관한 패턴들로, 하나의 객체가 수행할 수 없는 작업을 여러 객체들에게 분배하는 방법이다. 이렇게 분배하더라도 객체 간 결합도는 최소화한다.
    ex) 옵저버(Observer) 패턴, 스트래티지(Strategy) 패턴, 템플릿 메서드(Templete Method) 패턴 등

참조 : https://www.gofpatterns.com/design-patterns/module2/behavioral-creational-structural.php

 

Behavioral, Creational, Structural Patterns[Definition]

Behavioral, Creational, Structural Types of Design Patterns Based on the official Design Pattern Book, "Design Patterns: Elements of Reusable Object-Oriented Software", there are 23 design patterns. These patterns can be grouped into three categories: Crea

www.gofpatterns.com

 

전략 패턴의 정의

 

[헤드퍼스트 디자인패턴] 책에 써있는 내용을 그대로 적으면
"전략 패턴(Strategy Pattern)은 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 해 준다. 전략 패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있다."

라고 하는데, 처음 봤을 때는 당최 무슨 소린지 모르겠더라. 하나하나 해석해보면, 알고리즘은 일종의 행위를 말한다. 

즉 "오리"라는 객체는 날 수도 있고 (나는 행위), 헤엄칠 수도 있다. (헤엄치는 행위) 이런 행위들을 캡슐화하여 마치 대입하는 방식으로 특정 행위를 오리의 행동 변수에 대입하면 해당 행위로 작동하도록 만드는 것이다. 이때 클라이언트(호출자)는 해당 행위를 대입만 할 뿐 어떻게 구현되는지 모르고(독립적) 변경할 수 있다. (변경할 수 있다 -> 다른 행위로 바꿔 대입할 수 있다)

더 짧게 말하면 호출자는 객체가 어떤 알고리즘을 쓸 지 결정한다.

 

 

UML

 

Strategy는 문제 해결을 위한 전략이다. 인터페이스로 구성하여 여러 가지 전략을 정의할 수 있도록 한다. Context는 문제 해결에 대한 전략을 Strategy 인터페이스에게 위임한다. Client가 context.doAction를 요청할 시, 위임된 전략의 doStrategy 가 자동으로 지정되도록 구성한다. 

https://darrenfinch.com/strategy-design-pattern-behavioral-design-patterns/

 

[헤드퍼스트 디자인패턴]에서는 오리(context)의 행동을 디자인하는 예시를 들었다. 전략 패턴을 구성하는 사고흐름은 아래와 같은 디자인 원칙들을 따져가며 진행해 나간다. 

 

 

Design Priciple 1. 애플리케이션에서 달라지는 부분을 찾아내고 달라지지 않는 부분과 분리한다.

 

"달라지는 부분"이란 무엇일까? 이 세상에 [오리]라는 것이 단 하나 존재한다고 하자. 행동도 모두 똑같다고 가정하자. 그럼 아주 간단하게 다음과 같은 코드로 오리의 디자인은 끝날 것이다. 

class Duck {
    void display(); // 오리의 모습을 보여준다

    void performQuack() {
        System.out.println("quack !!"); // 꽥꽥거리기
    }

    void performFly() {
        System.out.println("fly !!"); // 날기
    }
}

하지만 실세계는 그렇게 호락호락하지 않다. [오리]의 종은 상당히 다양하다. 그리고 생물오리뿐이 아니라 오리 장난감도 있을 것이다. 이건 왜 끼워 넣냐고 할 수 있는데, 그런 요구사항을 내미는 고객사 또는 사장님한테 여쭤봐야 할 것이다.

위 코드는 다양한 오리를 표현할 수 있는가? 당연히 없다. 다양한 오리를 새로 쉽게 추가하는 방법은 기본 객체지향 디자인을 따른다. 오리를 추상화하여 다양한 오리를 쉽게 추가할 수 있도록 한다. 

abstract class Duck {
    abstract void display();

    void performQuack() {
        System.out.println("quack !!");
    }

    void performFly() {
        System.out.println("fly !!");
    }
}

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

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

class RubberDuck extends Duck {
    @Override
    void display() {
        System.out.println("This is Rubber Duck!");
    }
}

다양한 오리들이 되었다. 위 코드에서 오리들은 같은 방식으로 날고 꽥꽥거린다. 이제 각각 오리에 개성을 부여해 performFly와 performQuack을 구현하면 된다. 

하지만 오리의 종류가 1000종류(극단적으로 예시를 들어봤다)가 넘는다면, 이중 297종은 A방식으로 날고, 302종은 B방식으로 꽥꽥거리리는 등 같은 행동을 보이는 오리들도 있을 것이다. 이중 A방식으로 나는 행동을 모두 복사 붙여 넣기로 함수를 짜놓았다고 하자. 그런데 갑자기 A방식의 날기 방법이, 수정되야 한다는 요구사항이 발생하면 우리는 일일이 297개의 구현된 함수를 찾아나가며 수정해야 한다. 
(뭐 날갯짓을 20회 한다는 걸 25회하는 걸로 바뀐다거나...)

이러한 수정은 대단히 시간을 낭비하게 만들고, 개발자의 실수를 유발하기 매우 좋다. A방식의 날기는 수정된 코드로 295개는 잘 복사 붙여 넣기 했는데, 2개를 잘못했다고 하자. 이런 게 보통 "버그"다. 

여기서 [디자인 패턴]의 필요성이, 그것도 [전략 패턴 StratgyPattern] 의 필요성이 대두된다. 

필요성을 느끼지 못하고 정의만 보는 것은 그 의미가 쉽게 체득이 안되었다. 그래서 이런 극단적인 예시를 들어봤다.

분명 "달라져야하는 부분"은 오리의 [행동]이었는데, 행동을 수정하려다 보니, 오리에 오류가 발생한 것이다. 객체지향 디자인과 패턴은 보통 이러한 개발자의 실수를 줄이는데, 개발자가 시간을 더 효율적으로 쓰게 하는데 주안점을 두고 있다.

위 코드를 전략 패턴으로의 첫번째 리팩토링은 달라지는 부분을 분리하는 것이다. 달라지는 부분은 [날기 행동(FlyBehavior)]과 [꽥꽥거리기 행동(QuackBehavior)] 이였다. 우리는 이 부분을 분리해야 한다. 전략 패턴에서 전략은 오리의 행동이라고 할 수 있겠다.

 

 

Design Priciple 2. 구현보다는 인터페이스에 맞춰서 프로그래밍한다.

 

행동을 따로 분리한다고 하자. 여러 행동들을 오리들이 공유하고 때로는 오리가 자기 습성을 바꾸어 다른 날기 방법을 쓸 수도 있다. 즉 행동이 동적으로 바뀔 수 있으면 좋겠다. 낮에는 A방식으로 날다가 밤에는 B방식으로 날 수도 있을 것이다.

따라서 이왕이면 분리한 행동은 인터페이스로 추상화하자. 그러면 런타임에 전략(행동)을 동적으로 바꾸기 쉽고, 새로운 전략(행동)을 추가하기도 쉽다.

public interface FlyBehavior {
	public void fly();
}

public class FlyWithWings implements FlyBehavior {
	public void fly() {
		System.out.println("I'm flying!!");
	}
}

public class FlyRocketPowered implements FlyBehavior {
	public void fly() {
		System.out.println("I'm flying with a rocket");
	}
}

 

 

Design Priciple 3. 상속보다는 구성(Composition)을 활용한다.

 

상속(A는 B이다) 보다는 구성(A에는 B가 있다)를 활용하자. 전략 패턴에서는 구성을 활용하여, 행동(전략)들을 위임한다. 구성(Composition)은 분명 장단점이 있다. 다만 여기서는 구성이 시스템의 유연성을 상승시킨다. 이는 두 클래스를 합치는 여러 방법 중에 하나이다. 

인터페이스로 추상화한 각 행동(FlyBehavior, QuackBehavior)을 구성으로 가지고 있다. 최종적으로 완성된 오리의 디자인은 다음과 같다.

public abstract class Duck {
	FlyBehavior flyBehavior;
	QuackBehavior quackBehavior;

	public Duck() {
	}

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

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

	abstract void display();

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

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

	public void swim() {
		System.out.println("All ducks float, even decoys!");
	}
}

 

오리는 다음과 같이 생성자에서 행동을 부여할 수 있다. 더 유연하게는 생성자에서 행동을 파라미터로 런타임에 전달받아서 부여할 수도 있다.

public class MallardDuck extends Duck {

	public MallardDuck() {

		quackBehavior = new Quack();
		flyBehavior = new FlyWithWings();

	}

	public void display() {
		System.out.println("I'm a real Mallard duck");
	}
}

 

만약 FlyWithWigns()의 코드가 수정되더라도 오리의 코드에는 전혀 영향을 미치지 않는다.

즉 수정의 영향을 최소화하는 것. 이것이 객체지향을 디자인을 공부해야하는 이유이다.

 

최종적으로 비즈니스 로직을 보면 다음과 같다.

public class MiniDuckSimulator1 {
 
	public static void main(String[] args) {
 
		Duck mallard = new MallardDuck();
		mallard.performQuack();
		mallard.performFly();
   
		Duck model = new ModelDuck();
		model.performFly();
		model.setFlyBehavior(new FlyRocketPowered());
		model.performFly();

	}
}

 

 

이제 전략 패턴의 정의를 다시 읽어보면 조금 더 이해가 잘 될 것이다.

"전략 패턴(Strategy Pattern)은 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 해 준다. 전략 패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있다."

 

댓글