일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- java
- 코틀린
- 그래프
- DP
- 운영체제
- baekjoon
- lambda
- 모던자바
- OS
- 백트래킹
- DFS
- LEVEL2
- Brute-force
- 알고리즘
- 프로젝트
- 백준
- algorithm
- 네트워크
- programmers
- TDD
- 자바
- 프로그래머스
- BFS
- kotlin
- 스프링
- Java8
- back-end
- Spring
- 자료구조
- backtracking
- Today
- Total
요깨비's LAB
[Design Pattern, JAVA, 행위 패턴] Strategy Pattern - 알고리즘을 모두 바꾸기 본문
[Design Pattern, JAVA, 행위 패턴] Strategy Pattern - 알고리즘을 모두 바꾸기
요깨비 2019. 12. 11. 13:531. 스트래티지 패턴이란?
모든 프로그램은 문제를 해결하기 위해 작성됩니다. 그리고 문제를 해결하기 위해 특정 알고리즘이 구현돼있습니다.
Strategy 패턴에서는 그 알고리즘을 구현한 부분을 모두 교환할 수 있습니다. 정리하면
-
행위를 클래스로 캡슐화하여 동적으로 행위를 바꿀 수 있게 해주는 패턴
- 같은 문제를 해결하는 여러 알고리즘이 클래스 별로 캡슐화되어 있고 이들이 필요할 때 교체함으로써
동일한 문제를 다른 알고리즘으로 해결하도록 하는 디자인 패턴 -
즉 전략을 쉽게 바꿀 수 있도록 해주는 디자인 패턴
- Strategy : 어떤 목적을 달성하기 위해 일을 수행하는 방식, 비즈니스 규칙, 알고리즘 등을 의미 -
특히 게임 개발에서 유닛이 처한 상황에 따라 행동 양식을 바꾸고 싶을때 주로 사용됨
-
역할
- Strategy : 인터페이스나 추상 클래스로 외부에서 동일한 방식으로 알고리즘을 호출하는 방법을 명시, 전략을 이용하기 위한 인터페이스(API)를 결정
- ConcreteStrategy : 스트래티지 패턴에서 사용할 알고리즘을 실제로 구현한 클래스
- Context : 스트래티지 패턴을 이용하는 역할을 수행한다. 필요에 따라 동적으로 전략을 바꿀 수 있도록
setter 메소드(composite 조립)를 제공한다. -
분류
- 행위(Behavioral) 패턴
(1) 객체나 클래스 사이의 알고리즘이나 책임 분배에 관련된 패턴
(2) 한 객체가 혼자 수행할 수 없는 작업을 여러 개의 객체로 어떻게 분배하는지, 또 그렇게 하면서도 객체 사이의
결합도를 최소화하는 것에 중점을 둔다.
- 집약 관계(composite, 조립)
(1) setter 메소드를 통해 인스턴스의 값을 세팅하는 것
(2) 전체 객체의 라이프타임과 부분 객체의 라이프 타임은 독립적이다. -> 전체 객체가 사라져도 부분 객체는 사라지지 않는다.
- 예시 1 가위 바위 보 게임
- Hand Class
public class Hand {
public static final int HANDVALUE_ROCK = 0; // 주먹
public static final int HANDVALUE_SCISSOR = 1; // 가위
public static final int HANDVALUE_PAPER = 2; // 보
// 가위바위보의 손을 표시하는 3개의 인스턴스
public static final Hand[] hand = {
new Hand(HANDVALUE_ROCK),
new Hand(HANDVALUE_SCISSOR),
new Hand(HANDVALUE_PAPER),
};
// 가위바위보의 손의 문자열 표현
private static final String[] name = {
"주먹", "가위", "보"
};
private int handvalue; // 가위바위보의 손의 값
private Hand(int handvalue) {
this.handvalue = handvalue;
}
// 값에서 인스턴스를 얻는다
public static Hand getHand(int handvalue) {
return hand[handvalue];
}
// this가 h를 이길 경우 true
public boolean isStrongerThan(Hand h) {
return fight(h) == 1;
}
// this가 h에게 질 경우 true
public isWeakerThan(Hand h) {
return fight(h) == -1;
}
// 무승부는 0, this의 승이면 1, 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 Interface
public interface Strategy {
// 다음의 낼 손을 결정하는 메소드
public abstract Hand nextHand();
/*
직전에 낸 손으로 이겼는지, 졌는지를 학습하기 위한 메소드로 직전의 nextHand 메소드를 호출해서
이겼을 경우 study(true)로 호출하고, 진 경우 study(false)로 호출한다. 이에 따라
Strategy 인터페이스를 구현할 클래스는 자신의 내부 상태를 변화시켜 이후의 nexthand 메소드의
반환값을 결정하는 재료로 사용한다.
*/
public abstract void study(boolean win);
}
- WinningStrategy Class
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 Class
import java.util.Random;
/*
다음 손은 언제나 난수로 결정하지만, 과거 승패의 이력을 사용해서 각각의 손을 낼 확률을 바꿈.
*/
/*
* history 필드는 과거의 승패를 반영한 확률계산을 위한 표를 만듦.
* history[이전에 낸 손][이번에 낼 손]
* 이 식의 값이 크면 클수록 과거의 확률이 높다는 의미입니다.
* ...history[0][0] : 주먹, 주먹을 자신이 냈을 때의 과거의 승수
* ...history[0][1] : 주먹, 가위를 자신이 냈을 때의 과거의 승수
* ...history[0][2] : 주먹, 보를 자신이 냈을 때의 과거의 승수
*
* 이전에 자신이 주먹을 냈다고 가정하면, 이때 다음에 자신이 무엇을 낼 것인지 위에 적은 history[0][0], history[0][1], history[0][2]의 값에서 확률을 계산하는 것입니다.
* 요약하면 이 세 가지 식의 값을 더해서(getSum()) 0부터 그 수까지 난수를 계산하고 그것을 기초로 다음 손을 결정합니다.(nextHand())
*
* 예를들어,
* ... history[0][0]의 값이 3
* ... history[0][1]의 값이 5
* ... history[0][2]의 값이 7
* 이라고 가정하면 이 경우 주먹, 가위, 보를 낼 비율을 3:5:7로 해서 다음의 손을 결정합니다. 0 이상 15미만 (15는 3+5+7의 값)의 난수값을 얻어서
* ... 0 이상 3 미만이면 주먹
* ... 3 이상 8(=3+5) 미만이면 가위
* ... 8 이상 15(=3+5+7) 미만이면 보
* 라고 합니다.
*
*/
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 Class
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;
}
// 전략의 지시를 받는다.
/*
nextHand 메소드는 다음에 낼 손을 얻기 위한 것이지만, 실제로 다음의 손을 결정하는 것은
내가 아닌 내가 선택한 ConcreteStrategy이다.
*/
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++;
}
public String toString() {
return "[" + name + ":" + gamecount + "games, " + wincount +
" win, " + losecount + " lose" + "]";
}
}
- Main Class
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.toString());
System.out.println(player2.toString());
}
}
* 보통 프로그래밍을 할 때 메소드 내부에 동화된 형태로 알고리즘을 구현하기 쉽다. 그러나 Strategy 패턴은 알고리즘의
부분을 다른 부분과 분리해서 알고리즘의 인터페이스(API) 부분만 정의하고 있다. 그리고 프로그램에서 위임을 통해
알고리즘(=Strategy)을 이용한다.
이것은 프로그램을 더 복잡하게 만드는 것처럼 보이지만 실제로는 그렇지 않다.
예를들어, 새로운 알고리즘을 추가하거나 변경하고 싶다고 한다면 Strategy 패턴을 사용함으로써 Strategy 역할의
인터페이스는 변경하지 않고 Concrete Strategy의 역할만 추가하거나 수정하면 끝이다.
이것은 위임이라는 느슨한 연결을 사용하고 있으므로 알고리즘을 용이하게 교환할 수 있다. 다음 예시를 보자.
- 예시 2 로봇 만들기
1. Legacy Robot(패턴 적용하기 전 로봇)
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 move();
}
public class TaekwonV extends Robot {
public TaekwonV(String name) {
super(name);
}
@Override
public void attack() {
// TODO Auto-generated method stub
System.out.println("I have Missile.");
}
@Override
public void move() {
// TODO Auto-generated method stub
System.out.println("I can only walk.");
}
}
public class Atom extends Robot{
public Atom(String name) {
super(name);
}
@Override
public void attack() {
// TODO Auto-generated method stub
System.out.println("I have strong punch.");
}
@Override
public void move() {
// TODO Auto-generated method stub
System.out.println("I can fly.");
}
}
public class Client {
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.move();
taekwonV.attack();
System.out.println();
System.out.println("My name is " + atom.getName());
atom.move();
atom.attack();
}
}
문제점
1. 기존 로봇의 공격과 이동 방법을 수정하는 경우
- Atom이 날 수는 없고 걷게만 바꾸고 싶다면?
// Atom 클래스 내부에서
public void move() {
System.out.println("I can only walk.");
}
- TaekwonV를 날게 수정하려면?
// TaekwonV 클래스 내부에서
public void move() {
System.out.println("I can fly.");
}
=> 새로운 기능 및 기능의 수정을 위해 기존 코드의 내용을 수정해야 하므로 OCP에 위배된다.
또한 TaekwonV와 Atom의 move() 메서드가 중복된다. 이러한 중복은 유지보수의 어려움과 많은 문제를 야기한다.
2. 새로운 로봇을 만들어 공격 또는 이동 방법을 추가/수정하는 경우
public class Sungard extends Robot{
public Sungard(String name) {
super(name);
}
@Override
public void attack() {
// TODO Auto-generated method stub
System.out.println("I have Missile.");
}
@Override
public void move() {
// TODO Auto-generated method stub
System.out.println("I can only walk.");
}
}
- TaekwonV와 Sungard 클래스의 attack() 내용이 중복된다.
-
- 현재 시스템의 캡슐화의 단위가 'Robot' 자체이므로 로봇을 추가하기는 매우 쉽다.
- 그러나 새로운 로봇에 기존의 공격 또는 이동 방법을 추가하거나 변경하려고 하면 문제가 발생한다.
해결책
문제를 해결하기 위해서는 무엇이 변화되었는지 찾은 후에 이를 클래스로 캡슐화해야 한다.
1. 로봇 예시에서 변화되면서 문제를 발생시키는 요인은 로봇의 이동 방식과 공격 방식의 변화 이다.
2. 이를 캡슐화하려면 외부에서 구체적인 이동 방식과 공격 방식을 담은 구체적인 클래스들을 은닉해야한다.
3.
- Robot 클래스가 이동 기능과 공격 기능을 이용하는 클라이언트 역할을 수행
(1) 구체적인 이동, 공격 방식이 MovingStrategy와 AttackStrategy 인터페이스에 의해 캡슐화되어 있다.
(2) 해당 인터페이스들이 일종의 방화벽 역할을 수행해 Robot 클래스의 변경을 차단해준다.
- Strategy 패턴을 이용하면 새로운 기능의 추가(새로운 이동, 공격 기능)가 기존의 코드에 영향을 미치지
못하게 하므로 OCP를 만족시킨다.
2. New Robot(패턴 적용 후의 로봇)
-
Robot Class
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 move() {
movingStrategy.move();
}
// 집약 관계(Aggregation), 전체 객체가 메모리에서 사라진다 해도 부분 객체는 사라지지 않느다.
public void setAttackStrategy(AttackStrategy attackStrategy) {
this.attackStrategy = attackStrategy;
}
public void setMovingStrategy(MovingStrategy movingStrategy) {
this.movingStrategy = movingStrategy;
}
}
- Concrete Robot Class
public class TaekwonV extends Robot {
public TaekwonV(String name) {
super(name);
}
@Override
public void attack() {
// TODO Auto-generated method stub
System.out.println("I have Missile.");
}
@Override
public void move() {
// TODO Auto-generated method stub
System.out.println("I can only walk.");
}
}
public class Atom extends Robot{
public Atom(String name) {
super(name);
}
@Override
public void attack() {
// TODO Auto-generated method stub
System.out.println("I have strong punch.");
}
@Override
public void move() {
// TODO Auto-generated method stub
System.out.println("I can fly.");
}
}
-
공격 이동 기능에 대한 인터페이스와 구체적인 클래스
public interface AttackStrategy {
public void attack();
}
public class MissileStrategy implements AttackStrategy{
@Override
public void attack() {
// TODO Auto-generated method stub
System.out.println("I have Missile.");
}
}
public class PunchStrategy implements AttackStrategy{
@Override
public void attack() {
// TODO Auto-generated method stub
System.out.println("I have strong punch.");
}
}
public interface MovingStrategy {
public void move();
}
public class FlyingStrategy implements MovingStrategy{
@Override
public void move() {
// TODO Auto-generated method stub
System.out.println("I can fly.");
}
}
public class WalkingStrategy implements MovingStrategy{
@Override
public void move() {
// TODO Auto-generated method stub
System.out.println("I can only walk.");
}
}
- Client Class
public class Client {
public static void main(String[] args ){
Robot taekwonV = new TaekwonV("TaekwonV");
Robot atom = new Atom("Atom");
/* 수정된 부분: 전략 변경 방법 */
taekwonV.setMovingStrategy(new WalkingStrategy());
taekwonV.setAttackStrategy(new MissileStrategy());
atom.setMovingStrategy(new FlyingStrategy());
atom.setAttackStrategy(new PunchStrategy());
/* 아래부터는 동일 */
System.out.println("My name is " + taekwonV.getName());
taekwonV.move();
taekwonV.attack();
System.out.println();
System.out.println("My name is " + atom.getName());
atom.move();
atom.attack();
}
}
* 관련 패턴
-
Flyweight 패턴: ConcreteStrategy 역할은 Flyweight 패턴을 사용해서 복수의 장소에서 공유할 수도 있다.
-
Abstract Factory 패턴: Strategy 패턴은 알고리즘을 모두 교체할 수 있다. Abstract Factory 패턴은 구체적인 공장,
부품, 제품을 모두 교체할 수 있다. -
State 패턴: 둘 다 위임하는 곳을 교환하는 패턴이고 클래스 간의 관계도 매우 비슷하지만 두 패턴의 목적은 다르다.
Strategy 패턴은 '알고리즘'을 표현하는 클래스를 작성해서 그것을 ConcreteStrategy 역할의 클래스로 한다.
Strategy 패턴에서는 클래스를 교체할 수 있지만 필요하지 않으면 교체하지 않아도 된다.
한편, State 패턴에서는 '상태'를 표현하는 클래스를 작성해서 그것을 ConcreteStrategy 역할의 클래스로 한다.
State 패턴에서는 상태가 변화할 때마다 위임하는 곳의 클래스가 반드시 교체된다.
한빛 미디어 - JAVA 객체지향 디자인패턴
영진닷컴 - JAVA 언어로 배우는 디자인 패턴 입문
'컴퓨터 공학기초 > 디자인패턴' 카테고리의 다른 글
[Design Pattern, JAVA, 생성 패턴] Singleton Pattern - 인스턴스를 한개만 만들기 (0) | 2019.12.09 |
---|