전략 패턴 (Strategy Pattern)
알고리즘군을 정의하고 캡슐화해 각각의 알고리즘군을 수정해서 사용 가능
-> 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경 가능
'SimUDuck' 이라는 오리 시뮬레이션 게임이 있다.
이 게임에는 다양한 오리가 등장하는데, 각 오리는 헤엄도 치고 꽥꽥 소리도 낸다.
현재 이 게임은, Duck 이라는 슈퍼클래스와 이 클래스를 확장한 다양한 종류의 오리 클래스들로 구성되어 있다.
이를 그림으로 표현하면 다음과 같다.
Duck 이라는 슈퍼클래스에는 헤엄을 치는 swim()
메소드, 꽥꽥 소리를 치는 quack()
메소드, 그리고 각 오리의 생김새를 나타내는 display()
메소드가 있다.
이 때, 오리의 생김새는 오리의 종류마다 다르므로 display()
메소드는 추상 메소드 이다.
그리고 슈퍼클래스 Duck을 확장한 구체적인 오리들, MallardDuck 과 ReadheadDuck 은 각각의 생김새가 다르므로 display()
메소드를 별도로 구현하고 있다.
회사 임원진이 게임의 차별화를 위해 오리가 날 수 있게 만들라고 한다.
그것도 일주일 안에 !
어떻게 하면 이 상황을, 코드를 효율적으로 해결할 수 있을까?
fly()
메소드 추가하기Duck 클래스에 fly()
메소드만 추가하면 모든 오리가 이걸 상속받아서 날 수 있게 된다 !
이를 UML로 표현하면 다음과 같다.
에엥 근데 장난감 오리가 날아다닌다는 피드백이 왔다
❌ 문제 상황
몇몇 서브클래스의 오리들만 날아야 하는데 모든 클래스의 오리들이 날고 있다
슈퍼클래스 Duck 에 fly()
메소드를 구현한 다음, 서브클래스 RubberDuck 의 fly()
메소드에 아무것도 하지 않도록 오버라이딩 한다.
RubberDuck.java
quack() { // 삑삑 }
display() { // 장난감 고무 오리 }
fly() {
// 아무것도 하지 않도록 오버라이드
}
얼핏 보면 괜찮아보일 수도 있으나 전혀 아니다 🙅♀️
n개월마다 제품을 업데이트를 한다면, 규격은 계속 바뀔 것인데 그 때마다 서브클래스들을 다 확인하고 일일이 상황에 맞도록 오버라이딩을 한다 ?
너무 비효율적이다.
❌ 슈퍼클래스 상속의 단점
- 서브 클래스에서 코드가 중복된다.
- 실행 시에 특징을 바꾸기 힘들다.
- 모든 오리의 행동을 알기 힘들다.
- 코드를 변경했을 때 다른 오리들에게 원치 않은 영향을 끼칠 수 있다.
슈퍼클래스에 fly()
메소드가 들어있는 게 문제네 ? 🤔
꽥꽥 소리도 장난감 고무 인형 같은 경우엔 삑삑이 되네 ? 🤔
-> fly()
메소드가 들어있는 Flyable 인터페이스와 quack()
메소드가 들어있는 Quackable 인터페이스를 만들어 활용하자
그렇다면 이 디자인은 괜찮은가? 전혀 아니다.
날아다니는 동작의 디테일이 조금 변경되면, 모든 서브클래스들의 fly()
메소드를 전 ~~ 부 수정해야 한다.
❌ 문제 상황
변경사항이 생기면, 모든 서브클래스을 일일이 확인하고 수정해야 함.
-> 코드를 재사용 할 수 없다.
" 아무리 디자인을 잘한 어플리케이션이라도 시간이 지남에 따라 변화하고 성장해야 한다는 사실 " 이다.
위에서 봤던 해결을 위한 노력들을 다시 살펴보자.
먼저 상속 !
서브클래스마다(=오리의 종류마다) 오리의 행동이 바뀔 수 있는데, 슈퍼클래스에서 메소드를 선언하면 모든 서브클래스에서 한 가지 행동만 하도록 하는 것이므로 바람직하지 않다.
그 다음은 인터페이스 !
인터페이스 사용 자체는 나름 괜찮아보였지만, 코드를 재사용할 수 없다는 게 큰 문제였다.
즉, 한 가지 행동을 바꿀 때마다 그 행동이 정의되어 있는 서로 다른 버스클래스들을 전~부 찾아서 코드를 일일이 수정해야 한다. 이 과정에서 새로운 버그가 안 생길 거라고 장담할 수도 없다.
이 때 생각할 수 있는 디자인 원칙이 있다.
이 원칙은 여러 디자인 원칙 가운데 첫 번째 원칙이다.
1. 어플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리한다.
이 원칙은 다음과 같이 생각할 수 있다.
= 코드에 새로운 요구 사항이 있을 때마다 바뀌는 부분이 있으면 분리해야 한다.
= 바뀌는 부분은 따로 뽑아서 캡슐화한다. 그러면 나중에 바뀌지 않는 부분에 영향을 미치지 않고 그 부분만 고치거나 확장할 수 있다.
아까 오리 시뮬레이션 게임을 이어서 생각해보자.
오리마다 다르게 작용해서 문제가 되는 부분은 fly()
와 quack()
메소드였다.
여기서 변화하는 부분과 그렇지 않은 부분을 분리하려면,
슈퍼클래스와는 별개로 2개의 클래스 집합을 만들어야 한다.
그리고 각 클래스 집합에는 각각의 행동을 구현한 것을 전부 집어 넣어야 한다.
여기서 하나는 fly()
, 다른 하나는 quack()
과 관련된 부분일 것이다.
fly 행동과 quack 행동을 구현하는 클래스 집합은 어떻게 디자인해야 할까?
여기서 두 번째 디자인 원칙을 생각해 볼 수 있다.
2. 구현보다는 인터페이스에 맞춰서 프로그래밍한다.
각 행동을 인터페이스(ex. FlyBehavior, QuackBehavior .. ) 로 표현하고, 이런 인터페이스를 사용해서 행동을 구현하면 다음과 같다.
이제부터 Duck 의 행동은 (특정 행동 인터페이스를 구현한) 별도의 클래스 안에 들어있고,
이로 인해 Duck 클래스에서는 행동을 구체적으로 구현할 필요가 없다.
❔"인터페이스에 맞춰서 프로그래밍한다"
= "상위 형식에 맞춰서 프로그래밍한다"
= 변수를 선언할 때 보통 추상 클래스나 인터페이스 같은 상위 형식으로 선언해야 한다. 객체를 변수에 대입할 때 상위 형식을 구체적으로 구현한 형식이라면 어떤 객체든 넣을 수 있기 때문이다. 그러면 변수를 선언하는 클래스에서 실제 객체의 형식을 몰라도 된다.
이런 식으로 디자인하면 다른 형식의 객체에서도 행동을 사용할 수 있다 !
행동이 Duck 클래스 안에 숨겨져 있지 않기 때문이다.
그리고 기존의 행동 클래스나 Duck 클래스 및 행동을 사용하는 서브 클래스를 건드리지 않고 새로운 행동 또한 추가할 수 있다.
행동도 구현했으니 이제 Duck 클래스 (혹은 서브 클래스) 와 통합해보자.
이 때 중요한 건, 행동들을 클래스에서 정의한 메소드를 통해 구현하지 않고 다른 클래스에 위임
한다는 것이다.
public abstact class Duck {
QuackBehavior quackBehavor;
// 기타 코드
public void performQuack() {
quackBehavior.quack();
}
}
이 코드에서 꽥꽥 행동을 직접 처리하지 않고, quackBehavior 로 참조되는 객체에 그 행동을 위임
했다.
public class MallardDuck extends Duck {
public MallardDuck() {
quackBehavior = new Quack();
flyBehavior = new FlyWithWings();
}
// 기타 코드
}
여기에서도 꽥꽥 행동을 직접 처리하지 않았다.
꽥꽥 행동을 처리할 때 Quack 클래스를 사용했고, 이로 인해 performQuack()
이 호출되면 꽥꽥 행동이 Quack 객체에 위임
된다.
위에서 본 코드는 오리의 행동 형식을 생성자에서 인스턴스를 만드는 방식을 통해 지정했다.
그러나 Setter
메소드를 사용하면 서브 클래스에서 동적으로 행동을 지정할 수 있다 !
지금까지 본 오리의 행동들을 알고리즘군(family of algorithms) 으로 생각해보면, 여러 분야에 활용할 수 있다.
"A에는 B가 있다" 관계처럼 두 클래스를 합치는 것을 '구성(composition) 을 이용한다'고 말한다.
여기서 세 번째 디자인 원칙이 나온다.
3. 상속보다는 구성을 활용한다.
앞에서 봤던 것처럼 구성을 활용해 시스템을 만들면 유연성을 크게 향상시킬 수 있다.