10. Strategy 패턴

적을 쳐부수기 위한 작전, 군대를 움직일 때의 방책 그리고 문제를 해결해 갈 때의 방법 등의 의미를 가지고 있는 Strategy, 프로그래밍의 경우에는 ‘알고리즘’으로 생각하면 된다. 모든 프로그램은 문제를 해결하기 위해 작성된다. 그리고 문제를 해결하기 위해 특정 알고리즘이 구현되고 있다. ‘Strategy 패턴’에서는 그 알고리즘을 구현한 부분을 모두 교환 할수 있다. 알고리즘을 빈틈없이 교체해서 같은 문제를 다른 방법으로 쉽게 해결할 수 있도록 도와주는 패턴이 ‘Strategy 패턴’이다.

예제 프로그램

‘Strategy 패턴’을 사용한 예제 프로그램을 살펴보자. 프로그램 내용은 컴퓨터에서 ‘가위바위보’를 실행한다. 가위바위보의 ‘전략’으로 두 가지 방법을 생각한다. 하나는 ‘이기면 다음에도 같은 손을 내민다’ 라는 조금 어리석은 방법(WinningStrategy)과 다른 하나는 ‘직전에 냈던 손에서 다음 낼 손을 확률적으로 계산한다’라는 방법(ProbStrategy)이다. (클래스 다이어그램은 책을 참조하자 179p)

Hand 클래스

Hand 클래스는 가위바위보의 ‘손을 표시하는 클래스다. 클래스 내부에서 주먹은 0, 가위는 1, 보는 2라는 int로 표현하고 있다. 이것을 손의 값을 표시하는 필드(handvalue)에 저장한다. Hand 클래스의 인스턴스는 세 개만 작성되고, 처음에 세개의 인스턴스가 만들어져 배열 hand에 저장된다. 클래스 메소드 getHand를 사용해서 인스턴스를 얻을 수 있다. 손의 값을 인수로 할당하면 인스턴스가 반환값이 된다. 이것은 Singleton 패턴의 일종이다. isStrongerThan과 isWeakerThan은 손의 승패를 비교한다. 두개의 손 hand1과 hand2가 있을경우

hand1.isStrongerThan(hand2)

혹은

hand1.isWeakerThan(hand2)

와 같이 사용한다. 이 클래스의 내부에서 실제로 손의 승패를 판정하는 것은 fight라는 메소드다. 승패의 판정에서는 손의 값을 사용하고 있다. 여기에서 사용하는

this.handvalue + 1) % 3 == h.handvalue

라는 식은 조금 이해하기 어렵지만 this의 손의 값에 1을 더한 것이 h의 손의 값(즉, this가 주먹이라면 h는 가위, this가 가위라면 h는 보, hits가 보라면 h는 주먹)이 되므로 this는 h를 이긴다는 것을 의미한다. 연산자 %을 사용해서 3의 잉여(나머지)를 취하고 있는 것은 보(2)에 1을 더했을때 주먹(0)이 나오도록 하기 위해서다. 이 식에서는 this.handvalue라고 기술되어 있지만 이것은 h.handvalue와 확실하게 비교하기 위해서다. 프로그램에서는

(handvalue + 1) % 3 == h.handvalue

라고 기술해도 같은 의미이다. 이 Hand 클래스는 다른 클래스(Player, WinningStrategy, ProbStrategy)로부터 사용되지만 Strategy 패턴의 역할에는 포함되지 않는다.

//가위바위보의 '손'을 표시하는 클래스
public class Hand {
	public static final int HANDVALUE_GUU = 0;
	public static final int HANDVALUE_CHO = 1;
	public static final int HANDVALUE_PAA = 2;
	public static final Hand[] hand = { // 가위바위보의 손을 표시하는 3개의 인스턴스
		new Hand(HANDVALUE_GUU),
		new Hand(HANDVALUE_CHO),
		new Hand(HANDVALUE_PAA)
	};

	private static final String[] name = {
		"주먹", "가위", "보",
	};

	private int handvalue;

	private Hand(int handvalue) {
		this.handvalue = handvalue;
	}

	public static Hand getHand(int handvalue) {
		return hand[handvalue];
	}

	public boolean isStrongerThan(Hand h){
		return fight(h) == 1;
	}

	public boolean isWeakerThan(Hand h) {
		return fight(h) == -1;
	}

	private int fight(Hand h ){
		if (this == h){
			return 0;
		} else if((this.handvalue + 1) % 3 == h.handvalue) {
			return 1;
		} else {
			return -1;
		}
	}

	public String toString() {
		return name[handvalue];
	}
}

Strategy 인터페이스

Strategy 인터페이스는 가위바위보의 ‘전략’을 위한 추상 메소드의 집합이다. nextHand는 ‘다음에 내는 손을 얻기’위한 메소드다. 이 메소드가 호출되면 Strategy 인터페이스를 구현하는 클래스는 전략적으로 ‘다음에 낼 손’을 결정한다. Study는 ‘직전에 낸 손으로 이겼는지, 졌는지’를 학습하기 위한 메소드다. 직전의 nextHand 메소드를 호출해서 이겼을 경우 study(true)로 호출 하고, 진 경우 study(false)로 호출한다. 이에 따라 Stratedy 인터페이스를 구현할 클래스는 자신의 내부 상태를 변화시켜 이후의 nexthand 메소드의 반환값을 결정하는 재료로 사용한다.

//가위바위보의 '전략'을 표시하는 인터페이스
public interface Strategy {
	public abstract Hand nextHand();
	public abstract void study(boolean win);
}

WinningStrategy 클래스

WinningStrategy 클래스는 Strategy 인터페이스를 구현하는 클래스 중 하나다. Strategy 인터페이스를 구현한다는 것은 nextHand와 study라는 두개의 메소드를 구현하는 것이 된다. 이 클래스는 직전승부에서 이겼으면 다음에도 같은 손을 낸다는 어리석은 전략을 취한다. 만일 직전승부에서 졌다면 다음 손은 난수를 사용해 결정한다. random 필드는 이 클래스가 난수를 필요로 할 때 사용할 java.util.Random(스펙)의 인스턴스를 저장한다. 말하자면 이 클래스가 사용한 난수 발생기라고 할 수 있다. won 필드는 이전 승부의 결과를 저장한다. 이겼으면 true, 졌으면 false가 된다. prevHand 필드는 이전 승부에서 내밀었던 손을 저장한다.

import java.util.Random;

public class WinningStrategy implements Strategy{
	private Random random;

	private boolean won = false;

	private Hand prevHand;

	public WinningStrategy(int seed) {
		random = new Random(seed);
	}

	public Hand nextHand() {
		if (!won) {
			prevHand = Hand.getHand(random.nextInt(3));
		}
		return prevHand;
	}

	public void study(boolean win) {
		won = win;
	}

}

ProbStrategy 클래스

ProbStrategy 클래스는 또 하나의 구체적인 ‘전략’이다. 이것은 다음손은 언제나 난수로 결정하지만, 과거 승패의 이럭을 사용해서 각각의 손을 낼 확률을 바꾸고 있다. history 필드는 과거의 승패를 반영한 확률계산을 위한 표를 만든다. history는 int의 2차원 배열로 각 차원의 첨자는 다음과 같은 의미를 가진다.

history [이전에 낸 손][이번에 낼 손]

이 식의 값이 크면 클수록 과거의 확률이 높다는 의미다. 이전에 자신이 주먹을 냈다고 가정하자. 이때 다음에 자신이 무엇을 낼 것인지 위에 적은 history[0][0], history[0];1], history[0][2]의 값에서 확률을 계산하는 것이다. 요약하면 이 세 가지 식의 값을 더해서(getSum 메소드) 0부터 그 수까지의 난수를 계산하고 그것을 기초도 다음 손을 결정한다.(nextHand 메소드). study 메소드는 nextHand 메소드에서 반환한 손의 승패를 기초로 history 필드의 내용을 갱신한다. 주의 > 이 전략은 상대방이 내는 가위바위보의 방법에 일정한 패턴이 있다고 전제함.

package org.jellydiss.patternpractice.strategy;

import java.util.Random;

public class ProbStrategy implements Strategy {
	private Random random;
	private int prevHandValue = 0;
	private int currentHandValue = 0;
	private int[][] history = {
			{ 1, 1, 1},
			{ 1, 1, 1},
			{ 1, 1, 1},
	};

	public ProbStrategy(int seed) {
		random = new Random(seed);
	}

	public Hand nextHand() {
		int bet = random.nextInt(getSum(currentHandValue));
		int handvalue = 0;
		if (bet < history[currentHandValue][0]){
			handvalue = 0;
		} else if (bet < history[currentHandValue][0] + history[currentHandValue][1]){
			handvalue = 1;
		} else {
			handvalue = 2;
		}

		prevHandValue =  currentHandValue;
		currentHandValue = handvalue;
		return Hand.getHand(handvalue);
	}

	private int getSum(int hv){
		int sum = 0;
		for(int i =0; i<3; i++){
			sum += history[hv][i];
		}
		return sum;
	}

	public void study(boolean win) {
		if (win) {
			history[prevHandValue][currentHandValue]++;
		} else {
			history[prevHandValue][(currentHandValue + 1) % 3]++;
			history[prevHandValue][(currentHandValue + 2) % 3]++;
		}
	}
}

Player 클래스

Player 클래스는 가위바위보를 하는 사람을 표현한 클래스다. Player 클래스는 ‘이름’과 ‘전략’이 할당되어 인스턴스를 만든다. nextHand 메소드는 다음의 손을 얻기 위한 것이지만, 실제로 다음의 손을 결정하는 것은 자식의 ‘전략’이다. 전략의 nextHand 메소드의 반환값이 그대로 Player의 nextHand 메소드의 반환값이 된다. nextHand 메소드는 자신이 해야 할 처리를 Strategy에게 맡기고 있는 즉, ‘위임’을 하고 있다. 이기거나(win), 지거나(lose), 비기거나(even) 한 승부의 결과를 다음 승부에 활용하기 위해서 Player 클래스는 strategy 필드를 통해서 study 메소드를 호출한다. study 메소드를 사용해서 전략의 내부 상태를 변화시킨다. wincount, losecount, gamecount는 플레이어의 승수를 기록한다.

package org.jellydiss.patternpractice.strategy;

public class Player {
	private String name;

	private Strategy strategy;

	private int wincount;

	private int losecount;
	private int gamecount;

	public Player(String name, Strategy strategy) {
		this.name = name;
		this.strategy = strategy;
	}

	public Hand nextHand() {
		return strategy.nextHand();
	}

	public void win() {
		strategy.study(true);
		wincount++;
		gamecount++;
	}

	public void lose() {
		strategy.study(false);
		losecount++;
		gamecount++;
	}

	public void even() {
		gamecount++;
	}

	@Override
	public String toString() {
		return "[" + name + " : " +  gamecount + " games, " + wincount + "win , "
				+ losecount + " lose, " + "]";
	}

}

Main 클래스

Main 클래스는 앞의 클래스를 이용해서 실제로 컴퓨터에서 가위바위보를 실행하기 위한 클래스다. 여기에서는 다음 두 명의 플래에어를 대전시킨다. 10,000번 대전시키서 그 결과를 표시한다.

package org.jellydiss.patternpractice.strategy;

public class Main {
	public static void main(String[] args) {
		if (args.length != 2 ){
			System.out.println("Usage: java Main randomseed1 randomseed2");
			System.out.println("Example: java Main 314 15");
			System.exit(0);
		}

		int seed1 = Integer.parseInt(args[0]);
		int seed2 = Integer.parseInt(args[1]);

		Player player1 = new Player("두리", new WinningStrategy(seed1));
		Player player2 = new Player("하나", new ProbStrategy(seed2));

		for(int i = 0; i < 10000; i++){
			Hand nextHand1 = player1.nextHand();
			Hand nextHand2 = player2.nextHand();

			if (nextHand1.isStrongerThan(nextHand2)) {
				System.out.println("Winner: " + player1);
				player1.win();
				player2.lose();
			} else if (nextHand2.isStrongerThan(nextHand1)) {
				System.out.println("Winner:" + player2);
				player1.lose();
				player2.win();
			} else {
				System.out.println("Even...");
				player1.even();
				player2.even();
			}

		}
		System.out.println("Total result:");
		System.out.println(player1);
		System.out.println(player2);

	}
}

‘Strategy 패턴’의 등장인물

‘Strategy 패턴’의 등장인물은 다음과 같다.

Strategy(전략)의 역할 예제 프로그램의 Strategy에 해당하며, Strategy는 전략을 이용하기 위한 인터페이스(API)를 결정한다.

ConcreteStrategy(구체적인 적략)의 역할 예제 프로그램의 WinningStrategy 클래스와 ProbStrategy 클래스에 해당하며, Strategy의 인터페이스(API)를 실제로 구현한다. 여기에서 구체적인 전략(알고리즘)을 실제로 프로그래밍한다.

Context(문맥) 예제 프로그램의 Player 클래스에 해당하며, Context는 Strategy을 이용하는 역할을 한다. ConcreteStrategy의 인스턴스를 가지고 있으며 필요에 따라 그것을 이용한다.

…… 클래스 다이어그램 및 부가 설명은 책을 참조하자(책 189p) ……

사고 넓히기

일부러 Strategy 역할을 만들 필요가 있을까?

보통 프로그래밍을 할 때 메소드 내부에 동화된 형태로 알고리즘을 구현하기 쉽다. 그러나 ‘Strategy 패턴’에서는 알고리즘의 부분을 다른 부분과 의식적으로 분리해서 알고리즘의 인터페이스(API) 부분만을 규정한다. 그리고 프로그램에서 위임에 의해 알고리즘을 이용한다. 이것은 프로그램을 복잡하게 만드는 것처럼 보이지만 실제로는 그렇지 않다. 예를 들어, 알고리즘을 개량해서 좀 더 빠르게 하고 싶다고 가정하자. ‘Strategy 패턴’을 사용하면 Strategy 역할의 인터페이스(API)를 변경하지 않도록 주의하고 ConcreteStrategy의 역할만을 수정하면 된다. 더욱이 위임이라는 느슨한 연결을 사용하고 있으므로 알고리즘을 용이하게 교환할 수 있다. 원래의 알고리즘과 개량한 알고리즘의 속도를 비교하고 싶을 경우에도 간단하게 교체해서 시험할 수 있다. Strategy패턴을 사용하면 장기 게임을 하는 프로그램에서 사용자의 선택에 맞추어서 사고 루틴의 레벤을 간단하게 교체할 수도 있다.

실행 중에 교체도 가능

‘Strategy 패턴’을 사용하면 프로그램의 동작 중에 ConcreteStrategy 역할의 클래스를 교체할 수도 있다. 예를들어, 메모리가 적은 환경에서는 SlowbutLessMemoryStrategy(속도는 느리지만 메모리를 절약하는 전략)를 사용하고, 메모리가 많은 환경에서는 FastButMoreMemoryStrategy(속도는 빠르지만 메모리를 많이 사용하는 전략)를 사용하는 것도 생각할수 있다. 한쪽의 알고리즘을 다른 쪽 알고리즘의 ‘검산’에 이용할 수도 있다. 예를 들어, 표 계산 소프트웨어의 디버그 판에서 복잡한 계산을 실행한다고 가정하ㅏㅈ. 이때, ‘버그가 있을지도 모르는 고속의 알고리즘’과 ‘저속이지만 확실한 계산을 실행하는 알고리즘’을 준비해서 전자의 검산을 후자로 실행시키는 것이다. 10-1 순서 없이 다음의 손을 내는 RandomStrategy 클래스를 작성 하십시오. 내가 생각한 답 :

import java.util.Random;

public class RandomStrategy implements Strategy{
	private Random random;

	public Hand nextHand() {
			return Hand.getHand(random.nextInt(3));
	}

	public void study(boolean win) {
	}

}

책 해설 : 내가 생각한 답과 동일하다. 10-2 이 장의 예제 프로그램 중 Hand 클래스의 fight 메소드에서는 무승부를 판정하기위해

this.handvalue == h.handvalue

라는 식이 아니고

this == h

라는 식을 사용하고 있다. 왜 이 식으로 해도 문제가 발생하지 않을까? 내가 생각한 답 : java에서 클래스가 Object클래스의 toString 을 오버라이딩 했을경우 주소 값이 아닌 toString의 리턴값을 받기 때문이다. 책 해설 : Hand 클래스의 인스턴스는 원래(주먹, 가위, 보) 세 개만 존재하기 때문이다. handvalue 필드의 값이 같은 두 개의 인스턴스가 있으면 사실은 같은 인스턴스다. 10-3 어떤 사람이 WinningStrategy 클래스를 프로그래명할 경우에 won 필드를

private boolean won = false

가 아니고

private boolean won;

이라고 기술했습니다. 그러나 = false 를 기술할 때와 같은 동작을 했습니다. 왜 그럴까요? 내가 생각한 답 : 필드에서 선언한 boolean 값의 경우 기본적으로 false 를 가지게 된다. 책 해설 : 내가 생각한 답과 동일하다. 10-4 다음 프로그램은 소트(정렬)를 실행하기 위한 클래스와 인터페이스다. 이 프로그램의 실행결과는 그림과 같다 여기에서는 알고리즘으로 선택 정렬(selection sort)을 이용하고 있다. 다른 알고리즘을 표현하는 클래스를 Sortor의 인터페이스에 맞게 작성하시오. Sorter 인터페이스

public interface Sorter {
	public abstract void sort(Comparable[] data);
}

SelectionSorter 클래스

public class SelectionSorter implements Sorter {
	public void sort(Comparable[] data) {
		for(int i = 0; i < data.length - 1 ; i++){
			int min = i;
			for(int j = i + 1; j < data.length; j++){
				if(data[min].compareTo(data[j]) > 0){
					min = j;
				}
			}
			Comparable passingplace = data[min];
			data[min]  = data[i];
			data[i] = passingplace;
		}
	}
}

SortAndPrint 클래스

public class SortAndPrint {
	Comparable[] data;
	Sorter sorter;
	public SortAndPrint(Comparable[] data, Sorter sorter) {
		this.data = data;
		this.sorter = sorter;
	}

	public void execute() {
		print();
		sorter.sort(data);
		print();

	}

	public void print() {
		for (int i = 0; i < data.length; i++){
			System.out.print(data[i] + ", ");
		}
		System.out.println();
	}
}

Main 클래스

public class Main {
	public static void main(String[] args) {
		String[] data = {
				"Sumpty", "Bowman", "Carroll", "Elfland", "Alice",
		};
		SortAndPrint sap = new SortAndPrint(data, new InsertSolter());
		sap.execute();
	}
}

내가 생각한 답 :

public class InsertSorter implements Sorter{

	public void sort(Comparable[] data){
		for(int index = 1 ; index < data.length ; index++){

		      Comparable temp = data[index];
		      int aux = index - 1;

		      while( (aux >= 0) && ( temp.compareTo(data[aux]) < 0) ) {

		         data[aux+1] = data[aux];
		         aux--;
		      }
		      data[aux + 1] = temp;
		   }
	}
}

책 해설 : 퀵소트 알고리즘을 적용하여 만들었다. (책 466p참조)