배경

Duck 클래스를 상속한 여러 오리 클래스가 존재합니다. (MallardDuck, RedHeadDuck …) Duck 클래스에는 quack(), display() 가 사전에 선언되어 있습니다.

변경 요청 발생

🙋‍♂️ “오리가 날아야 해요!”

결정: 모든 오리들은 Duck을 상속하고 있으니 Duck 클래스에 fly()를 추가하자!

문제점:

  • 해당 기능이 필요 없는 하위 오리도 날게 됩니다.
    • 아무것도 안 하도록 오버라이딩 하지 않으면 부모의 fly() 가 실행될 수 있습니다.
  • fly() 를 오버라이딩 하기에는 추후에 나무 오리처럼 quack() 도 아무것도 안 하도록 오버라이딩해야 합니다.
    • 추가할 때마다 불필요한 오버라이딩 반복 작업이 필요합니다. (심지어 아무것도 하지 않도록 하는)
    • 즉, 확장에 용이하지 않습니다.

결정: 그렇다면 날 수 있는(Flyable), 꽥꽥 거릴 수 있는(Quackable)으로 추상화해서(interface) 필요한 클래스만 구현하도록 하자!

문제점:

  • 인터페이스는 구현이 없기 때문에 어떻게 나는지, 어떻게 꽥꽥 거리는지를 모든 오리 종류에 해당하는 클래스에 구현해야 합니다.
  • 따라서 코드 재사용이 어렵습니다.

소프트웨어 개발 불변의 진리

아무리 설계를 잘한 애플리케이션이라도 시간이 지남에 따라 변화하고 성장해야 합니다. 변화하고 성장하지 않으면 그 애플리케이션은 사장될 것입니다.

따라서 항상 변경될 것을 염두에 두고 소프트웨어를 설계해야 합니다.

문제를 명확하게 파악하기

디자인 원칙 애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리합니다.

변경되는 부분은 따로 추출해서 캡슐화합니다. 이렇게 하면 나중에 이 부분만 수정하거나 교체하는 것이 가능해집니다.

오리 재설계

그렇다면 예제에서 변하는 것은 무엇이고 변하지 않는 것은 무엇일까요?

변하지 않는(혹은 않아도 되는) 것

  • Duck 클래스 자체는 잘 작동하고 있습니다.
    • 오리라고 정의할 수 있는 속성들은 변경될 확률이 적습니다.
    • 프로덕션 상으로 보여져야 하므로 ‘보여지다’ 라는 동작(display())도 변할 가능성은 적어 보입니다.

변하는 것

  • fly() : 고무오리는 날지 못합니다.
  • quack(): 나무 오리는 꽥꽥 거리지 못합니다.

따라서 변하는 나는 것과 우는 것을 떼어내서 캡슐화해야 합니다. 그리고 런타임에도 변경이 가능하도록 유연해야 합니다.

디자인 원칙 구현보다 인터페이스에 맞춰서 프로그래밍합니다.

구현보다 인터페이스에 맞춰서 프로그래밍해야 다형성을 이용해서 손쉽게 구현체만 교체 혹은 수정이 가능합니다.

나는 행동 추상화

“날다” 라는 것은 사실 오리에만 국한되는 개념이 아닙니다. 따라서 행위로써 추상화할 수 있을 것 같습니다.

public interface FlyBehavior{
    void fly();
}

꽥꽥 행동 추상화

"꽥꽥"은 사실 오리 말고 떠오르는 게 별로 없긴 하지만 행위의 관점에서는 추상화시킬 수 있습니다. MakeSound 같은 이름으로 하면 다른 동물들에도 쉽게 확장할 수 있지 않을까요?

public interface QuackBehavior {
    void quack();
}

어떻게 나는지, 어떤 소리를 내면서 꽥꽥 거릴지는 인터페이스를 구현한 클래스에서 구체화해서 사용합니다.

오리와 어떻게 연결할 것인가?(오리 행동 통합하기)

추상화가 끝났으면 이제 오리에 어떻게 연결할 것인가를 고민해야 합니다. 목표는 두 가지입니다.

  • 유연해야 합니다.
    • ‘구성(Composition)’ 을 사용해서 다른 클래스로 위임합니다.
      • 서브 클래스에 강제하지 않습니다.
    • 위임할 때, 인터페이스에 위임합니다. -> 다형성 사용
  • 런타임에 수정이 가능해야 합니다.
    • setter 같은 메서드로 교체가 가능해야 합니다.
public abstract class Duck {

    // 유연하도록 다형성을 사용하기 위해 인터페이스 타입을 참조합니다.
    // 이렇게 필드로 참조하는 것을 '구성(Composition)을 이용한다' 라고 합니다.
    QuackBehavior quackBehavior;
    FlyBehavior flyBehavior;

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

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

    // 런타임에 변경이 가능하도록 setter 메서드를 제공합니다.
    public void setFlyBehavior(FlyBehavior flyBehavior) {
        this.flyBehavior = flyBehavior;
    }

    public void setQuackBehavior(QuackBehavior quackBehavior) {
        this.quackBehavior = quackBehavior;
    }

    public void display() {
        System.out.println(this.getClass().getSimpleName() + " displayed!");
    }

    public void swim() {
        System.out.println(this.getClass().getSimpleName() + " swimming!");
    }
}

오리 추가

책에서 나온 나무 오리를 추가해 보겠습니다.

public class WoodDuck extends Duck {

    public WoodDuck() {
        quackBehavior = new MuteQuack(); // 꽥꽥 못함
        flyBehavior = new FlyNoWay();   // 날지 못함
    }
}

인터페이스에 맞춰서 프로그래밍하라면서 왜 구현체를 생성하나요? => DI 를 지원하는 환경이 아니면 사실상 불가능할 것 같습니다.

리팩토링 결과

public class StrategyPatternMain {

    public static void main(String[] args) {

        MallardDuck mallardDuck = new MallardDuck();
        mallardDuck.display();
        mallardDuck.performQuack(); // Duck 코드 재사용
        mallardDuck.performFly();   // Duck 코드 재사용

        mallardDuck.setFlyBehavior(new FlyNoWay()); // 런타임 변경 가능
        mallardDuck.performFly(); // 이제 날지 못합니다.
    }
}

전략 패턴

위처럼 “알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 해주는 패턴”“전략 패턴” 이라고 합니다.

  • 알고리즘 군은 변경되는 부분입니다.
  • 전략 패턴을 사용하면 알고리즘을 분리해서 독립적으로 원하는 알고리즘으로 교체 혹은 수정할 수 있습니다.

디자인 패턴의 필요성

전문 용어로써 생산성 제공

  • 위에서 했던 것을 풀어서 설명하는 것보다 “전략 패턴을 사용했습니다.” 라고 간결하게 전달할 수 있습니다.
  • 디자인 패턴을 모든 팀원이 공유하면 오해의 소지가 줄어들고 자질구레한 것들을 모여서 결정할 필요가 없습니다.

애플리케이션의 구조를 만드는 데 도움을 줌

  • 객체 지향의 개념(상속, 추상 등)만으로는 해결하기 어려운 애플리케이션 구조 설계에 도움을 줍니다.
  • 애플리케이션 구조를 더 이해하기 쉽고 관리하기 쉽게 만들어 줍니다.

코드를 유연성 있게 리팩토링할 수 있게 해줌

  • 디자인 패턴을 익히면 스파게티 코드를 빠르게 구분해내는 능력을 갖게 됩니다.
  • 그 스파게티 코드를 인지한 디자인 패턴에 맞추어 유연한 코드로 쉽게 개선할 수 있습니다.