스트래티지 패턴(Strategy Pattern)이란

  • 행위를 클래스로 캡슐화해 동적으로 행위를 자유롭게 바꿀 수 있게 해주는 패턴
    • 같은 문제를 해결하는 여러 알고리즘이 클래스별로 캡슐화되어 있고 이들이 필요할 때 교체할 수 있도록 함으로써 동일한 문제를 다른 알고리즘으로 해결할 수 있게 하는 디자인 패턴
    • 행위 패턴(Behavioral) 중 하나이다.
  • 즉, 전략을 쉽게 바꿀 수 있도록 해주는 디자인 패턴
    • 전략 : 어떤 목적을 달성하기 위해 일을 수행하는 방식, 비즈니스 규칙, 문제를 해결하는 알고리즘 등을 말한다.
  • 예를 들어, 게임 프로그래밍에서 게임 캐릭터가 자신이 처한 상황에 따라 공격이나 행동하는 방식을 바꾸고 싶을 때 스트래티지 패턴은 유용하다.

그림

각 역할이 수행하는 작업

  • Strategy
    • 인터페이스나 추상 클래스로 외부에서 동일한 방식으로 알고리즘을 호출하는 방법을 명시
  • ConcreteStrategy
    • 스트래티지 패턴에서 명시한 알고리즘을 실제로 구현한 클래스. 즉, 구현체
  • Context
    • 스트래티지 패턴을 이용하는 역할을 수행한다.
    • 필요에 따라 동적으로 구체적인 전략을 바꿀 수 있도록 setter 메소드(집약 관계)를 제공한다.

집약 관계

  • 참조값을 인자로 받아 필드를 세팅하는 경우
  • 전체 객체의 라이프 타임과 부분 객체의 라이프 타임은 독립적이다.
  • 즉, 전체 객체가 메모리에서 사라진다 해도 부분 객체는 사라지지 않는다.

예시

오리 게임을 만들어보자.

오리 어플리케이션 게임을 운영하는 회사를 다니면서 오리게임을 만든다고 가정해보자. 표준적인 객체지향 기법을 사용해 Duck 이라는 슈퍼클래스를 만든 다음, 그 클래스를 확장하여 다른 종류의 오리를 만든다.

추상 클래스인 Duck 클래스를 RedHeadDuck 클래스와 MallardDuck 클래스가 상속을 받아 추상 메소드인 display()를 각각 구현한다.

문제의 시작1

처음 계획에는 없었는데 오리들이 물에 떠있는 기능 이외에 날아다녀야 하는 요구사항이 생겼다. 이건 간단하다.

그림처럼 나는 기능을 하는 fly() 메소드를 추가했다. 이로써 모든 오리들에게 날 수 있는 기능이 추가되었다.

그런데 오리 중 날 수 없는 오리가 있다는 사실을 잊고 있었다.

Duck 클래스에서 코드 한 부분만을 바꿈으로 인해서 프로그램 전체에 부작용이 발생했다. 장난감 고무 오리도 날게 된 것이다.

문제를 해결하기 위해 RubberDuck 클래스에서 fly() 메소드와 quak() 메소드를 오버라이드 하여 소리를 낼 수 있고 날 수 있게 하는 기능을 변경시켜 주었다.

눈 앞에 있는 문제는 해결되었지만, 향후에 RubberDuck과 같은 가짜 오리가 더 추가가 된다면 그때마다 맞지 않는 상속되는 메소드들을 오버라이드해서 구현해야 하는 문제가 여전히 존재한다.

문제의 시작2

갑자기 회사에서 1개월 마다 한번씩 새로운 오리를 업데이트 한다고 한다. 여러 오리가 새롭게 추가될 것이고 그 규격도 계속 변할 것이라고 한다.

그렇다면 매번 모든 오리 서브클래스의 fly()와 quack() 같은 메소드를 일일이 살펴봐야 하고 상황에 따라 오버라이드 해야 할 수도 있다. 그럼 여기서 의문이 생긴다. ‘과연 상속을 활용하는게 맞는 방법일까?’ 다시 생각해보자.

‘인터페이스는 어떨까?’

음, 코드 중복이 엄청나다. 메소드 몇개 오버라이드 하는 것을 피하다가 날아가는 동작 바꾸기 위해서 새롭게 생긴 모든 Duck 서브 클래스들을 전부 고쳐야 한다.

해결해보자

상속을 사용하는 것도 서브 클래스의 행동이 바뀔 수 있는데도 모든 서브 클래스들이 하나의 행동을 사용하는 것이 문제가 된다.

그리고 Flyable, Quackable 인터페이스를 사용하는 방법도 코드 재사용을 할 수 없다는 문제가 있다. (한가지의 행동을 바꿀 때마다 그 행동이 정의되어 있는 모든 서브 클래스들은 전부 찾아서 코드를 일일히 고쳐야 하고, 그 과정에서 새로운 버그가 생길 가능성이 많다.)

디자인 원칙

  • 애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분으로부터 분리킨다.

위의 디자인 원칙을 적용해보자. 달라지는 부분을 찾아서 나머지 코드에 영향을 주지 않도록 캡슐화를 시켜준다.

그러면 위에서 달라지는 부분은 fly(), quak() 이다.
이러한 행동을 Duck 클래스로부터 분리시키기 위해 각 행동을 나타낼 새로운 클래스의 집합을 만들어준다.

행동에 관한 인터페이스가 2개 생겼고 구체적인 행동을 구현하는 클래스들이 각각 생성된다. 이제 더이상 Duck에서 나는 행동과 소리를 내는 행동을 Duck 클래스나 그 서브 클래스에서 구현하지 않고 다른 클래스에게 위임을 해주게 된다.

디자인 원칙

  • 상속보다는 구성을 활용한다.
  • 구현이 아닌 인터페이스에 맞춰서 프로그래밍 한다.

그리고 Duck 클래스는 두 개의 인터페이스 형식의 인스턴스 변수가 추가된다.

Duck 클래스에서는 이제 행동을 직접 처리하는 대신에 새로 만든 performFly(), performQuack() 메소드에서 각각 FlyBehavior, QuackBehavior로 참조되는 객체에 그 행동을 위임해 줄 것이다.

A는 B이다 보다는 A에는 B가 있다가 나을 수 있다.
각각의 오리들에게는 FlyBehavior와 QuackBehavior 인터페이스 객체가 있으며 각각 나는 행동과 소리 행동을 위임 받는다.

이런 방식으로 두 클래스를 합치는 것을 구성(Composition)을 이용하는 것이라고 한다. 여기의 오리 클래스는 행동을 상속받는 대신, 올바른 행동 객체로 구성됨으로써 행동을 부여받게 되는 것이다.

예시2

이번에는 로봇 만들기로 예시를 들어보자.

Robot 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class Robot {
private String name;

public Robot(String name) {
this.name = name;
}

public String getName(){
return name;
}

// 추상 메소드.
public abstract void attack();
public abstract void movie();
}

TaekwonV, Atom 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class TaekwonV extends Robot {
public TaekwonV(String name) {
super(name);
}

@Override
public void attack() {
System.out.println("I have Missile.");
}

@Override
public void movie() {
System.out.println("I can only walk.");
}
}

public class Atom extends Robot {
public Atom(String name) {
super(name);
}

@Override
public void attack() {
System.out.println("I have strong punch.");
}

@Override
public void movie() {
System.out.println("I can fly.");
}
}

client 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class RobotClient {
public static void main(String[] args) {
Robot taekwonV = new TaekwonV("TaekwonV");
Robot atom = new Atom("Atom");

System.out.println("My Name is "+taekwonV.getName());
taekwonV.movie();
taekwonV.attack();

System.out.println();
System.out.println("My Name is "+atom.getName());
atom.movie();
atom.attack();
}
}

문제점

  1. 기존 로봇의 공격과 이동 방법을 수정하는 경우
    • Atom이 날 수는 없고 오직 걷게만 만들고 싶다면?
    • TaekwonV를 날게 하려면?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Atom extends Robot {
public Atom(String name) {
super(name);
}

@Override
public void attack() {
System.out.println("I have strong punch.");
}

@Override
public void movie() {
System.out.println("I can only Walk.");
}
}
  • 새로운 기능으로 변경하려고 기존 코드의 내용을 수정해야 한다. 따라서 OCP(개방 폐쇄 원칙)에 위배된다.
  • 또한, TaekwonV와 Atom의 move() 메소드의 내용이 중복된다. 이런 중복 상황은 많은 문제를 야기하는 원인이 된다.
  • 만약, 걷는 방식에 문제가 있거나 새로운 방식으로 수정하려면 모든 중복 코드를 일관성 있게 변경해야만 한다.
  1. 새로운 로봇을 만들어 기존의 공격 또는 이동 방법을 추가, 수정하는 경우
  • 새로운 로봇으로 Sungard를 만들어 TaekwonV의 미사일 공격 기능을 추가하려면?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Sungard extends Robot {
public Sungard(String name) {
super(name);
}

@Override
public void attack() {
System.out.println("I have Missile.");
}

@Override
public void movie() {
System.out.println("I can only walk.");
}
}
  • TaekwonV와 Sungard 클래스의 attack() 메소드의 내용이 중복된다.
  • 현재 시스템의 캡슐화 단위가 Robot 자체이므로 로봇을 추가하기는 매우 쉽다.
  • 그러나 새로운 로봇인 Sungard에 기존의 공격 또는 이동 방법을 추가하거나 변경하려고 하면 문제가 발생한다. (메소드의 중복 문제라던지 OCP 원칙 위반 등)

해결책

문제를 해결하기 위해서는 무엇이 변화되었는지를 찾은 후에 변화하지 않는 부분과 분리하여 변화하는 부분을 클래스로 캡슐화해야 한다.

  • 로봇 예제에서 변화되면서 문제를 발생시키는 요인은 로봇의 이동 방식과 공격 방식의 변화이다.
  • 이를 캡슐화하려면 외부에서 구체적인 이동 방식과 공격 방식을 담은 구체적인 클래스들을 은닉해야 한다.
    • 공격과 이동을 위한 인터페이스를 각각 만들고, 이들을 실제로 구현한 클래스를 만들어야 한다.
  • Robot 클래스가 이동 기능과 공격 기능을 이용하는 클라이언트 역할을 수행한다.
    • 구체적인 이동, 공격 방식이 MovingStrategy와 AttackStrategy 인터페이스에 의해 캡슐화 되어 있다.
    • 이 인터페이스들이 일종의 방화벽 역할을 수행해 Robot 클래스의 변경을 차단해준다.
  • 스트래티지 패턴을 이용하면 새로운 기능의 추가(새로운 이동, 공격 기능)가 기존의 코드에 영향을 미치지 못하게 하므로 OCP를 만족하는 설계가 된다.
    • 이렇게 변경된 새로운 구조에서는 외부에서 로봇 객체의 이동, 공격 방식을 임의대로 바꾸도록 해주는 setter 메소드가 필요하다.
    • setMovingStrategy, setAttackStrategy
    • 이렇게 변경이 가능한 이유는 상속 대신 집약 관계를 이용했기 때문이다.

스트래티지 패턴을 적용해서 로봇 예시를 수정해보자.

공격에 대한 인터페이스 구체적인 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 공격 기능에 대한 인터페이스.
public interface AttackStrategy {
void attack();
}
// 공격 인터페이스를 구현한 구현체 클래스.
public class MissileStrategy implements AttackStrategy {
@Override
public void attack() {
System.out.println("I have Missile");
}
}

public class PunchStrategy implements AttackStrategy {
@Override
public void attack() {
System.out.println("I have strong punch.");
}
}

이동에 대한 인터페이스와 구체적인 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 이동 기능에 대한 인터페이스.
public interface MovingStrategy {
void move();
}
// 이동 인터페이스를 구현한 구체적인 클래스.
public class FlyingStrategy implements MovingStrategy {
@Override
public void move() {
System.out.println("I can fly.");
}
}

public class WalkingStrategy implements MovingStrategy {
@Override
public void move() {
System.out.println("I can only walk.");
}
}

Robot 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public abstract class Robot {
private String name;
private AttackStrategy attackStrategy;
private MovingStrategy movingStrategy;

public Robot(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void attack() {
attackStrategy.attack();
}

public void movie() {
movingStrategy.move();
}

// setter 메소드
// 집약 관계, 전체 객체가 메모리에서 사라진다 해도 부분 객체는 사라지지 않는다.
public void setAttackStrategy(AttackStrategy attackStrategy) {
this.attackStrategy = attackStrategy;
}

public void setMovingStrategy(MovingStrategy movingStrategy) {
this.movingStrategy = movingStrategy;
}
}

구체적인 Robot 클래스

1
2
3
4
5
6
7
8
9
10
11
public class TaekwonV extends Robot {
public TaekwonV(String name) {
super(name);
}
}

public class Atom extends Robot {
public Atom(String name) {
super(name);
}
}

Client에서 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class RobotClient {
public static void main(String[] args) {
Robot taekwonV = new TaekwonV("TaekwonV");
Robot atom = new Atom("Atom");

taekwonV.setAttackStrategy(new MissileStrategy());
taekwonV.setMovingStrategy(new FlyingStrategy());

System.out.println("My name is " + taekwonV.getName());
taekwonV.attack();
taekwonV.move();

// 수정된 부분 : 전략 변경 방법.
System.out.println();
taekwonV.setAttackStrategy(new PunchStrategy());
taekwonV.setMovingStrategy(new WalkingStrategy());
taekwonV.attack();
taekwonV.move();


System.out.println();
atom.setMovingStrategy(new WalkingStrategy());
atom.setAttackStrategy(new PunchStrategy());

System.out.println("My name is " + atom.getName());
atom.attack();
atom.move();
}
}

따라서 기존의 코드를 수정하지 않고도 기능을 추가하거나 수정할 수 있게 되었다. OCP 원칙을 위배하지 않고 스트래티지 패턴을 적용한 예시이다.

참고