들어가며
이 포스팅의 내용은 <<Java 언어로 배우는 디자인 패턴>> 8장 Strategy 패턴을 정리한 것입니다. 여기 나오는 코드는 JDK 1.8에서 작성, 테스트 되었습니다. 이 포스팅에 있는 코드는 여기 서 확인하실 수 있습니다. 코드 또한 책에서 예제로 나온 코드를 조금 변경한 것입니다.
이전에 동일한 주제로 Head First Design Patterns 의 전략 디자인 패턴을 포스팅 한 적이 있습니다.
그 포스팅 또한 여기서 확인하실 수 있습니다.
Strategy 패턴
전략 디자인 패턴은 상황에 맞춰서 필요한 전략을 주입받아 사용하는 디자인 패턴입니다. 이번 포스팅에서 만들 프로그램은 가위바위보를 하는 프로그램입니다. 스펙은 다음과 같습니다.
- 두 개의 프로그램을 가위바위보 만 번을 시킨다.
- 각기 다른 가위바위보 전략을 사용한다.
- 이전에 낸 게 이겼다면, 다음에도 같은 걸 낸다. 예를 들면 가위를 내서 이겼다면 다음에도 가위를 냅니다.
- 이전에 낸 게 주먹이라면, 다음에 내는 것은 이전까지 낸 가위, 바위, 보 중 가장 승률이 높은걸 냅니다.
이 전략을 각각 Player 객체에 주입합니다. 주입된 전략을 Player 객체는 사용하게 됩니다. 이것이 전략 패턴입니다. 변경되는 지점을 캡슐화 한 다음에, 캡슐화 한 전략을 교환할 수 있게 하는 것입니다.
지금부터 만들 코드의 다이어그램은 다음과 같습니다.
다이어그램
앞에서 등장하는 클래스들은 각기 이런 역할을 맡습니다.
- Hand : Enum 클래스입니다. 주먹, 가위, 보 세 가지 프로퍼티가 있습니다. 그 외에도 Hand 에서 담당할만한 잔여 메서드를 가지고 있습니다.
- 값을 가지고 일치하는 프로퍼티를 찾는다
- 입력받은 Hand 객체가 자신하고 가위바위보를 했을때 누가 이기는지 판단한다.
- Strategy : 가위바위보에서 사용하는 전략을 결정하는 인터페이스입니다. 두 가지 메서드를 추상 인터페이스로 가지고 있습니다.
- nextHand : 이 전략을 사용시에 다음에 낼 주먹, 가위, 보를 정합니다.
- study : 승패를 가지고 학습하여, 다음에 어떤 걸 낼지 결정합니다.
- Player : 가위바위보를 하는 플레이어 객체입니다. 다음과 같은 역할을 맡습니다.
- 이기고 지는 경우, Strategy 객체를 통해 다음에 어떻게 행동할지 전략을 입력합니다.
- 총 대전 회수와 승패를 기록합니다.
Strategy는 두 가지 구현체를 가지고 있습니다. ProbStrategy와 WinningStrategy 입니다. 이는 아래쪽에서 어떤 역할을 맡고 있는지 설명하겠습니다.
Hand
Hnad 클래스는 주먹, 가위, 보를 구현하고 있습니다. 값을 입력받으면 일치하는 프로퍼티를 찾는 메서드와, 입력받은 Hnad 객체와 자신을 비교하여 가위바위보 시 누가 승리하였는지를 확인할 수 있습니다.
코드는 다음과 같습니다.
public enum Hand {
ROCK(0, "주먹"),
SCISSORS(1, "가위"),
PAPER(2, "보");
private final int value;
private final String desc;
private Hand(int value, String desc) {
this.value = value;
this.desc = desc;
}
public int getValue() {
return value;
}
public String getDesc() {
return desc;
}
/*
h가 입력되었을 때, h를 제외한 다른 프로퍼티 객체를 리스트로 리턴한다.
h가 Hand.ROCK인 경우, [Hand.SCISSORS, Hand.PAPER]을 리턴한다.
*/
public List<Hand> getRestHands(Hand h) {
Hand[] list = Hand.values();
List<Hand> result = new ArrayList<>();
for (int i =0; i<list.length; i++) {
if(list[i] != h) {
result.add(list[i]);
}
}
return result;
}
public static Hand findByValue(int value) {
Hand[] list = Hand.values();
for (int i =0; i<list.length; i++) {
if(list[i].getValue() == value) {
return list[i];
}
}
// 없으면 주먹을 디폴트로 한다.
return Hand.ROCK;
}
/*
승패를 판정한다. value에서 1을 더한 후에 3으로 모듈러 연산을 수행하면, 자신의 다음 순서 프로퍼티 벨류가 나온다.
예를 들면, 내가 주먹이고 h가 가위라면, (0 + 1) % 3 = 1 이므로, h.getValue() = 1 이기 때문에, isWin은 true가 리턴된다.
*/
private boolean isWin(Hand h) {
return ((getValue() + 1) % 3) == h.getValue();
}
public boolean isStrongerThan(Hand h) {
return isWin(h);
}
// 코드에서는 사용하지 않는다.
public boolean isWeakerThan(Hand h) {
return !isWin(h);
}
}Strategy
가위바위보 전략의 인터페이스를 결정합니다. 코드는 다음과 같습니다.
public interface Strategy {
// 다음에 어떤 손을 낼지 정한다.
Hand nextHand();
// 승패를 가지고 학습한다.
void study(boolean win);
}WinningStrategy
이 전략은 Strategy의 구현체입니다. 이 구현체에서 취하는 전략은 다음과 같습니다.
- 앞에서 낸 가위/바위/보 가 이겼다면, 같은 걸 낸다.
- 진 경우에는 랜덤으로 하나를 낸다.
간단한 전략이므로 바로 코드로 넘어가겠습니다. 코드는 다음과 같습니다.
public class WinningStrategy implements Strategy{
private Random random = new Random();
private boolean won;
private Hand prevHand = Hand.ROCK;
@Override
public Hand nextHand() {
if(won) {
return prevHand;
}
return Hand.findByValue(random.nextInt(3));
}
@Override
public void study(boolean win) {
this.won = win;
}
}ProbStrategy
이 전략 또한 Strategy의 구현체입니다. 이 구현체에서 취하는 전략은 다음과 같습니다.
- 지금까지 낸 가위/바위/보 중 가장 승률이 높은 걸 낸다.
예들 들어 말씀드리겠습니다. 만약 저번 판에 가위를 냈고, 지금까지 가위를 냈을 때 이긴 승률이 다음과 같습니다
- 가위 : 3번
- 바위 : 4번
- 보 : 5번
그러면, 보를 냅니다. 코드는 다음과 같습니다.
public class ProbStrategy implements Strategy{
private Random random = new Random();
private Hand prevHand = Hand.ROCK;
private Hand currentHand = Hand.ROCK;
/*
이전에 낸 값을 저장하는 Map 입니다. Map<Hand, Map<Hand, Integer>> 에서 키 Hand는 이전 판에서 어떤 값을 냈는지 나타냅니다.
리턴되는 Map<Hand, Integer>>각각 Hand를 내서 이긴 횟수를 나타냅니다.
history.get(Hand.ROCK) 는 이전 판에서 주먹을 냈을 때 각각 가위/바위/보의 이긴 횟수를 나타냅니다.
history.get(Hand.ROCK).get(Hand.PAPER) = 1 은, 이전 판에서 주먹을 냈을 때 바위를 내서 지금까지 한 판 이겼다는 것을 보여줍니다.
*/
private Map<Hand, Map<Hand, Integer>> history = new HashMap<>();
public ProbStrategy() {
initHistory();
}
// 데이터를 숫자 1로 초기화합니다.
private void initHistory() {
for(Hand h : Hand.values()) {
Map<Hand, Integer> map = new HashMap<>();
for(Hand h2 : Hand.values()) {
map.put(h2, 1);
}
history.put(h, map);
}
}
@Override
public Hand nextHand() {
// 이전에 가위, 바위, 보를 냈을 때 이겼던 횟수를 구합니다.
Map<Hand, Integer> prevHistory = history.get(prevHand);
// 1. 전체 합을 구합니다.
int sum = prevHistory.values().stream().mapToInt(i -> i.intValue()).sum();
// 2. 0 ~ 1에서 구한 전체합-1까지 난수를 생성합니다.
int bet = random.nextInt(sum);
// 3. 난수를 가지고 낼 Hand를 결정합니다.
Hand hand = findHandByBet(bet);
// 4. 값을 갱신 후 리턴합니다.
prevHand = currentHand;
currentHand = hand;
return hand;
}
/*
bet값이 들어왔을 때, 해당하는 Hand를 결정하여 리턴합니다.
bet값은 0~가위바위보를 낸 총 합 사이의 난수입니다. 예를 들어, 이전에 주먹을 냈을 때 바위로 4번, 가위로 3번, 보로 5번 이겼다면 bet는 0~14이 됩니다. (배열의 초기값이 1이기 때문)
만약 위에서 서술한 거 같이 히스토리가 쌓였다면 bet값이
0~4 사이라면 : 바위
5~8 사이라면 : 가위
9~14 사이라면 : 보
이렇게 되게 됩니다.
*/
private Hand findHandByBet(int bet) {
Hand hand;
if (bet < history.get(currentHand).get(Hand.ROCK)) {
hand = Hand.ROCK;
} else if (bet < history.get(currentHand).get(Hand.ROCK) + history.get(currentHand).get(Hand.SCISSORS)) {
hand = Hand.SCISSORS;
} else {
hand = Hand.PAPER;
}
return hand;
}
@Override
public void study(boolean win) {
Map<Hand, Integer> prevHistory = history.get(prevHand);
if(win) {
Integer value = prevHistory.get(currentHand);
prevHistory.put(currentHand, ++value);
} else {
List<Hand> restHands = prevHand.getRestHands(prevHand);
for(Hand h : restHands) {
Integer value = prevHistory.get(h);
prevHistory.put(h, ++value);
}
}
history.put(prevHand, prevHistory);
}
}Player
player 객체는 전략을 통하여, 게임을 진행하는 역할을 맡고 있습니다. 생성자에서 전략을 주입받으면, 그 전략을 가지고 다음과 같은 역할을 합니다.
- 승리/패배/무승부 시에 승패 스코어를 기록합니다.
- 주입받은 전략을 통해 다음에 낼 Hand를 결정합니다.
- 상태를 출력합니다.
코드는 다음과 같습니다.
public class Player {
private Strategy strategy;
private String name;
private int winCount;
private int gameCount;
private int loseCount;
public Player(String name, Strategy strategy) {
this.name = name;
this.strategy = strategy;
}
public Hand nextHand() {
return strategy.nextHand();
}
public void win() {
strategy.study(true);
gameCount++;
winCount++;
}
public void even() {
gameCount++;
}
public void lose() {
strategy.study(false);
gameCount++;
loseCount++;
}
public String printStatus() {
return "[" + name + ":" + gameCount + "game, " + winCount + " win, " + loseCount + " lose]";
}
public String getName() {
return name;
}
}StrategyService
위에서 작업한 코드를 가지고, 두 명의 플레이어를 가위바위보 시킵니다. 코드는 다음과 같습니다.
@Service
public class StrategyService {
public void fight() {
Player player1 = new Player("두리", new WinningStrategy());
Player player2 = new Player("하나", new ProbStrategy());
for(int i = 0; i< 10000; i++) {
Hand nextHand1 = player1.nextHand();
Hand nextHand2 = player2.nextHand();
if(nextHand1.isStrongerThan(nextHand2)) {
System.out.println("Winner:" + player1.getName());
player1.win();
player2.lose();
} else if(nextHand2.isStrongerThan(nextHand1)) {
System.out.println("Winner:" + player2.getName());
player1.lose();
player2.win();
} else {
System.out.println("Even...");
player1.even();
player2.even();
}
}
System.out.println("Total Result :");
System.out.println(player1.printStatus());
System.out.println(player2.printStatus());
/*
[두리:10000game, 2988 win, 4262 lose]
[하나:10000game, 4262 win, 2988 lose]
같은 전략을 쓰면 승률이 50%가 나온다.
*/
}
}승패를 확인해 보면, 하나쪽이 좀 더 승률이 높음을 알 수 있습니다. 이처럼 변경되는 부분을 하나의 객체로 분리하고, 필요할 때마다 인터페이스를 통하여 각기 다른 전략을 주입하는 것을 전략 패턴이라고 합니다.
결론
이 패턴은 스프링에서 많이 사용되고 있으니, 알아두면 좋을 거 같습니다. 스프링에서는 DI를 활용하여 전략 패턴을 구현하고 있는데요. 이전에 토비의 스프링 3.1 1권 앞부분을 정리하면서 DI에 관해서 정리한 슬라이드 쉐어가 있으니, 간단하게나마 참고하실 수 있을 거 같습니다. 여기 에서 확인하실 수 있습니다.