Strategy Pattern 은 개인적인 생각으론 가장 기본이 되는 Pattern 으로, 많은 Pattern 들이 이 Strategy Pattern 으로 부터 파생되었다고도 한다. Strategy Pattern 은 어떻게 보면 무지성 Interface 사용하기에서 가장 기본적인 사용을 알 수 있도록 해주기도 한다.
우선 예제로 같이 살펴보자. 다음과 같은 UML을 구현해보려고 한다. Duck 이라는 최상위 계층이 있고, 다음과 같은 요구사항들을 만족하는 UML을 제시한다.
1. 모든 오리는 quack 할 수 있고, fly 할 수 있다
2. 모든 오리는 반드시 동일한 방식으로 swim 한다
3. 모든 오리는 display 방식을 스스로 구현해야 한다
Duck 이라는 최상위 계층이 있고, Duck 은 모두 quack(), swim(), fly() 를 기본적으로 할 수 있다. 또한, 자신만의 모습을 보여주기 위한 display() 함수를 abstract 함수로 구현해 자식 메소드들에게 구현을 위임하고 있다. 현 요구사항에 맞춰진 아주 적절한 디자인 패턴의 모습이다.
여러 종류의 Duck 이 추가될 수 있도록 구현된 일반적인 디자인이며, MalladDuck, RedheadDuck 같은 Class 들이 추가되어 각자 자신의 display 함수를 @Override 하고 있다. 그렇다면 변종과 같은 Duck 이 등장한다면 어떨까?
앞으로 변종들이 안 나올 것으로 판단된다면, 해당 구현체들에 대해서만 Do Nothing 으로 처리해주는 방법도 있다. 하지만 이런 새로운 변종 오리들이 무궁 무진하게 애플리케이션에 접근할 수 있다고 생각이 든다면, 쓸데 없는 implementation 을 하는 구현체들이 많아지기 대문에 올바른 디자인 패턴이 아니란걸 알 수 있다. (상황에 맞춰 Best 디자인은 계속 바뀌게 된다)
Behavior 자체를 인터페이스화 하는 것이 좋은 방향인걸 접한적이 있다면 Flyable, Quackable 이라는 인터페이스를 제안할 수도 있다. abstract 한 super-class 와 상관없이 속성을 부여할 수 있기 때문이다. 하지만 이 경우에는 또다른 문제를 야기한다.
지금은 Duck Super Class 가 어쨌든 필연적으로 있는게 나은 상황이기 때문이다. 대부분의 오리 종들은 fly() 와 quack() 에 동일한 구현을 부여할 것인데, 이 Behavior 을 Interface 로 두면서 대부분 오리들이 동일하게 사용할 구현을 지속적으로 해줘야 하기 때문이다.
그래도 이 디자인을 해봄으로써 정확하게 파악할 수 있는건, 현재 중요한 문제는 구현체가 추가될 때마다 변경될 가능성이 있는 것들 (fly, quack) 과, 변경되지 않아 하는 것들 (swim, display) 이 같은 Class 에 있다는 것이 문제라는걸 확인할 수 있다.
변경되지 않아야 하는 것들은 Super Class 를 통해 해결할 수 있다. 변경될 가능성이 있는 것들의 변경 속성들을 묶어서 이를 분리해보겠다 (Flying Behavior, Quacking Behavior).
Fly 와 Quack 이란 목적을 달성하기 위한 각자의 알고리즘들을 분리해주었고, 어떤 변종들이 오든 자신이 가지고 갈 FlyBehavior 와 QuackBehavior 을 선택해서 Run-Time 중 가져가거나 변경할 수 있다. 만약 기존에 없는 Behavior 라면 쉽게 구현체를 추가하고 Implementation 을 구현해주면 된다. Duck 이 할 일을 Interface 가 하도록 위임 (Delegation)하도록 하는 설계이다.
이쯤에서 코드를 한 번 살펴볼 필요가 있을 것 같다. 가장 기본적인 MallardDuck 을 기준으로 살펴보면 아래와 같다. 이와 같은 설계를 했을 때 중요한 포인트는 역할체들의 구현체들을 어디서 어떻게 결정해서 넣어줄건지이며, 아래와 같은 방식들이 있을 수 있다.
public class MallardDuck extends Duck implements FlyBehavior, QuackBehavior {
private FlyBehavior flyBehavior;
private QuackBehavior quackBehavior;
/*
생성시점에 Composition 방식으로 사용할 구현체들을 등록할 수 있다
> Client 가 직접 선택하는 방식
*/
public MallardDuck() {
this.flyBehavior = new FlyWithWings(); // Object Composition
this.quackBehavior = new QuackNormal();
}
/*
외부에서 Dependency 방식으로 구현체를 주입해주는 것을 Dependency Injection(DI) 이라고 부른다
> 이와 같은 방식을 하면 Client 는 어떤 Impl 을 사용하는지 아예 모를 수 있는 장점
*/
public MallardDuck(FlyBehavior flyBehavior, QuackBehavior quackBehavior) {
this.flyBehavior = flyBehavior;
this.quackBehavior = quackBehavior;
}
@Override
void display() {
System.out.println("Shows Mallard Duck");
}
/*
Interface 에게 해당 Behavior 의 수행을
각자 알고리즘대로 수행하도록 위임한다
*/
@Override
public void fly() { // 수행을 인터페이스 구현체에게 위임 (Delegation)
this.flyBehavior.fly();
}
@Override
public void quack() {
this.quackBehavior.quack();
}
/*
Setter 를 통해 애플리케이션 Run-Time 시에도
필요에따라 구현체를 바꿔줄 수 있다
*/
public void setFlyBehavior(FlyBehavior flyBehavior) {
this.flyBehavior = flyBehavior;
}
public void setQuackBehavior(QuackBehavior quackBehavior) {
this.quackBehavior = quackBehavior;
}
}
이와같이 설계함으로써 확장에 훨씬 열려있는 디자인 패턴을 가져갈 수 있었다. 또한, 만약 MallardDuck 이 다쳐서 못날게 되었다면 setter 를 통해서 FlyNoWay Class 구현체로 애플리케이션 Run-Time 시점에 바꿔줌으로써 더욱 dynamic 한 대응이 가능해졌다.
처음과 현재를 비교해본다면, 처음에는 Inheritance 관계만을 사용하였고, 지금은 Composition 을 통해서 Class 간 상호작용을 설계했다고 할 수 있다 (여기서 말하는 Composition 이란 Inheritance 를 제외한 4가지 Class 관계인 Dependency, Shared Aggregation, Composition, Assocaition 을 말한다).
전체적인 디자인 패턴들의 형성을 보면 위와 같은 Inheritance 보단 Interface, Delegation, Compoisition 을 통해서 구성해 나가는 디자인 패턴들이 훨씬 많은데, 이를 더 선호하라는 Design Principle 이 있기 때문이다. 다른 Class 에게 역할을 위임하는 Delegation 방식은 유지 보수에 있어서 강력한 무기가 되기 때문이다. 둘의 차이를 비교해보면 다음과 같다.
Inheritance (White-box reuse) | Composition (Black-box reuse) |
* Sub Class 의 Implementation 함수들이 모두 Parent Class 기반으로 만들어져서, 강한 Coupling 으로 형성된 관계이다 * Parent Class 의 모든 함수들이 Sub Class 로 내려오는 방식으로 재사용을 하게 되므로, 강한 Coupling 을 유지한채 재사용이 이루어진다 |
* Object 들 간의 기능 구현을 위해 서로 조합된 관계를 말한다 * Polymorphism 에 의해 Run-Time 시 동적으로 변경할 수 있다 * 재사용 관계보단 그 Object 에게 기능 수행을 요구하는 관계이다 (Delegation) |
+) Compile-Time 에 모든 일이 일어나고 재사용이 쉽다 -) Sub Class 는 Parent Class 에 의존성이 강해진다 -) Parent Class 에서 받은 Implementation 들을 Run-Time 시 변경할 수 없다 |
+) Run-Time 시 구현체를 바꿀 수 있다 (바꿀 일이 많지는 않다, 많다면 다른 패턴이 필요하다 -> State Pattern) +) Coupling 이 많이 떨어져서 유지보수에 유리하다 -) 관계를 이해하는 과정이 추가되므로 어려울 수 있고, 특히 구현체를 바꾸는 로직이 추가될 시 더 어려워진다 |
정리하며
전략 패턴은 다음과 같이 정리될 수 있다.
1. 구현체들의 알고리즘들을 Client 로부터 Encapsulate 한다
2. Client 들은 각 구현체들을 종합적으로 사용하며(Composition) 그들에게 Behavior 의 수행을 위임하게 된다
3. 장점이긴 하지만 구현체들을 동적으로 변경하는 것이 포인트라기보단, 2번과 같은 위임성과 의존성의 분리가 굉장히 큰 장점이라 할 수 있다
앞으로 더 알게 되겠지만 설계란 Fix 되어 있는 것과 Fix 되지 않은 것들을 잘 분리하는 것이 시작인 것 같다. 같은 의미로 언제든 요구사항이 변경될 수 있으니, 그 앱에 100% 완벽한 디자인이란 없다는 의미와도 같은 것 같다.
출처
1) 에릭 프리먼, 『Head First Design Patterns』, O'Reilly Media (2004)
2) 중앙대학교 소프트웨어학부 이찬근 교수님 디자인 패턴 수업자료 중
'SW 설계 > Design Pattern' 카테고리의 다른 글
[Design Pattern] Creational - 빌더 패턴 (Builder Pattern) (0) | 2023.10.21 |
---|---|
[Design Pattern] Creational - 팩토리 메소드 패턴 (Factory Method Pattern) (0) | 2023.10.20 |
[Design Pattern] Behavioral - 중재자 패턴 (Mediator Pattern) (1) | 2023.10.20 |
[Design Pattern] Behavior - 템플릿 메소드 패턴 (Template Method Pattern) (0) | 2023.10.19 |
[Design Pattern] Behavioral - 옵저버 패턴 (Observer Pattern) (0) | 2023.10.19 |