Notice
Recent Posts
Recent Comments
«   2024/11   »
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
Archives
Today
Total
관리 메뉴

DeFacto-Standard IT

Strategy Pattern 본문

Design Pattern/Behavioral Pattern

Strategy Pattern

defacto standard 2017. 9. 26. 10:10

1. 가정


 - 로봇을 2개 만든다.

 - 로봇은 이동, 공격이라는 동작을 할 수 있다.

 - 로봇1은 이동으로는 도보, 공격으로는 펀치를 사용할 수 있다.

 - 로봇2는 이동으로는 비행, 공격으로는 미사일을 사용할 수 있다.



2. Naive Code


가장 먼저 생각할 수 있는 코드를 살펴보자.

 - 로봇 클래스 정의

 - name, move(), attack() 정의

 - Main method 정의



소스코드는 다음과 같다.

 - NaiveContext

public abstract class NaiveContext {
private String name;

public abstract void attack();
public abstract void move();

public String getName() {
return name;
}

protected void setName(String name) {
this.name = name;
}
}

로봇을 2개 만드므로 추상적 개념인 NaiveContext를 정의하였다.

로봇은 이동, 공격이 가능하므로 move()와 attack()이라는 추상메서드를 정의하였다.


 - NaiveConcreteContext1, 2

public class NaiveConcreteContext1 extends NaiveContext {

public NaiveConcreteContext1(String name) {
this.setName(name);
}

@Override
public void attack() {
System.out.println("Punch");
}

@Override
public void move() {
System.out.println("Walking");
}
}
public class NaiveConcreteContext2 extends NaiveContext {

public NaiveConcreteContext2(String name) {
this.setName(name);
}

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

@Override
public void move() {
System.out.println("Flying");
}
}


각 NaiveConcreteContext는 구체적인 개체(로봇)에 해당하며, 가정한 대로 펀치/도보, 미사일/비행 기능을 수행할 수 있다.


 - NaiveClient

public class NaiveClient {
public static void main(String args[]) {

NaiveContext naiveContext;

naiveContext= new NaiveConcreteContext1("Robot 1");
System.out.println(naiveContext.getName());
naiveContext.move();
naiveContext.attack();


naiveContext = new NaiveConcreteContext2("Robot 2");
System.out.println(naiveContext.getName());
naiveContext.move();
naiveContext.attack();
}
}

로봇1, 2를 만들고 이들에 대한 이름과 이동, 공격 메서드를 호출한다.


위 코드에서 여러 개의 ConcreteContext를 대표하는 Context란 클래스를 통해 추상화 하였고, 이에 따라 어떤 ConcreteContext가 바인딩 되어도 Context만 상속받았다면 구현한 대로 동작하게 된다.


따라서 로봇을 하나 더 만들고 싶다면 

1. 클래스를 정의

2. Context를 상속

3. move(), attack() 메서드 구현

만 한다면 로봇이 하나 추가된다. 



3. 문제점


Naive Code는 제대로 동작하지만, 적절한 코드는 아니다. 만약 다음과 같은 요구사항의 변경이 생긴다고 생각해보자.

 - 로봇은 공격 또는 이동 방식을 현재와 다른 방식으로 변경한다. ( ex. 도보에서 비행으로, 미사일에서 펀치로 변경)


현재 Naive Code에서, 변경된 요구사항을 적용하기 위해서는 다음 작업을 수행해야 한다.

 - NaiveConcreteContext1, 2의 move()와 attack()메서드의 내용을 각각 비행/미사일, 도보/펀치로 변경한다.


이는 OCP에 위배된다. OCP를 위배하면 다음과 같은 상황이 벌어질 수 있다.

1. 만약 이것을 로봇의 종류가 3천가지(NaiveConcreteContext1 ~ 3000)가 된다면, 일관성있게 전부 수정을 해야한다.

단 한가지라도 실수하여 잘못 고친다면 버그가 되는 것이다.


2. 만약 프로그램에서 미사일 공격 시 'Missile' 이라 라는 텍스트를 출력하던 것을,

'Missile Attack' 이라는 문자열로 단 7바이트를 고치기 위해 attack() 메서드의 내용이 'Missile'로 출력되는 모든 클래스를 전부 뒤져서 수정해야한다.


이는 소스코드의 중복으로 일어난 일이며, 위와 같이 해결하는 방법은 상당히 비효율적이다.



4. 해결방안


현재 문제가 되는 부분을 파악하고, 이를 클래스로 변경을 해야한다.


현재는 move()와 attack()메서드가 각 NaiveConcreteContext 클래스마다 하드코딩되어 있어서 발생하는 문제이다.

이를 해결하려면 동작에 관한 부분을 클래스로 정의할 필요가 있다.


이동에는 도보/비행 기능이 존재하며, 공격에는 펀치/미사일 기능이 존재한다.

위 코드에서, move()와 attack()의 내용을 클래스화 해야한다.


마치 Context와 ConcreteContext는 각각 추상화된 로봇과 구체적인 로봇인 것 처럼,

추상적 행동과 구체적 행동으로 나누는 것이다.

로봇의 경우는 IS-A 관계가 해당이 되므로 Abstract Class로 추상화 하였으나

move()와 attack()의 내용은 어떠한 동작에 해당하므로 Interface로 구현하는 것이 후에 더 유연한 코드로 변경할 수 있는 여지가 많다.


여기서 추상적인 동작을 'Strategy'라고 한다.

따라서 이동, 공격 이라는 추상적인 동작과

펀치/미사일/도보/비행 이라는 구체적인 동작을 구분하여 구현하도록한다.


Context(로봇)에는 이 2가지 종류의 동작(이동, 공격)을 레퍼런스로 가지고 있으며, Client에서는 이 추상적인 동작에 구체적인 동작을 바인딩 함으로써  "로봇.이동();" 과 같은 코드를 실행시 켰을 때, 원하는 결과를 도출해낼 수 있게 된다.


이러한 구조(위 4가지 구체적인 동작을 클래스로 분리)는, 이 외의 추가적인 구체적인 동작을 추가하기도 쉽다. 새로운 기능의 추가가 기존의 코드에 영향을 미치지 못하므로 OCP를 만족한다.


Context에서는 Strategy라는 추상적인 개념으로 레퍼런스를 가지고 있어 setStrategy(new ConcreteStrategy()) 와 같은 방식으로 바꿔치기할 수 있다.


즉, 외부에서 이동, 공격 방식을 임의대로 바꾸도록 해주는, Context 클래스의 Strategy1, 2에 대한 메서드(setter)를 통해 로봇의 이동, 공격 방식이 필요할 떄 바꿀 수 있도록 했다.


ConcreteStrategy를 원하는 때에 할당하기만 하면 ConcreteStrategy가 인자로 넘어가도,

Context 클래스의 입장에서는 Strategy가 넘어오므로 코드는 아무 변화가 없다.


이런 변경이 가능한 이유는,

상위 클래스(Context)의 메서드(move(), attack())를 상속받아 구현하는 상속 대신,

Context에 Strategy Interface에 대한 레퍼런스를 두어 setter를 사용해 ConcreteStrategy를 바인딩 받아 구성하는 집약 관계를 이용했기 때문이다.



5. Solution Code


4번 해결방안에서 말했던 코드는 다음과 같은 예로 기술될 수 있다.

 - Context

public abstract class Context {
private String name;
private Strategy1 strategy1;
private Strategy2 strategy2;

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

public String getName() {
return name;
}

public void move() {
strategy2.move();
}

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

public void setStrategy2(Strategy2 strategy2) {
this.strategy2 = strategy2;
}

public void setStrategy1(Strategy1 strategy1) {
this.strategy1 = strategy1;
}
}

Strategy1은 공격에 관련된 추상화된 동작, Strategy2는 이동에 관련된 추상화된 동작이다.

사실 네이밍을 하면서 Attack/MoveStrategy로 사용하는 것이 더 알맞다고 생각하였으나,

UML이나 Class Diagram등의 범용성을 고려하여 Strategy1, 2 등으로 기술하였다.

즉, 이 클래스는 변화를 처리하기 위해 추상적 동작인 Strategy 인터페이스를 포함해야 한다.


 - ConcreteContext1, 2

public class ConcreteContext1 extends Context {
public ConcreteContext1(String name) {
super(name);
}
}

public class ConcreteContext2 extends Context {
public ConcreteContext2(String name) {
super(name);
}
}


NaiveConcreteContext의 경우, 구체적인 클래스에 전부 메서드를 구현하였지만(move()과 attack()에 대한 Overriding),

위에서 말했듯이 이를 클래스로 다룰 것이고, Context에서 추상화된 동작인 Strategy1, 2가 있기 때문에 구체 클래스에서는 이를 더이상 기술할 필요가 없다.


 - Strategy1, 2

public interface Strategy1 {
public void attack();
}

public interface Strategy2 {
public void move();
}


공격과 이동이라는 추상적인 동작을 의미하는 Strategy이다. 동작을 의미하므로 인터페이스로 구현한다.


 - ConcreteStrategy1_1, 1_2

public class ConcreteStrategy1_1 implements Strategy1 {
public void attack() {
System.out.println("Missile");
}
}

public class ConcreteStrategy1_2 implements Strategy1 {
public void attack() {
System.out.println("Punch");
}
}

공격의 구체적인 동작인 미사일과 펀치에 대한 내용을 클래스로 분리시킨 것이다.


 - ConcreteStrategy2_1, 2_2

public class ConcreteStrategy2_1 implements Strategy2 {
public void move() {
System.out.println("Flying");
}
}

public class ConcreteStrategy2_2 implements Strategy2 {
public void move() {
System.out.println("Walking");
}
}

이동의 구체적인 동작인 비행과 도보에 대한 내용을 클래스로 분리시킨 것이다.


 - Client

public class Client {
public static void main(String[] args) {
Context context1 = new ConcreteContext1("ConcreteContext1");
Context context2 = new ConcreteContext2("ConcreteContext2");

context1.setStrategy1(new ConcreteStrategy1_1());
context1.setStrategy2(new ConcreteStrategy2_2());

context2.setStrategy1(new ConcreteStrategy1_2());
context2.setStrategy2(new ConcreteStrategy2_1());

System.out.println("ConcreteContext1's name is " + context1.getName());
context1.move();
context1.attack();

System.out.println();

System.out.println("ConcreteContext2's name is " + context2.getName());
context2.move();
context2.attack();
}
}



6. Strategy Pattern


전략(동작)을 쉽게 바꿀 수 있도록 해주는 패턴이다. 동작이란 어떤 목적을 달성하기 위해 일을 수행하는 방식, 알고리즘으로 이해하면 된다.


어떤 행동을 할지 구체적으로 기술한 ConcreteStrategy를 바로 쓰지 않고, 여러 ConcreteStrategy들을 Strategy Interface를 통해 캡슐화한다.


 같은 문제를 해결하는 여러 알고리즘(방식)이 클래스별로 캡슐화되어 있고, 이들이 필요할 때 교체할 수 있도록 함으로써 동일한 문제를 다른 알고리즘으로 해결할 수 있게 하는 디자인 패턴이다.


- UML & Sequence Diagram


 - Strategy : 인터페이스나 추상 클래스로, 추상화 된 동작을 의미.

 - ConcreteStrategy 1~3 : 구체화 된 동작을 클래스로 분리한 것.

 - Context : 추상화된 동작인 Strategy를 이용. 동적으로 구체적인 동작을 바꿀 수 있도록 setter가 필요.



7. 장단점


장점 : 상황에 따라 필요한 ConcreteStrategy를 할당함으로써 Context Class는 변화에 유연하게 대응할 수 있다.

단점 : 

'Design Pattern > Behavioral Pattern' 카테고리의 다른 글

Template Method Pattern (기본)  (0) 2017.09.26
Observer Pattern  (0) 2017.09.26
Command Pattern  (0) 2017.09.26
State Pattern과 Strategy Pattern의 공통점과 차이점  (0) 2017.09.26
State Pattern  (0) 2017.09.26
Comments