[헤드퍼스트 디자인패턴] Chapter 01 - 디자인 패턴 소개와 전략 패턴

뚱이·2023년 4월 2일
post-thumbnail

전략 패턴 (Strategy Pattern)

알고리즘군을 정의하고 캡슐화해 각각의 알고리즘군을 수정해서 사용 가능
-> 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경 가능


1. 문제 상황

'SimUDuck' 이라는 오리 시뮬레이션 게임이 있다.
이 게임에는 다양한 오리가 등장하는데, 각 오리는 헤엄도 치고 꽥꽥 소리도 낸다.
현재 이 게임은, Duck 이라는 슈퍼클래스와 이 클래스를 확장한 다양한 종류의 오리 클래스들로 구성되어 있다.

이를 그림으로 표현하면 다음과 같다.

Duck 이라는 슈퍼클래스에는 헤엄을 치는 swim() 메소드, 꽥꽥 소리를 치는 quack() 메소드, 그리고 각 오리의 생김새를 나타내는 display() 메소드가 있다.
이 때, 오리의 생김새는 오리의 종류마다 다르므로 display() 메소드는 추상 메소드 이다.
그리고 슈퍼클래스 Duck을 확장한 구체적인 오리들, MallardDuck 과 ReadheadDuck 은 각각의 생김새가 다르므로 display() 메소드를 별도로 구현하고 있다.


이 때 !

회사 임원진이 게임의 차별화를 위해 오리가 날 수 있게 만들라고 한다.
그것도 일주일 안에 !

어떻게 하면 이 상황을, 코드를 효율적으로 해결할 수 있을까?


2. 해결을 위한 노력

1) 슈퍼클래스 Duck 에 fly() 메소드 추가하기

Duck 클래스에 fly() 메소드만 추가하면 모든 오리가 이걸 상속받아서 날 수 있게 된다 !

이를 UML로 표현하면 다음과 같다.

에엥 근데 장난감 오리가 날아다닌다는 피드백이 왔다

❌ 문제 상황

몇몇 서브클래스의 오리들만 날아야 하는데 모든 클래스의 오리들이 날고 있다


2) 상속을 통한 오버라이딩

슈퍼클래스 Duck 에 fly() 메소드를 구현한 다음, 서브클래스 RubberDuck 의 fly() 메소드에 아무것도 하지 않도록 오버라이딩 한다.

RubberDuck.java

quack() { // 삑삑 }
display() { // 장난감 고무 오리 }
fly() {
// 아무것도 하지 않도록 오버라이드
}

얼핏 보면 괜찮아보일 수도 있으나 전혀 아니다 🙅‍♀️

n개월마다 제품을 업데이트를 한다면, 규격은 계속 바뀔 것인데 그 때마다 서브클래스들을 다 확인하고 일일이 상황에 맞도록 오버라이딩을 한다 ?

너무 비효율적이다.

❌ 슈퍼클래스 상속의 단점

  • 서브 클래스에서 코드가 중복된다.
  • 실행 시에 특징을 바꾸기 힘들다.
  • 모든 오리의 행동을 알기 힘들다.
  • 코드를 변경했을 때 다른 오리들에게 원치 않은 영향을 끼칠 수 있다.

3) 인터페이스 활용하기

슈퍼클래스에 fly() 메소드가 들어있는 게 문제네 ? 🤔
꽥꽥 소리도 장난감 고무 인형 같은 경우엔 삑삑이 되네 ? 🤔

-> fly() 메소드가 들어있는 Flyable 인터페이스와 quack() 메소드가 들어있는 Quackable 인터페이스를 만들어 활용하자

그렇다면 이 디자인은 괜찮은가? 전혀 아니다.
날아다니는 동작의 디테일이 조금 변경되면, 모든 서브클래스들의 fly() 메소드를 전 ~~ 부 수정해야 한다.

❌ 문제 상황

변경사항이 생기면, 모든 서브클래스을 일일이 확인하고 수정해야 함.
-> 코드를 재사용 할 수 없다.


그렇다면 여기서 우리가 생각해봐야 할 소프트웨어 개발 불변의 진리 는 ?

" 아무리 디자인을 잘한 어플리케이션이라도 시간이 지남에 따라 변화하고 성장해야 한다는 사실 " 이다.


3. 찐 해결 방법

(1) 문제를 명확하게 파악하기

위에서 봤던 해결을 위한 노력들을 다시 살펴보자.

먼저 상속 !
서브클래스마다(=오리의 종류마다) 오리의 행동이 바뀔 수 있는데, 슈퍼클래스에서 메소드를 선언하면 모든 서브클래스에서 한 가지 행동만 하도록 하는 것이므로 바람직하지 않다.

그 다음은 인터페이스 !
인터페이스 사용 자체는 나름 괜찮아보였지만, 코드를 재사용할 수 없다는 게 큰 문제였다.
즉, 한 가지 행동을 바꿀 때마다 그 행동이 정의되어 있는 서로 다른 버스클래스들을 전~부 찾아서 코드를 일일이 수정해야 한다. 이 과정에서 새로운 버그가 안 생길 거라고 장담할 수도 없다.

이 때 생각할 수 있는 디자인 원칙이 있다.
이 원칙은 여러 디자인 원칙 가운데 첫 번째 원칙이다.

1. 어플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리한다.

이 원칙은 다음과 같이 생각할 수 있다.

= 코드에 새로운 요구 사항이 있을 때마다 바뀌는 부분이 있으면 분리해야 한다.
= 바뀌는 부분은 따로 뽑아서 캡슐화한다. 그러면 나중에 바뀌지 않는 부분에 영향을 미치지 않고 그 부분만 고치거나 확장할 수 있다.


(2) 바뀌는 부분과 그렇지 않는 부분 분리하기

아까 오리 시뮬레이션 게임을 이어서 생각해보자.
오리마다 다르게 작용해서 문제가 되는 부분은 fly() 와 quack() 메소드였다.

여기서 변화하는 부분과 그렇지 않은 부분을 분리하려면,
슈퍼클래스와는 별개로 2개의 클래스 집합을 만들어야 한다.
그리고 각 클래스 집합에는 각각의 행동을 구현한 것을 전부 집어 넣어야 한다.
여기서 하나는 fly(), 다른 하나는 quack() 과 관련된 부분일 것이다.


(3) 오리의 행동 디자인하는 방법

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

  1. 우선 최대한 유연하게 만든다.
  2. 슈퍼클래스(Duck)의 인스턴스에 행동을 할당할 수 있어야 한다. (-> 초기화 )
  3. 오리의 행동을 동적으로 바꿀 수 있게 한다. (-> setter )

여기서 두 번째 디자인 원칙을 생각해 볼 수 있다.

2. 구현보다는 인터페이스에 맞춰서 프로그래밍한다.

각 행동을 인터페이스(ex. FlyBehavior, QuackBehavior .. ) 로 표현하고, 이런 인터페이스를 사용해서 행동을 구현하면 다음과 같다.

이제부터 Duck 의 행동은 (특정 행동 인터페이스를 구현한) 별도의 클래스 안에 들어있고,
이로 인해 Duck 클래스에서는 행동을 구체적으로 구현할 필요가 없다.

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

= "상위 형식에 맞춰서 프로그래밍한다"
= 변수를 선언할 때 보통 추상 클래스나 인터페이스 같은 상위 형식으로 선언해야 한다. 객체를 변수에 대입할 때 상위 형식을 구체적으로 구현한 형식이라면 어떤 객체든 넣을 수 있기 때문이다. 그러면 변수를 선언하는 클래스에서 실제 객체의 형식을 몰라도 된다.


(4) 오리의 행동 구현 방법

이런 식으로 디자인하면 다른 형식의 객체에서도 행동을 사용할 수 있다 !
행동이 Duck 클래스 안에 숨겨져 있지 않기 때문이다.

그리고 기존의 행동 클래스나 Duck 클래스 및 행동을 사용하는 서브 클래스를 건드리지 않고 새로운 행동 또한 추가할 수 있다.

(5) 오리 행동 통합하기

행동도 구현했으니 이제 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 객체에 위임 된다.


(6) 동적으로 행동 지정하기

위에서 본 코드는 오리의 행동 형식을 생성자에서 인스턴스를 만드는 방식을 통해 지정했다.
그러나 Setter 메소드를 사용하면 서브 클래스에서 동적으로 행동을 지정할 수 있다 !



4. 정리

(1) 캡슐화된 행동 살펴보기

지금까지 본 오리의 행동들을 알고리즘군(family of algorithms) 으로 생각해보면, 여러 분야에 활용할 수 있다.

(2) 두 클래스를 합치는 방법

"A에는 B가 있다" 관계처럼 두 클래스를 합치는 것을 '구성(composition) 을 이용한다'고 말한다.

여기서 세 번째 디자인 원칙이 나온다.

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

앞에서 봤던 것처럼 구성을 활용해 시스템을 만들면 유연성을 크게 향상시킬 수 있다.

0개의 댓글