본 포스트는 헤드퍼스트 디자인패턴을 읽고 정리한 글입니다. |
Ch1 디자인 패턴 소개와 전략 패턴
전략 패턴 (Strategy Pattern)
알고리즘군을 정의하고 캡슐화하여, 실행 중에 알고리즘을 교체할 수 있게 하는 디자인 패턴이다.
변경 가능성이 있는 부분을 분리하여 인터페이스를 이용하여 캡슐화하고, 실행 중에 구현체를 선택하여 사용할 수 있도록 한다.
Question
다양한 오리(Duck)을 만들고 싶다.
모든 오리는 각자 다른 모양(display)을 가지지만, 모두 꽥꽥 소리(quack)를 낼 수 있으며, 날(fly) 수 있다.
어떻게 구현할 수 있을까?
간단하게 생각해 보자면
Duck이라는 슈퍼 클래스를 두고, 이를 다양한 종류의 오리가 상속받도록 구현할 수 있을 것이다.
Problem
하지만, 이러한 구현에는 단점이 존재한다.
만약에 훨씬 더 많은 종류의 오리를 만든다고 할 때, 같은 행위에 대하여 요구사항이 같거나 다를 수 있다.
ex. 어떤 오리는 꽥꽥하고 울지만, 또 다른 오리는 울지 않을 수도 있다. 어떤 오리는 자신의 날개로 날 수 있지만, 다른 오리는 날지 못 할 수 있으며, 어떤 오리는 그 외의 힘으로 날 수도 있다. |
만약 요구사항이 같다면 코드의 중복이 늘어날 것이고, 요구사항이 다르다면 모든 하위 클래스를 살펴보아야 한다.
이는 굉장히 비효율적이며, SOLID 원칙 중 OCP 원칙을 위배한다.
💡 개방-폐쇄 원칙 (Open/Closed Principle)
소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
앞으로 세 가지 디자인 원칙을 통하여 문제를 해결해 보자.
디자인 원칙
첫 번째, 캡슐화
애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리한다.
달라지는 부분을 찾아서, 나머지 부분에 영향을 주지 않도록 ‘캡슐화’한다.
그렇게 하면 서로 다른 형식의 객체에서 사용할 수 있다는 점에서 ‘재사용성’이 증가하며, 나중에 바뀌지 않는 부분에는 영향을 미치지 않고 그 부분만 고치거나 '확장'할 수 있다.
모든 디자인 패턴의 기반을 이루는 원칙이다.
why?
‘시스템의 일부분을 다른 부분과 독립적으로 변화시킬 수 있는’ 방법을 제공하기 때문이다.
Solution 1
슈퍼 클래스인 Duck에서 오리 객체마다 다를 수 있는 행동 요소인 fly(나는 행동)와 quack(꽥꽥 우는 행동)를 분리하고, 캡슐화 하자.
그렇다면 캡슐화는 어떻게 할 수 있을까?
이는 두 번째 디자인 원칙을 통해 알 수 있다.
두 번째, 다형성
구현보다는 인터페이스에 맞춰서 프로그래밍한다.
실제 실행 시에 쓰이는 객체가 코드에 고정되지 않도록 상위 형식(supertype)에 맞춰서 프로그래밍하여 ‘다형성’을 활용한다.
- 변수를 선언할 때, 추상 클래스 혹은 인터페이스 같은 상위 형식으로 선언한다.
- 객체를 변수에 대입할 때 상위 형식을 구체적으로 구현한 형식이라면 어떤 객체든 넣을 수 있다.
- 변수를 선언하는 클래스에서 실제 객체의 형식을 몰라도 된다.
Solution 2
분리한 두 행동은 인터페이스를 이용하여 캡슐화 할 수 있다.
알고리즘군: FlyBehavior, QuackBehavior 알고리즘: FlyNoWay, FlyRocketPowered, FlyWithWings / MuteQuack, Quack, Squeak |
이렇게 분리를 마치고 나면 두 행동에 대하여 변수를 선언할 때,
FlyNoWay flyBehavior = new FlyNoWay();
위 코드와 같이 실제 객체의 형식으로 선언하는 것이 아닌
아래와 같이 상위(추상 클래스 또는 인터페이스) 형식으로 선언하는 것이 가능하다.
// 나는 행위
FlyBehavior flyBehavior = new FlyNoWay();
// or FlyBehavior flyBehavior = new FlyWithWings();
// 우는 행위
QuackBehavior quackBehavior = new Squeak();
// or QuackBehavior quackBehavior = new Quack();
이렇게 하면 서로 다른 형식의 오리 객체에서 나는 행동과 우는 행동을 ‘재사용’할 수 있다.
또한, 기존의 행동 클래스를 수정하거나 Duck 클래스를 건드리지 않고 새로운 행동을 추가할 수 있다.
그렇다면 Duck 클래스에서 두 행동을 어떻게 사용할 수 있을까?
세 번째, 구성
상속보다는 구성(composition)을 활용한다. = “A에는 B가 있다.”
구성으로 사용하는 객체를 확장하면(올바른 인터페이스를 구현하면),
실행 중에 동적으로 설정할 수 있게 되어 시스템의 유연성이 크게 향상된다.
→ 새로운 코드를 만들어 기능을 추가할 수 있다.
Solution 3
모든 오리(Duck)는 FlyBehavior과 QuackBehavior 인스턴스를 가진다.
이는 오리의 두 행동을 위임하는 것이며, 상속보다 ‘구성’을 사용한다는 원칙을 지킬 수 있게 된다.
코드로는 다음과 같이 나타낼 수 있다.
public abstract class Duck {
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
public Duck(){}
public void performFly() {
flyBehavior.fly(); // 행동 클래스에 위임
}
public void performQuack( ){
quackBehavior.quack(); // 행동 클래스에 위임
}
}
이렇게 하면 아래 다이어그램과 같이 오리(Duck) 객체는 구성을 이용하여 행동을 부여 받을 수 있다.
전략 패턴 again
전략 패턴이란 알고리즘군을 정의하고 캡슐화하여, 실행 중에 알고리즘을 교체할 수 있게 하는 디자인 패턴이다.
지금까지의 과정을 통하여 알고리즘군을 정의하고 캡슐화하는 과정은 확인할 수 있었다.
그렇다면 알고리즘은 어떻게 교체할 수 있을까?
Solution 4
바로, setter method를 두는 것이다.
public void setFlyBehavior(FlyBehavior flyBehavior) {
this.flyBehavior = flyBehavior;
}
public void setQuackBehavior(QuackBehavior quackBehavior) {
this.quackBehavior = quackBehavior;
}
Duck 클래스에서 FlyBehavior과 QuackBehavior 인스턴스를 가지므로, 각각에 대하여 setter를 추가해주면 실행 중에 알고리즘을 교체할 수 있다.
Main Code 예시
import duck.Duck;
import duck.MallardDuck;
import duck.ModelDuck;
import flyBehavior.FlyRocketPowered;
public class DuckSimulator {
public static void main(String[] args) {
// MallardDuck
Duck mallard = new MallardDuck();
mallard.performFly();
mallard.performQuack();
// ModelDuck
Duck model = new ModelDuck();
model.performQuack();
model.performFly();
// 나는 행동 알고리즘 교체
model.setFlyBehavior(new FlyRocketPowered());
model.performFly();
}
}
Problem Solve
위 3가지 디자인 원칙을 적용한 클래스 다이어그램은 다음과 같다
정리
전략 패턴이란?
알고리즘군을 정의하고 캡슐화하여, 실행 중에 알고리즘을 교체할 수 있게 하는 디자인 패턴이다.
=
변경 가능성이 있는 부분을 분리하여 인터페이스를 이용하여 캡슐화하고, 실행 중에 구현체를 선택하여 사용할 수 있도록 한다.
장점
기존 코드의 변경 없이 새로운 전략을 추가 및 수정할 수 있어 매우 유연하다.
→ 문제가 되었던 OCP 원칙 배반을 해결할 수 있다.
단점
코드의 복잡도가 증가하며, 모든 전략 간의 차이를 알고 있어야 한다.
알고리즘이 단순하고 다양하지 않다면, 코드만 복잡해질 수 있다.
GitHub
전체 실습 코드는 아래 레포지터리의 ch1_StrategyPattern 폴더에서 확인할 수 있습니다.