JAVA 언어로 배우는 디자인 패턴 입문, 헤드퍼스트 디자인 패턴을 통해 학습 중이며 두 책의 예제 위주로 정리.
생성패턴(Creational Pattern)
1. 싱글톤(Singleton)
2. 빌더(Builder)
3. 팩토리 메소드(Factory Method)
4. 추상 팩토리(Abstract Factory)
5. 프로토타입(Prototype)
구조패턴(Structural Pattern)
2. 브릿지(Bridge)
3. 컴포지트(Composite)
4. 데코레이터(Decorator)
5. 퍼사드(Facade)
6. 플라이웨이트(flyweight)
7. 프록시(Proxy)
행동(행위) 패턴(Behavioral Pattern)
1. 책임 연쇄(Chain of Responsibility)
2. 커맨드(Command)
3. 인터프리터(Interpreter)
5. 중재자(Mediator)
6. 메멘토(Memento)
7. 옵저버(Observer)
8. 상태(State)
9. 전략(Strategy)
11. 방문자(Visitor)
전략 패턴(Strategy Pattern)은 행위패턴(Behavioral Pattern)에 속하는 디자인 패턴이다.
전략패턴은 스위치를 전환하듯 알고리즘(전략)을 바꿔서 같은 문제를 다른 방법으로 해결하기 쉽게 만들어주는 패턴이다.
특정한 계열의 알고리즘들을 정의하고 각 알고리즘을 캡슐화하여 이 알고리즘들을 해당 계열 안에서 상호 교체가 가능하게 만든다.
예제
자바 언어로 배우는 디자인패턴 입문 책에서의 예제를 먼저 본다.
예제 프로그램은 가위바위보를 하는 프로그램이다.
이 프로그램에서는 두가지의 전략을 갖는다.
1. 이기면 다음에도 같은 손을 내는 다소 어리석은 방법(WinningStrategy)
2. 직전 손에서 다음 손을 확률적으로 계산하는 방법(ProbStrategy)
//예제 출처 - JAVA 언어로 배우는 디자인패턴 입문
public enum Hand {
ROCK("바위", 0)
, SCISSORS("가위", 1)
, PAPER("보", 2);
private String name; //가위바위보 손 이름
private int handValue; //가위바위보 손의 값
private static Hand[] hands = {
ROCK, SCISSORS, PAPER
};
// Constructor
private Hand(String name, int handValue) {
this.name = name;
this.handValue = handValue;
}
//손의 값으로 enum 상수를 가져온다
public static Hand getHand(int handValue) {
return hands[handValue];
}
//this가 h 보다 강하다면 true
public boolean isStrongerThan(Hand h) {
return fight(h) == 1;
}
//this가 h보다 약하다면 true
public boolean 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;
}
@Override
public String toString() {
return name;
}
}
public interface Strategy {
public abstract Hand nextHand();
public abstract void study(boolean win);
}
/**
* 이기면 다음에도 같은 손을 내는 전략
*/
public class WinningStrategy implements Strategy{
private Random random;
private boolean won = false;
private Hand prevHand;
public WinningStrategy(int seed) {
random = new Random(seed);
}
@Override
public Hand nextHand() {
if(!won)
prevHand = Hand.getHand(random.nextInt(3));
return prevHand;
}
@Override
public void study(boolean win) {
won = win;
}
}
/**
* 확률을 계산해 다음 손을 결정하는 전력
*
* history[직전에 낸 손][이번에 낸 손]
* 배열의 value는 과거의 승리 수를 의미
*/
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);
}
@Override
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 handValue) {
int sum = 0;
for(int i = 0; i < 3; i++)
sum += history[handValue][i];
return sum;
}
@Override
public void study(boolean win) {
if(win)
history[prevHandValue][currentHandValue]++;
else{
history[prevHandValue][(currentHandValue + 1) % 3]++;
history[prevHandValue][(currentHandValue + 2) % 3]++;
}
}
}
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]";
}
}
public class Main{
public static void main(String[] args) {
int seed1 = 314;
int seed2 = 15;
Player player1 = new Player("Kim", new WinningStrategy(seed1));
Player player2 = new Player("Lee", 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);
}
}
구조
Hand | 가위바위보에서 손을 나타내는 클래스 |
Strategy | 가위바위보의 '전략'을 나타내는 인터페이스 |
WinningStrategy | 이기면 다음에도 같은 손을 내는 전략의 클래스. Strategy의 구현체 |
ProbStrategy | 직전 손에서 다음 손을 확률적으로 계산하는 전략의 클래스. Strategy의 구현체 |
Player | 가위바위보를 하는 플레이어 클래스 |
메인메소드 실행 시 10,000번의 게임을 진행하고 가장 마지막에 A와 B의 승리, 패배 횟수가 출력된다.
Hand는 enum 클래스로 가위, 바위, 보 라는 세개의 enum 상수를 갖고 있으며, 필드로 name, handValue를 갖고 있다.
또한 isStrongerThan() 메소드를 통해 A와 B의 결과를 반환 할 수 있다.
각 전략에 대한 인터페이스인 Strategy가 존재한다.
그리고 전략에 대한 구체화를 표현한 WinningStrategy, ProbStrategy가 있다.
각 플레이어는 이름과 전략을 갖고 생성된다.
그래서 게임이 진행될때마다 각 플레이어는 자신의 전략에 따라 다음에 낼 손을 결정하게 된다.
Strategy 패턴의 구조는 아래와 같다.
1. Strategy - 전략을 이용하기 위한 인터페이스. 예제에서 인터페이스로 처리했지만 추상 클래스로 처리하는 것도 가능하다.
2. ConcreteStrategy - Strategy의 구현체. 여러개의 ConcreteStrategy로 다양한 전략이 가능하다.
3. Context - Strategy를 이용하는 주체. ConcreteStrategy의 인스턴스를 갖고 있다가 필요에 따라 이용한다. 예제에서의 Player 클래스.
마무리 정리는 조금 뒤로 미루고 헤드퍼스트 디자인 패턴 책에서의 예제도 정리한다.
예제 프로그램은 오리 시뮬레이터이다.
객체로는 일반 오리와 모형오리 두가지가 존재.
코드 먼저 정리한다.
이 예제에서는 패키지가 나눠져 있으니 패키지 구조도 확인.
//예제 출처 - 헤드퍼스트 디자인 패턴
package strategyPattern.headfirst.fly;
public interface FlyBehavioral {
public void fly();
}
package strategyPattern.headfist.fly;
public class FlyNoWay implements FlyBehavioral {
@Override
public void fly() {
System.out.println("날지 못해요");
}
}
package strategyPattern.headfist.fly;
public class FlyRocketPowered implements FlyBehavioral {
@Override
public void fly() {
System.out.println("로켓 추진!!!!!!!!!!!");
}
}
package strategyPattern.headfist.fly;
public class FlyWithWings implements FlyBehavioral {
@Override
public void fly() {
System.out.println("날고 있어요");
}
}
package strategyPattern.headfist.quack;
public interface QuackBehavioral {
public void quack();
}
package strategyPattern.headfist.quack;
public class MuteQuack implements QuackBehavioral {
@Override
public void quack() {
System.out.println("---");
}
}
package strategyPattern.headfist.quack;
public class Quack implements QuackBehavioral {
@Override
public void quack() {
System.out.println("꽥!");
}
}
package strategyPattern.headfist.quack;
public class Squeak implements QuackBehavioral {
@Override
public void quack() {
System.out.println("삑!");
}
}
package strategyPattern.headfirst;
public abstract class Duck {
FlyBehavioral flyBehavioral;
QuackBehavioral quackBehavioral;
public Duck() {
}
public void swim() {
System.out.println("모든 오리는 물에 뜹니다.");
}
public abstract void display();
public void perfomFly() {
flyBehavioral.fly();
}
public void performQuack() {
quackBehavioral.quack();
}
public void setFlyBehavioral(FlyBehavioral fb) {
flyBehavioral = fb;
}
public void setQuackBehavioral(QuackBehavioral qb) {
quackBehavioral = qb;
}
}
package strategyPattern.headfirst;
public class MallardDuck extends Duck {
@Override
public void display() {
System.out.println("물오리!");
}
public MallardDuck() {
quackBehavioral = new Quack();
flyBehavioral = new FlyWithWings();
}
}
package strategyPattern.headfirst;
public class ModelDuck extends Duck {
@Override
public void display() {
System.out.println("모형오리!");
}
public ModelDuck() {
flyBehavioral = new FlyNoWay();
quackBehavioral = new MuteQuack();
}
}
package strategyPattern.headfirst;
public class DuckSimulator {
public static void main(String[] args) {
// 물오리
Duck mallardDuck = new MallardDuck();
mallardDuck.display();
mallardDuck.performFly();
mallardDuck.performQuack();
System.out.println("------------");
Duck modelDuck = new ModelDuck();
modelDuck.display();
modelDuck.performFly();
modelDuck.setFlyBehavioral(new FlyRocketPowered());
modelDuck.performFly();
modelDuck.performQuack();
}
}
메인 메소드를 실행해보면
물오리!
날고 있어요
꽥!
----------
모형오리!
날지 못해요
로켓 추진!!!!!!!
---
이렇게 출력된다.
그럼 책에서의 예시를 정리해보자.
다양한 종류의 오리의 모습을 보여주고, 헤엄치고 울 수 있는 시뮬레이터를 만들었다.
그래서 Duck 이라는 클래스에 quack(), swim(), display() 메소드를 만들었다.
모든 오리는 꽥꽥 소리를 내고 헤엄을 칠 수 있기 때문에 display() 메소드만 추상 메소드로 만들었다.
그래서 각 오리 객체는 Duck 클래스를 상속받고, display() 메소드를 각각 구현하고 있다.
그런데 시뮬레이터를 서비스하다가 날아가는 것도 표현되었으면 좋겠다는 사용자 의견에 따라 날아 가는 기능을 추가하게 되었다.
그래서 Duck 클래스에 fly() 메소드를 추가하게 되었다.
시간이 조금 지나서 좀 더 다양한 객체를 추가하려고 러버덕을 추가하기로 했다.
러버덕 객체 역시 다른 오리 객체들과 마찬가지로 Duck을 상속받는 형태로 작성했다.
그랬더니??
날 수 없는 러버덕이 날아다녔다.
또한 러버덕은 꽥꽥 소리가 아닌 삑삑 이라는 소리가 나야 하는데 똑같이 꽥꽥 소리를 내며 울고 있었다.
그래서 러버덕 객체에서는 quack() 메소드와 fly() 메소드를 모두 오버라이드해 처리하는 방법을 생각했지만, 그 후 객체를 계속 추가하게 된다면 이것 역시 비효율적이라는 생각을 하게 된다.
그래서 분리를 생각하게 되었고, 공통적으로 모든 객체가 수행하는 기능인 swim()과 display() 메소드는 Duck 클래스에 남겨두고, fly()와 quack()은 각각의 인터페이스로 분리한 뒤 필요로 하는 클래스에서 인터페이스들을 확장해 구현하도록 처리한다.
여기까지가 책에서의 예시이다.
그럼 위와 같이 처리했을 때의 문제점으로는 무엇이 있을까?
바로 코드의 중복이다.
디자인패턴의 장점으로 유지보수성과 재사용성이 있다.
인터페이스로 분리하는것까지는 좋았지만, 인터페이스를 확장해 구현하는 모든 객체에서 동일한 기능을 하는 객체가 단 하나도 없다면 이러한 분리도 괜찮을 수 있을지도 모르겠다.
하지만, 가장 먼저 시뮬레이터에 생성한 객체인 실제 오리들의 날아가는 행동이 '날고 있어요' 라는 기능으로 동일하다면?
모든 해당 객체들에서는 인터페이스 구현으로 인해 '날고 있어요' 라는 기능의 코드가 작성되어 있을 것이다.
그럼 코드의 중복이 발생한다는 것이다. 중복이 발생한다는 것은 동일한 코드를 사용하고 있음에도 재사용을 하지 않고 새로운 코드를 사용한다고 할 수 있다.
또한, 이 '날고 있어요' 라는 기능을 '날고 있네요??' 라고 수정해야 하는 상황이 왔다면??
그리고 여기에 해당하는 객체들이 몇십 몇백개라면???
모든 객체들의 구현 코드를 수정해야 하는 상황이 발생하게 된다.
이 문제의 해결책이 예제이다.
분리까지는 좋은 방법이니 swim, display는 Duck에 그대로 두고 Quack과 Fly를 인터페이스로 만들어준다.
단, 이 인터페이스들을 각 객체에서 구현하는 것이 아닌 클래스 집합으로 문제를 해결한다.
예제 구조를 보면 FlyBehavioral 인터페이스를 구현하는 클래스는 FlyNoWay(날지 못해요), FlyRocketPowered(로켓 추진!!!!), FlyWithWings(날고 있어요) 이렇게 존재한다.
그리고 각 오리 객체에서는 이를 구현하지 않고 있으며, Duck 클래스에서는 각 인터페이스변수를 갖고 있게 된다.
각 객체는 Duck 클래스를 상속받고 있기 때문에 생성자를 통해 각 인터페이스 변수에 행동 클래스 인스턴스를 대입하게 된다.
그 코드가 각 객체 생성자에 있는 flyBehavioral = new FlyWithWings(); 코드이다.
그럼 객체가 생성되고 행동 처리를 하는 메소드를 호출하게 된다면 행동 클래스 인스턴스가 대입되어 있는 인터페이스 변수를 통해 기능을 수행하게 되는 것이다.
이렇게 수행했을때를 한번 살펴보면,
일단, 오리 객체들이 직접 fly()를 구현하고 있지 않기 때문에 코드의 중복이 발생하지 않는다.
동일하게 날아가는 모습을 표현하는 오리 객체가 200개가 존재한다고 했을 때,
이 객체들은 생성자를 통해 flyBehavioral에 행동 클래스 인스턴스를 담아주기만 하면 될 뿐이지 직접 '날고 있어요' 라는 기능의 코드를 작성할 필요가 없게 된다.
그럼 FlyWithWings()의 구현체는 재사용이 처리되고 있는 코드라고 볼 수 있고, 그로인해 중복이 제거되며 재사용성이 높아졌다고 볼 수 있게 된다.
다음은 유지보수 관점에서다.
각 객체에서 구현하는 경우 '날고 있어요'를 '날고 있네요??' 라고 수정하기 위해서는 200개의 객체를 모두 수정해야 했다.
하지만 이렇게 구현을 해두고 나면 FlyWithWing() 의 구현체만 수정한다면 문제가 해결된다.
그럼 200개의 메소드를 수정할 필요없이 단 한개의 메소드 수정으로 문제가 해결되는 것이다.
또한, 클래스 집합에 대한 디자인은 최대한 유연하게 만드는 것이 좋다.
메인 메소드를 보면 모형오리는 날지 못하는 것이 생성자를 통한 기본값이지만, setter를 통해 FlyRocketPowered 클래스 인스턴스를 담도록 처리한다.
그리고 모형오리를 날도록 해보면 로켓을 달아놓은 모형오리가 된다.
이렇게 유연하게 만들면 다양한 기능을 사용할 수 있도록 처리할 수 있다.
위 정리에서의 중점은 나는 행동과 우는 행동을 Duck(슈퍼 클래스)나 그 서브클래스에서 정의한 메소드를 사용해 구현하는 것이 아닌 다른 클래스에 '위임'한다는 것이다.
정리
두 책에서의 예제는 완전 다른 스타일의 예제였다.
JAVA 언어로 배우는 디자인패턴 입문 책의 예제는 전략 패턴 그 자체에 집중한 예제였다.
가위바위보를 수행하는 프로그램을 만들것이지만, 승리했을 때 이전에 냈던 손을 그대로 낸다는 전략(WinningStrategy)와 직전에 낸 손에서 다음 손을 확률적으로 계산해 낸다는 전략(ProbStrategy)가 존재하며 플레이어는 생성될 때 어떤 전략을 사용할 것인지를 결정하게 되고, 이 전략은 Strategy 라는 인터페이스를 통해 같은 메소드의 구현체로 전략을 구현한다.
그리고 플레이어는 생성될 때 전략을 받아 Strategy 인터페이스 변수에 전략을 담아두고 게임에 임하는 내내 해당 전략을 구사한다.
이렇게 구현한다면 각 전략을 플레이어 객체에서 구현하느라 여러 플레이어 객체를 생성해 구현할 필요가 없고 전략 인터페이스를 구현하는 집합체로 구현 클래스를 통한 전략 설계를 하면 된다.
헤드퍼스트 디자인 패턴에서는 전략 패턴은 캡슐화가 중요하다! 라는 느낌이었다.
아무래도 좀 더 다양한 기능에 대한 예제라 그렇지 않나 싶다.
시뮬레이터라는 프로그램에서의 기능을 전략이라고 볼 수 있다.
직관적으로 이게 전략이다! 라는 느낌은 없었지만 이렇게 캡슐화를 해야 코드의 재사용성과 유지보수성을 높일 수 있다 라는 것에 대해 이해하기는 더 좋았다.
두 책에서의 공통적인 중점.
전략패턴은 알고리즘을 캡슐화 해 각각의 알고리즘 수정에 다른 클래스를 수정해야 하는 경우가 발생하지 않도록 해야 한다.
구현체인 ConcreteStrategy를 수정하거나 추가로 생성하는 형태로 처리할 수 있으며, 위임이라는 약한 결합으로 알고리즘 수정이 용이하다.
'JAVA > Design Pattern' 카테고리의 다른 글
디자인패턴(Java) - 템플릿 메소드(Template Method) (0) | 2023.12.27 |
---|---|
디자인패턴(Java) - 어댑터(Adapter) (0) | 2023.12.16 |
디자인패턴(Java) - 반복자(Iterator) (1) | 2023.12.15 |
디자인패턴(Design Pattern)이란 (0) | 2023.12.15 |