[디자인패턴] 1. 스트래티지 패턴 개념과 예제 (strategy pattern)

Dev Hanna·2021년 1월 12일
8

디자인패턴

목록 보기
1/3
post-thumbnail

Head First Design Patterns 책을 보고 정리한 내용입니다. 디자인 패턴을 처음 입문하시는 분들께 추천드리고픈 책입니다.

들어가면서...


디자인패턴을 왜 배워야 할까?

똑같은 객체지향 프로그래밍을 하면서도, 내가 하는 프로그래밍과 고수들이 하는 프로그래밍은 뼈대가 다르다. 똑같은 요구사항을 받아도, 초수의 코드는 변경이 어렵고 하나를 고치려면 열을 더 고쳐야 하지만, 고수는 기존의 코드를 거의 변경하지 않고도 새로운 기능을 추가하곤 한다.

초수가 어떻게 하면 고수가 될 수 있을까? 연차를 쌓는 것만이 답일까?

최고의 전략가 이순신 장군은 손자병법을 달고 사셨다. 다양한 전투상황에 대비하기 위해, 선대 전략가들의 병법서들을 숙지한 것이다. 다행히도, 객체지향 프로그래머들에게도 선배들로부터 내려지는 병법서가 있다!

그것이 바로 디자인패턴이다.

지금 내가 고민하는 이 문제는, 이미 십여년 전에 선배 개발자분들도 직면했던 문제다. 그것만으로도 디자인패턴을 공부해야하는 이유는 충분하다. 선배 개발자들이 직면했던 문제와, 해결하기 위한 지혜들을 디자인패턴을 통해 습득할 수 있다.

Head First Design Patterns 책이 다른 디자인패턴 책들과 다른점은, 이해하기 쉬운 예제로 문제상황부터 해결방법까지 마치 고사성어 풀이하듯 설명한다는 점이다. 나처럼 디자인패턴을 처음 접하는 초보자들에게 추천하고 싶다.

  1. 스트래티지 패턴
  2. 옵저버 패턴
  3. 데코레이터 패턴
  4. 팩토리 패턴
  5. 싱글턴 패턴
  6. 커맨드 패턴
  7. 어댑터 패턴 & 퍼사드 패턴
  8. 템플릿 메소드 패턴
  9. 이터레이터와 컴포지트 패턴
  10. 스테이트 패턴
  11. 프록시 패턴
  12. 컴파운드 패턴

이 책에서 소개하는 패턴들을 벨로그에 정리할 예정이다. 패턴소개에 중점을 두기 때문에, 코드를 상세하게 적진 않을 것이다. 참고로 벨로그 글은 한국버전(?)으로 각색이 들어가서 원서와 조금 다를 수 있다. 먼저 첫번째 주제는 스트래티지 패턴이다.

1. 스트래티지 패턴


# 문제는 '우아한 오리들' 애플리케이션에서 시작됐다.

명수는 연못에 오리를 키우는 '우아한 오리들' 이라는 게임회사를 다니고 있다. 이 게임에서는 헤엄도 치고 꽥꽥거리는 다양한 오리들이 존재한다. 이 게임을 처음 만든 사람들은 객체지향 기법에 따라, Duck 이라는 수퍼클래스를 만들고 그 클래스를 확장하여 다른 오리클래스들을 만들었다.

수퍼클래스 Duck

class Duck {
	quack() { "꽥꽥" } < 꽥꽥과 수영은 모든 오리들이 하므로 수퍼클래스에 미리 구현함
	swim() { "수영하기" }
	display() < 모양은 서로 다르기 때문에 추상메소드
 }

하위클래스 MallardDuck, RedheadDuck

class MallardDuck extends Duck {
	display() // 청둥오리 모양새
}

class RedHeadDuck extends Duck {
	display() // 홍머리오리 모양새
}

그런데, 게임업계 경쟁이 치열해지면서 획기적인 변화가 필요하게 되었다. 오리들은 더이상 꽥꽥거리거나 수영만 하지 않는다.

앞으로 오리가 날도록 해야한다!

1. 상속은 올바른 해결책이 아니다.

새로운 요구사항이 언짢은 명수는 Duck클래스에 fly()메소드만 추가하면 모든 오리들이 날 수 있을거라 생각했다. 상속은 이럴때 쓰는거지!

fly()가 추가된 수퍼클래스 Duck

class Duck {
	quack() { "꽥꽥" } 
	swim() { "수영하기" }
	display()
	fly() { "훨훨 날기" } < 추가
 }

[ 시스템 ] Duck 클래스에 fly() 가 추가되었습니다!
[ 시스템 ] Duck을 상속한 모든 오리클래스들에게 fly() 기능이 부여됩니다!

그런데..! 이렇게 코드를 수정한 명수는 사장님께 불려가게 된다.

"아니 이봐요 명수씨! 도대체 패치를 어떻게 했길래 고무오리들이 날아다니게 된겁니까?! 예?!!"

그렇다. 수퍼클래스에 fly() 메소드를 추가하자, fly() 기능이 필요 없는 다른 클래스들까지 해당 메소드가 생기고 말았다. 상속을 아주 잘 활용하였다고 생각했지만, 실제로는 버그를 생산하는데 일조한 것이다.

그러면... 고무오리만 fly() 메소드를 오버라이드 해버리지 뭐....

RubberDuck 클래스

class RubberDuck extends Duck {
	quack() { "삑삑" } // 삑삑 소리가 나게 오버라이드 
	fly() {  } < 아무것도 하지 않도록 오버라이드
	display() // 고무오리 모양새
 }

과연 이것이 Best Practice일까? 앞으로 나무오리가 생긴다면, quack()fly() 도 아무것도 하지 않도록 오버라이드 해야한다.
앞으로 새로운 오리들이 계속해서 생겨날 텐데, 그렇게 되면 서브클래스마다 상속받는 메소드들을 일일히 검토해야한다. 결국 전체가 아닌 일부 오리만 날거나 꽥꽥거릴 수 있도록하는 깔끔한 방법을 찾아야한다.

2. 인터페이스가 해결책이 될 수 있을까?

그렇다면 수퍼클래스 Duck에서 fly() 기능을 삭제하는 대신, Flyable 인터페이스에 fly()를 만들고, 날 수 있는 오리들만 Flyable 인터페이스를 구현하는 건 어떨까?
모든 오리들이 소리를 내지 않을 수도 있으니, Quackable 인터페이스를 만드는 것도 좋겠다!

수퍼클래스 Duck

class Duck {
	swim()
	display()
 }

인터페이스 for Duck

interface Flyable {
	fly()
 }
 
interface Quackable {
	quack()
 }

하위클래스 MallardDuck, RubberDuck, WoodDuck

class MallardDuck extends Duck implements Flayable, Quackable {
	display()
	fly()
	quack()
 }
 
class RubberDuck extends Duck implements Quackable {
	display()
	quack()
 }
 
 class WoodDuck extends Duck {
 	display()
 }

이렇게 구현하는 건 정말 바보같은 짓이다. 날지 않는 오리들의 fly()메소드를 오버라이드 하지 않으려고 날 수 있는 오리들에게 fly()메소드를 구현해줘야하는 일이 생겨버렸다.
또한, fly() { "훨훨 날다" } 라고 날 수 있는 하위클래스들에게 똑같은 메소드를 복붙해야하는 일이 생긴다. 즉, 코드를 재사용 할 수 없다는 것이다.

상속도 답이 아니고, 인터페이스도 처음엔 괜찮아 보였지만, 인터페이스에는 구현된 코드가 전혀 들어 갈 수 없다는 점에서 재사용성에 대한 문제가 있음을 깨달았다.

# 달라지는 부분을 분리해라! 일명 캡슐화 원칙.

디자인원칙 1.
애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분으로부터 분리시킨다.

하나의 모델에 불변하는 부분과 바뀌는 부분이 나뉘어져 있다면, 바뀌는 부분을 따로 뽑아 정의한다. 그러면 바뀌지 않는 부분에 영향을 주지 않고, 바뀌는 부분만 컨트롤 할 수 있게 된다. 이것이 캡슐화이다.

1. 그렇다면 어떻게 분리해야 할까?

Duck 클래스의 경우 fly()quack() 외의 다른 부분은 잘 작동하고 있다. 즉, 오리의 행동이 바뀌는 부분이다. 그렇다면 fly()quack()같은 오리의 행동을 동적으로 세팅 할 수 있다면 아주 좋을 것이다
예를들어 MallardDuck 객체를 생성할 때에, 특정 fly 행동을 생성자 인자에 넣는 방법이 있겠다. 한 보 더 나아가, 오리의 행동과 관련된 setter 메소드를 추가하여 객체를 생성한 후에도 얼마든지 오리의 행동을 재설정 할 수 있다면 더 좋을 것이다.
이렇게 비지니스 로직을 구성한 다음, 두 번째 디자인 원칙에 따라 설계해보자.

디자인원칙 2.
구현이 아닌 인터페이스에 맞춰서 프로그래밍 한다.

각 행동의 큰 틀은 인터페이스 FlyBehavior, QuackBehavior로 표현하고, 구체적인 설명은 각 인터페이스를 상속받아 구현하도록 하자.

인터페이스 FlyBehavior

interface FlyBehavior {
	fly()
 }
 
class FlyWithWings implements FlyBehavior {
	fly() { "날개로 훨훨 날다" } // 나는 모습을 구현
 }
 
class FlyNoWay implements FlyBehavior {
	fly() { "stay.." } // 날 수 없음
 }

인터페이스 QuackBehavior

interface QuackBehavior {
	quack()
 }
  
class Quack implements QuackBehavior {
	quack() { "꽥꽥" } 
 }
 
class Squeak implements QuackBehavior {
	quack() { "삑삑" } 
 }

class MuteQuack implements QuackBehavior {
	quack() { "..." } // 아무 소리도 내지 않음 
 }

이런 식으로 디자인하면 다른 형식의 객체에서도 나는 행동과 꽥꽥 행동을 재사용 할 수 있다. 또한, 기존의 Quack, Squeak 같은 행동 클래스를 전혀 건드리지 않고도 새로운 MuteQuack 행동을 추가할 수 있다.

2. 그런데, 왜 인터페이스로 구현해야 할까?

꼭 인터페이스를 사용해야하는 이유가 있을까? 추상클래스를 상속받아도 똑같은 기능을 구현할 수 있지 않을까?

'인터페이스'라는 것은 꼭, 자바의 interface 구조를 지칭하는 것이 아니다. '인터페이스에 맞춰 프로그래밍한다' 라는 말은 즉, 어떤 상위 형식(super type)에 맞춤으로 인해 다형성을 활용한다는 것이다.
객체의 변수 타입을 지정할 때 상위 형식을 사용한다면, 실제 그 객체에 변수를 대입할 때 에는 어떤 하위 객체가 와도 된다.
예를 든다면, Cake 인터페이스에 eat()이라는 메소드가 있고, 이 인터페이스를 CheeseCake 클래스에서는 eat() { "치즈맛이 난다" } 로 오버라이드, StrawBerryCake 클래스에서는 eat() { "딸기맛이 난다" }로 오버라이드했다고 하자. 아래와 같이 상위형식 Cake로 변수타입 선언 후, 실제로는 CheeseCake 객체를 대입한다면, eat() 메소드 호출시 "치즈맛이 난다"가 실행되게 된다. 만약 StrawBerryCake 객체였다면 똑같은 eat()메소드지만 "딸기맛이 난다"가 실행될 것이다. 그것이 다형성이다.

Cake cake = new CheeseCake();
cake.eat();

3. Duck 클래스에 오리의 행동 을 인터페이스로 위임하기

Duck 클래스에서 행동과 관련된 부분을 캡슐화하여, 별도의 인터페이스와 그것을 구현한 하위클래스들로 만들었다. 이제 Duck 클래스에서 기존에 있던 fly()기능을 FlyBehavior 인터페이스에 위임할 차례다.

  1. Duck 클래스에 flyBehaviorquackBehavior라는 레퍼런스 변수를 추가한다. 이제 Duck 클래스를 상속받는 모든 오리들은 인터페이스를 구현하는 것에 대한 레퍼런스를 가진다.
  2. flyBehaviorquackBehavior에 이미flyquack이 구현되어 있기 때문에, Duck 및 모든 하위클래스에 있는 fly()quack()메소드를 삭제한다.
  3. Duck클래스에 performFly(), performQuack()이라는 메소드를 추가한다.
  4. 다음과 같이 메소드들을 구현한다. 꽥꽥거리는 행동을 직접 구현하지 않고 quackBehavior로 참조되는 객체에 그 행동을 위임한다.
public class Duck {
	QuackBehavior quackBehavior;
	
	public void performQuack() {
		quackBehavior.quack();
	}
}
  1. Duck 클래스를 상속하는 하위클래스를 정의할 때에, flyBehaviorquackBehavior 인스턴스 변수를 설정한다.
public class MallardDuck extends Duck {
	public MallardDuck() { << 생성자에 어떤 나는 행동과, 어떤 꽥꽥거리는 행동을 넣을 지 정의한다.
		quackBehavior = new Quack();
		flyBehavior = new FlyWithWings();
	}

	public void display() {
		// 청둥오리의 모양새
	}
}

4. 이게 최선인가요?

"잠깐만. 아까전엔 특정 구현에 맞춘 프로그래밍은 안좋다고 했잖아. 이것도 new Quack() 이라고 함으로써, 결국 MallardDuck은 꽥꽥거리는 거 밖에 못하는거 아니야?"

일단은 그렇지만, 앞으로 디자인 패턴을 공부하다보면 그 문제를 해결하는 데 도움이 되는 다양한 패턴들을 배울수 있다.
다만 이제는 레거시코드(?) 라고 할 수 있는 Duck클래스의 quack() { "꽥꽥" }과 비교하자면, 이제는 Quack이나 MuteQuack같은 객체를 만들어서 레퍼런스 변수에 대입을 하는 형태로 quack()을 표현하기 때문에, 행동을 특정 클래스로 설정하고 있긴 하지만 실행시에 쉽게 변경할 수 있다.
Head First Design Patterns책에는 그 부분에 대한 내용과 예제문제들도 수록되어있다. 벨로그엔 그 부분은 생략하도록 하겠다.

# "A는 B이다"보다 "A에는 B가 있다"가 낫다.

A에는 B가 있다. 오리에는 FlyBehavior와 QuackBehavior가 있다. 그리고 그 행동들을 위임받는다.
두 클래스를 이런식으로 합치는 것을 구성(comosition)을 활용한다고 말한다. 청둥오리, 홍머리오리등의 모든 오리클래스들은 특정 행동을 상속받는 것이 아닌, 특정 행동객체로 구성됨으로써 행동이 부여된다. 이것이 3번째 디자인 원칙이다.

디자인원칙 3.
상속보다는 구성을 활용한다.

이 처럼 구성을 잘 활용하면 시스템의 유연성을 향상시킬 수 있다. setFlyBehavior(FlyBehavior fb)같은 메소드로 고무오리를 이벤트 성으로 날게 만들수도 있을 것이다.

마치며..


드디어 첫 번째 디자인패턴을 익혔다! 바로 스트래티지 패턴이다.

스트래티지 패턴에서는 알고리즘군을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만든다. 스트래티지를 활용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.

+ 추천강의

[우아한테크세미나] 190620 우아한객체지향 by 우아한형제들 개발실장 조영호님

개인적으로 추천하는 객체지향 프로그래밍 강의 영상이다. 배달의민족앱에 실제 사용된 객체지향 설계법을 강의한 영상이다. 디자인패턴을 학습중에 있는 신입 개발자 입장에선 아직 어려운 내용들이 많다. 하지만 이 책을 정리하고 벨로그에 소개하면서 12가지 디자인패턴법을 익힌다면, 다시 봤을 때 지금과는 더 많이 얻어가는 것이 있지 않을까?

profile
오늘도 1보 걷기

2개의 댓글

comment-user-thumbnail
2021년 1월 30일

안녕하세요, 혹시 헤드퍼스트 디자인 패턴 정리글 업로드 하는 과정에서 저작권 문제와 관련해 한빛미디어 측과 연락해보셨나요? 저도 정리글을 쓰고 싶은데 , 한빛미디어 공식홈페이지 가이드라인이 조금 애매하다고 느껴서요.

답글 달기
comment-user-thumbnail
2024년 1월 23일

짤과 예시는 직접 작성하신건가요? 너무 재밌네요 ㅋㅋ 재밌게 잘 봤습니다!

답글 달기