첫번째로 포스팅할 디자인 패턴은 스트래티지 (Strategy : 전략) 패턴이다.
소프트웨어 공학에서 가장 중요시되는 것들 중 하나가 '유지보수'이다. 절차적으로 짜여져 있는
엄청나게 긴 분량의 소스코드나, 복잡하게 꼬여있는 스파게티 코드는 유지보수하기에 최악의 코드라고 말한다.
하지만 객체지향 패러다임을 따르는 객체 지향적인 언어로 소프트웨어를 설계한다면, 앞서말한 하나의 소프트웨어를 구성하는 여러가지 기능들을 '모듈화' 즉 객체지향 4대요소중 하나인 '캡슐화' (encapsulation) 를 할수있다.
따라서 기능을 추가하거나, 변경을 할때 다른기능들에 영향을 주지않고 작업할수있는 설계가 가능하다.
이러한 객체지향적인 패러다임을 충실히 따르는 디자인패턴의 기본이 바로 스트래티지 패턴이라고 생각한다.
처음 포스팅이라 서두가 길었다. 앞으로 하는 포스팅엔 중요 개념만 정리할란다...
스트래티지 패턴은 아래와같이 정의할수 있다.
"알고리즘 군을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만든다.
스트래티지를 활용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할수 있다."
발단은 이렇다.
'SimUDuck' 이라는 오리 연못 시뮬레이션이 있다. 이 프로그램에는 매우 다양한 오리 종류가 존재한다.
처음 디자인한 사람들은, 표준적인 객체지향 기법을 이용하여 'Duck' 이라는 수퍼 클래스를 만들어놓고,
이 'Duck'을 상속하여, 여러가지 오리의 종류를 만들었다.
수퍼클래스에서는 꽥꽥거리는 기능의 quack메서드, 수영을 할수있는 swim 메서드, 날아다닐수있는 fly메서드가 있다.
아래의 MallardDuck 클래스와 RedheadDuck클래스는 위의 세가지 메서드를 그대로 상속받고, 생김새를 표현하는 display 추상 메서드를 오버라이딩 하여 각각의 모습을 표현한다.
하지만 여기에 RubberDuck (고무오리) 클래스를 추가하면 어떨까? 날수없는 고무오리에게도 fly 메서드가 상속되어 고무오리가 날아다니는 진풍경이 오리연못 시뮬레이션에 나타날수 있다.
Attempt 1.
RubberDuck 클래스는 그대로 Duck 클래스를 상속하지만, 날수없어야 하므로 fly 메서드를 아무 행동도 하지 않게 오버라이딩 한다. 간단하게 해결되는 듯 하다. 하지만 여기에 또다른 오리인 "DecoyDuck" (가짜오리)이 추가된다고 가정해보자. 이 가짜오리는 날수도없고 꽥꽥거리지도 못한다. 그렇다면 fly 메서드와 quack 메서드 두개를 또다시 오버라이딩해야한다. 만약 이 시뮬레이션의 기능 변경이 잦다면, 일일히 서브클래스를 뒤져야 하고 상황에따라 오버라이딩해야하며 이것은 상당이 비용이 많이 드는 작업이다.
Attempt 2.
결국엔 상속을 사용하여 전체 서브클래스가 상속을 받는것이 아닌 일부 서브클래스만 날거나 꽥꽥거릴수 있도록 하는방법을 찾아야한다.
인터페이스를 사용해보자.
딱 보아도 복잡해보인다. 일단은 Flyable, Quackable 인터페이스를 정의하여 필요한 오리클래스마다 implements 하여 구현할 수 있도록 하였다. 어쨌든 일부 오리만 날거나 꽥꽥거릴수 있게하는 소기의 목적은 달성했지만, 치명적인 단점이 존재한다.
바로 '코드의 중복'
인터페이스의 메서드는 추상메서드이므로, 그것을 구현하는 구상 클래스에서 일일히 fly, quack 메서드를 구현해야한다. 따라서 엄청난 코드의 중복을 초래하고, 날아다니는 동작을 조금 바꾸기위해 100여개의 구상클래스의 fly 메서드를 일일히 다 찾아 고쳐야 한다는것이다. 결국 인터페이스도 답은 아니다.
Solution .
'변하는 부분'을 생각해보자. fly와 quack은 오리의 클래스마다 달라지는 부분이다. 가짜오리는 날지 못하며, 고무오리는 '꽥'이 아니라 '삑' 소리를 낸다. 이러한 변하는 부분들의 알고리즘들을 '캡슐화' 한다면 다음과같은 그림이 완성된다.
일단의 행동들(날거나, 소리를 내는)을 인터페이스로 정의했다. 그리고 그 인터페이스를 구현하는 클래스들이 일련의 '알고리즘군'을 형성하였다. 이러한 인터페이스형식의 인스턴스 변수를 Duck클래스의 멤버로 추가한다. 그리고 각각의 오리 클래스들은 'Duck'클래스를 상속받는다. 중요한부분인데, Duck클래스에는 각각의 인스턴스 변수를 실행시에 동적으로 대입하기 위해서 setter 메서드를 선언하였다.
이제 이러한 디자인이 어떻게 활용되는지 보자.
일단 소리를 낼수는 있지만 날지못하는 'ModelDuck' 클래스를 정의한다.
public class ModelDuck extends Duck {
public ModelDuck(){
flyBehavior = new FlyNoWay();
quackBehavior = new Quack();
}
@Override
public void display() {
// TODO Auto-generated method stub
}
}
보는바와 같이 생성자에서 FlyBehavior 와 QuackBehavior 인스턴스를 날수없는 동작으로 구현된 FlyNoWay 클래스와, '꽥' 하는 소리를 내는 Quack 클래스의 인스턴로 할당한다.
이제 FlyBehavior의 새로운 구상클래스인인 로켓빠워로 날아가는 'FlyRocketPowered' 클래스를 정의하자.
public class FlyRocketPowered implements FlyBehavior {
@Override
public void fly() {
System.out.println("로켓 추진으로 날아 갑니다");
}
}
준비는 마쳤다. 이제 테스트를 위한 MiniDuckSimulator 클래스를 작성한다.
public class MiniDuckSimulator {
public static void main(String[] args) {
Duck model = new ModelDuck();
model.performQuack();
model.performFly();
System.out.println();
model.setFlyBehavior(new FlyRocketPowered());
model.performFly();
}
}
Duck 추상클래스로 선언된 변수에 ModelDuck 클래스의 인스턴스를 할당하고,
performQuack, performFly 메서드를 실행한다. 그리고 ModelDuck 인스턴스의 나는 동작을
setFlyBehavior 메서드를 이용하여 로켓추진으로 날아가는 FlyRocketPowered 클래스의 인스턴스로
갈아끼운후 다시 performFly 메서드를 실행한다. 결과는 다음과같다.
처음에 modelDuck의 Behavior 변수에는 날수없는 FlyNoWay클래스와, 꽥소리를 내는 Quack클래스의 인스턴스를 할당하였다. 그래서 처음 두 동작은 '꽥' 소리와 '저는 못날아요' 라는 동작이 실행된것이다. 하지만, setFlyBehavior 메서드를 이용해서 실행시에 동적으로 FlyRocketPowered 클래스의 인스턴스를 대입 하였다. 다시 실행한 performFly메서드는 로켓추진으로 날아가는 동작을 보여준다.
가만히보니..... 스프링을 공부할때 배웠던 의존성주입 (Dependency Injection) 개념,, 특히나 세터 인젝션(Setter Injection)과 매우 흡사하다. 그렇다. 스프링 프레임워크는 바로이 스트래티지 패턴을 기반으로 구성된 프레임워크인 것이다. (물론 스프링이 스트래티지 패턴으로만 이루어진것은 아니다.)
어쨌든, 이렇게 알고리즘'군'을 각각 캡슐화 하여 정의하고, 그것들을 사용하는 클라이언트 단에서 캡슐화된 알고리즘 즉, '기능'들을 유연하게 교체해가며 사용할수 있다는것이 스트래티지 패턴의 핵심이다. 이렇게되면 각각의 서브클래스나 슈퍼클래스를 전혀 건드릴 필요없이 캡슐화된 알고리즘 클래스만을 추가하거나, 수정한다면 side effect의 risk 없이 코드를 유지보수할수 있게 될 것이다.