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

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

전략 패턴 (Strategy Pattern)

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


1. 문제 상황

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

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

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


이 때 !

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

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


2. 해결을 위한 노력

1) 슈퍼클래스 Duckfly() 메소드 추가하기

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

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

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

문제 상황

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


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

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

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개의 댓글