핵심

바뀌는 부분을 찾아내고, 바뀌지 않는 부분으로부터 분리 시킨다.
바뀌는 부분은 따로 뽑아서 캡슐화 시킨다.

문제의 시작

상속만을 사용하여 클래스를 디자인하였을때 발생하는 제약사항들로 시작되었습니다.


아래 그림은 Duck(오리)와 이를 상속하여 다양한 Duck을 구현하는 기초적인 객체지향의 상속을 그려낸 클래스 uml 입니다.

 


매우 평화롭습니다. 아직까지 말이죠.


오리가 날수 있다는 요구사항을 받았습니다.
이를 처리하기 위해 Duck 클래스에 fly() 함수를 추가했습니다.


이번엔 장난감 오리를 만들어야 합니다.

장난감 오리는 소리도없고 날수도 없습니다. 오버라이딩을 해봐야겠습니다.






요구사항을 적용하고 나니 뭔가.... 코드 정비가 잘 되지 않고,

계속 요구사항을 받아내기가 좀처럼 쉽지 않을것같은 예감이 듭니다.


위 설계에 대한 문제점을 하나하나 짚고 넘어가보겠습니다.

  1. 서브클래스에서 코드가 중복된다. 
    수퍼클래스에서 정의하였지만.. 결과적으로는 서브클래스에서 다시 한번 재정의하는 불상사가 발생하였습니다.
  2. 모든 서브클래스의 행동을 알기 어렵다.
    행동의 정의가 매우 자유롭기 때문에 어떤 클래스가 어떤 함수를 재정의 했는지는 그 클래스를 열어보기 전까지 알 수 없습니다.
  3. 실행시 특징을 변경하기 어렵다.
    날수 없는 오리지만 요구사항을 맞이하여 특별한 상황에는 날 수 있도록 해야합니다. 어떻게 할까요? 런타임에 함수를 교체할까요?
  4. 부모 클래스를 변경할때 서브클래스들에게 원치않는 영향을 끼칠 수 있다.
    함수의 내용을 변경하게 이를 상속하는 서브클래스중 어느 하나는 분명 문제가 생길 수 있습니다. 평소 사용되던 대로 흘러가지 않았기 때문이죠.
상속만으로는 클래스 디자인을 완벽하게 해결해주는 방법이 아니었습니다.

인터페이스는 어떠한가?

처음 설명했던 핵심을 토대로 위 문제를 interface로 해결해본다면
날거나 소리내는 행위를 수퍼클래스에서 제외하고 interface로 정의하여 각 클래스에서 구현하는 방식을 취할 수 있습니다.



그러나, 인터페이스를 구현하여 이러한 문제를 해결하는것은 옳지 못합니다.
인터페이스에는 구현된 코드가 절대 들어갈 수 없기때문에 재사용성인 측면에서 매우 부적합 합니다.

구체적으로 설명을 드리자면, 
CoolDuck 과 SharpDuck의 fly 가 동일한 행동을 하게 된다면 이는 똑같은 코드를 반복하여 작성하는 좋지못한 결과를 초래하게 됩니다.

다시한번 핵심을 상기

달라지는 부분을 찾아서 나머지 코드에 영향을 주지 않도록 "캡슐화" 합니다.
그러면 코드를 변경하는 과정에서 의도치 않은 일이 일어나는것을 줄이면서 시스템의 유연성을 향상시킬 수 있습니다.

이 개념은 매우 간단하게 들리지만 가장 중요한 원칙입니다.
모든 디자인 패턴의 기반을 이루는 원칙이기 때문이죠.

모든 패턴은 시스템의 일부분을 다른 부분과 독립적으로 변화 시킬 수 있는 방법을 제공하기 위한 것이니까요.


그럼 어떻게 하면 좋을까?

우선 바뀌는 부분과 바뀌지 않는 부분을 분리하도록 합니다.
fly(), quack() 문제를 제외하면 Duck 클래스는 잘 작동하고 있으므로 이 두 함수를 따로 떼어냅니다.

그리고 fly 와 quack 는 각각 행위에 대한 집합(set) 으로 분리합니다. 
fly 집합에는 다양한 fly행위를 클래스로 구성하고, quack 집합에는 다양한 quack 행위를 클래스로 구성합니다.

예를 들면 아래와 같도록 말이죠.
  • Fly (set)
    • FlyNoWay (날지 못한다)
    • Quick (빠르게 난다)
    • Slow (느리게 난다)
    • 등..
  • Quack (set)
    • Mute (소리를 내지 못한다)
    • Boom (폭발음의 소리를 낸다)
    • OMG (오마이갓을 외친다)
    • 등...

행동에 대한 디자인

fly 와 quack 행동을 구현하는 클래스 집합은 어떻게 디자인 해야할까요?

구현이 아닌 인터페이스에 맞춰 프로그래밍하도록 합니다.

우선 각 행동(behavior)을 인터페이스로 표현 합니다.
  1. FlyBehavior
  2. QuackBehavior
그리고 이런 행동을 구현할 때 위 인터페이스를 구현하도록 합니다.

fly와 quack 는 이제 더이상 Duck 클래스에서 구현하지 않습니다.
또한 위 인터페이스를 Duck 클래스가 구현하는것도 아닙니다.

행동(behavior) 인터페이스는 각각의 행동 클래스에서 구현 합니다.





이 방법은 지금까지 썻던 행동을 Duck 클래스에서 구체적으로 구현하거나,

서브클래스 자체에서 별도로 구현하는 방법과 상반된 방법 입니다.


기존 방식은 행동을 변경할 여지가 없는 반면, 

이 방법은 행동이 변경되면 그에따른 구현만 추가/수정해주면 쉽게 해결 될 수 있습니다.


위와같은 디자인을 사용하게 된다면 Duck의 서브클래스에서는 인터페이스를 사용하기만 하면 됩니다.


인터페이스에 맞춰서 프로그래밍 한다

핵심은 실행시 쓰이는 객체가 코드에 의해서 고정되지 않도록,
어떤 상위 형식(super type) 에 맞춰서 프로그래밍함으로써 다형성을 활용해야 한다는 것입니다.

이 원칙의 사용을 좀더 구체적으로 설명하면 아래와 같습니다.
  • 변수를 선언할때 추상 클래스나 인터페이스 같은 상휘 형식으로 선언한다.
  • 객체를 변수에 대입할 때 상위 형식을 구현한것이라면 어떤것이든 대입이 가능하다.
  • 이렇게 하면 변수를 선언하는 클래스에서는 실제 객체의 형식을 몰라도 된다.

간단한 다형성 예제

Animal 이라는 추상 클래스가 있고, 이를 상속하는 Dog, Cat이라는 구상 클래스가 있다고 생각해봅니다.

일반적인 구현에 맞춰 프로그래밍 한다면 아래와 같을 수 있습니다.
Dog d = new Dog();
d.bark();

하지만, 인터페이스/상휘 형식에 맞춰 프로그래밍 한다면 아래와 같을것입니다.
Animal animal = new Dog();
animal.makeSound();

좀더 바람직한 방법은 아래와 같을 수 있습니다.
Animal animal = getAnimal();
animal.makeSound();





각각의 행동을 구현

다시 되돌아가 행동에 대한 디자인을 이어서 해보겠습니다.

FlyBehavior와 QuackBehavior 두 인터페이스를 사용하고 구체적인 행동을 구현하는 클래스들은 아래와 같습니다.




행동 통합하기

가장 중요한점은 Duck에 나는것과 소리내는 행위를 Duck 또는 이를 상속하는 클래스에서 구현하지 않고 다른 클래스에 위임하는 일입니다.
따라서 Duck 클래스에 아래와 같은 인터페이스 형식의 인스턴스 변수와 각 행위를 간접적으로 수행해주는 함수를 추가합니다.



Duck 클래스 내의 performFly 와 performQuack 함수를 구현해 봅니다.

class Dock {

FlyBehavior flyBehavior;

QuackBehavior quackBehavior;


public void performFly() {

flyBehavior.fly();

}


public void performQuack() {

quackBehavior.quack();

}

}


각 행동들에 대해서 Duck 클래스는 인터페이스에 의해 참조되는 fly(), quack() 를 실행하기만 하면 됩니다.

어떤 객체의 종류인지는 전혀 신경쓸 필요가 없습니다.

중요한것은 실행시킬줄만 알면 되는것입니다.


행동 변수 설정하기

flyBehavior 와 quackBehavior 두 인터페이스의 행동 정의는 Duck 을 상속하는 클래스에서 정의하도록 합니다.

class CoolDuck extends Duck {

public CoolDuck() {

flyBehavior = new Quick();

quackBehavior = new Boom();

}

}


CoolDuck은 빠르게 날아갈 수 있고 폭발 소리를 낼 수 있는 오리의 특색을 갖추게 되었습니다.

(조금 억지스러운 오리가 되었네요...) 


런타임시에 동적으로 행동을 변경할 수도 있습니다.

setter 를 구현하여 행위를 교체하면 그만이죠.

class CoolDuck extends Duck {

public CoolDuck() {

flyBehavior = new Quick();

quackBehavior = new Boom();

}


// 실행시 setter 를 통해 행위를 변경 할 수 있음.

public void setFlyBehavior(FlyBehavior flyBehavior) {

this.flyBehavior = flyBehavior;

}

}



보다 나은 클래스 관계

A = B(A 는 B 이다) vs A has B(A에는 B가 있다)

후좌(A has B) 가 더 나을 수 있습니다.


클래스를 has 관계로 합치는것을 "구성을 이요하는것" 이라고 부릅니다.

상위 클래스로부터 행동을 상속받는것보다 행동 자체를 객체로 구성하는것이죠.


이 테크닉은 아주 중요한데 이것인 또다른 디자인의 원칙기도 합니다.

상속보다는 구성을 활용한다. 


지금까지 봐 왔던것처럼 구성을 이용하여 시스템을 만들면 유연성을 크게 향상시킬 수 있습니다.

캡슐화에 아주 좋다는 뜻입니다.


결론, Strategy pattern 이란

  1. 알고리즘군을 정의하고
  2. 각각을 캡슐화하여 
  3. 교환 하여 사용 가능하도록 구성한다.


이 패턴을 사용하여 사용하여 취할 수 있는 대표적인 장점은 바로 독립성입니다. 

이미 클라이언트가 사용하는 알고리즘을 변경없이 새로운 알고리즘으로 교체 할 수 있다는거죠.


'design pattern' 카테고리의 다른 글

헐리우드 원칙  (0) 2018.08.24
의존성 뒤집기 원칙 - dependency inversion principle  (0) 2018.08.22
최소 지식 원칙  (0) 2018.08.04
strategy pattern - 전략 패턴  (0) 2018.07.01
디자인 패턴과 원칙  (0) 2018.07.01

+ Recent posts