Command 패턴

5.디자인패턴 2016. 9. 18. 11:19

프로그래밍을 하다 보면 종종 사용자가 입력한 명령들을 기억해야 하는 경우가 있다. 매크로 커맨드를 만들어야 한다거나 undo / redo와 같이 사용자의 요청에 따라서 이미 수행한 명령을 취소하거나 다시 수행해야 할 필요가 있기 때문이다. 이럴 때 명령들을 객체화 하여 저장해 둠으로써 사용자에게 필요한 기능들을 제공해 주는 것이 Command 패턴이다.

Command 패턴의 이해는 우선 명령보다는 실제 다루고자 하는 객체로부터 시작된다. 우선 Shape 타입이 있고, 하위 타입인 Circle과 Rectangle이 다루려는 대상이다. Shape 타입은 draw() 메소드와 undraw() 메소드를 제공한다. 실제 하려고 하는 일은 이 두 메소드를 이용해서 도형을 그리거나 지우는 일이다.

Command 패턴에서 필요한 것은 Command를 객체화 하는 일이다. Shape을 다루기 위한 Command이므로 ShapeCommand라는 것을 만든다. 이 ShapeCommand는 위에서 정의한 Shape 타입에 대한 연산을 수행하는 객체이다. 예를 들어 ShapeCommand의 execute() 라는 메소드를 호출하면 Shape의 draw()가 호출되고, undo()라는 메소드를 호출하면 undraw()를 호출하는 식이다.

그리고 만약 Command들을 이용하여 사용자의 요청을 처리하고자 할 경우, 즉 사용자가 새로운 도형을 그리거나(execute) 이미 그린 객체를 지우거나(undo), 지웠던 객체를 다시 그리고자 할 때(redo) 명령 객체들을 저장해 두었다가 사용자의 요청에 따라 동작을 수행하는 객체가 필요하다. 아래 예제에서는 이를 CommandManager라는 이름으로 구현하였다.


Command 패턴의 클래스 다이어그램

우리가 다룰 대상 객체는 Circle과 Rectangle 객체이고, 이들을 공통 타입으로 묶기 위해서 Shape 인터페이스를 선언하였다. ShapeCommand는 Command 객체인데, 다양한 Command 객체가 구현될 수 있도록 ICommand 인터페이스를 선언하였다. ShapeCommand 객체는 대상이 되는 도형을 가지고 있고, 사용자의 명령에 따라 대상 객체를 조작한다. CommandManager 클래스는 사용자의 요청에 따라 생성된 Command 객체들을 저장하고 있다가 전체 실행(executeAll()), undo / redo와 같은 Command 객체 핸들링을 지원해주는 클래스이다.


Command 패턴의 구현

class CommandManager{

    private List<ICommand> undo = new ArrayList<ICommand>();

    private List<ICommand> redo = new ArrayList<ICommand>();

   

    public void execute(ICommand command){

        command.execute();

        undo.add(command);

    }

   

    public void executeAll(){

        for(ICommand command : undo){

            command.execute();

        }

    }

   

    public void undo(){

        ICommand command = undo.get(undo.size() - 1);

        command.undo();

        undo.remove(command);

        redo.add(command);

    }

   

    public void redo(){

        ICommand command = redo.get(redo.size() - 1);

        command.redo();

        redo.remove(command);

        undo.add(command);

    }

}

interface ICommand{

    public void execute();

    public void undo();

    public void redo();

}

class ShapeCommand implements ICommand{

    Shape shape;

    public void setShape(Shape shape){ this.shape = shape; }

    public void execute(){ shape.draw(); }

    public void undo(){ shape.undraw(); }

    public void redo(){ execute(); }

}

interface Shape{

    public void draw();

    public void undraw();

}

class Circle implements Shape{

    public void draw() {System.out.println("\tdraw Circle"); }   

    public void undraw() {System.out.println("\tundraw Circle"); }

}

class Rectangle implements Shape{

    public void draw() {System.out.println("\tdraw Rectangle"); }

    public void undraw() {System.out.println("\tundraw Rectangle"); }

}


실행 방법

public static void main(String[] args) {

    Scanner scan = new Scanner(System.in);

    int cmd;

    CommandManager manager = new CommandManager();

    do{

        System.out.println("1.execute");

        System.out.println("2.undo");

        System.out.println("3.redo");

        System.out.println("8.execute All");

        cmd = scan.nextInt();

   

        if(cmd == 1){

            System.out.println("Which on?");

            System.out.println("1.Circle");

            System.out.println("2.Rectangle");

            cmd = scan.nextInt();

            if(cmd == 1){

                ShapeCommand command = new ShapeCommand();

                command.setShape(new Circle());

                manager.execute(command);

            }

            else{

                ShapeCommand command = new ShapeCommand();

                command.setShape(new Rectangle());

                manager.execute(command);

            }

        }

        else if(cmd == 2){

            manager.undo();

        }

        else if(cmd == 3){

            manager.redo();

        }

        else if(cmd == 8){

            manager.executeAll();

        }

    }while(cmd != 9);

}

main() 메소드를 통해서 Command 패턴을 테스트 하는 방식은 다음과 같다. 먼저 대상 객체를 생성하는 명령은 1번 exetue이고, undo는 2번, redo는 3번을 키보드로 입력하면 된다. 여태 실행된 명령어들을 보기 위해서는 8번을, 종료할 때는 9번을 입력한다. 1번을 입력했을 경우에는 어떤 도형을 생성할지를 선택해야 한다. 1번은 Circle을 2번은 Rectangle을 생성한다.

도형을 몇 개 생성하고 나서부터는 자연스럽게 undo와 redo를 수행해보고 결과를 execute all 명령을 통해 확인해 볼 수 있다.

CommandManager를 조금씩 확장시켜 나가면서 Macro(특정 시점 이후에 입력된 여러 명령어들을 모아서 연속으로 실행 시키는 것)를 만드는 것도 가능하다.

'5.디자인패턴' 카테고리의 다른 글

Mediator 패턴  (0) 2016.09.18
Facade 패턴  (0) 2016.09.18
Flyweight 패턴  (0) 2016.09.18
Chain Of Responsibility 패턴  (0) 2016.09.17
Composite 패턴  (0) 2016.09.17
Posted by 이세영2
,

Flyweight 패턴

5.디자인패턴 2016. 9. 18. 07:48

Flyweight 패턴은 비용이 큰 자원을 공통으로 사용할 수 있도록 만드는 패턴이다. 자원에 대한 비용은 크게 두가지로 나눠 볼 수 있다.

1. 중복 생성될 가능성이 높은 경우.

중복 생성될 가능성이 높다는 것은 동일한 자원이 자주 사용될 가능성이 매우 높다는 것을 의미한다. 이런 자원은 공통 자원 형태로 관리하고 있다가 요청이 있을 때 제공해 주는 편이 좋다.

2. 자원 생성 비용은 큰데 사용 빈도가 낮은 경우.

이런 자원을 항상 미리 생성해 두는 것은 낭비이다. 따라서 요청이 있을 때에 생성해서 제공해 주는 편이 좋다.

이 두가지 목적을 위해서 Flyweight 패턴은 자원 생성과 제공을 책임진다. 자원의 생성을 담당하는 Factory 역할과 관리 역할을 분리하는 것이 좋을 수 있으나, 일반적으로는 두 역할의 크기가 그리 크지 않아서 하나의 클래스가 담당하도록 구현한다.


Flyweight 패턴의 클래스 다이어그램

Flyweight 패턴의 구현

class Flyweight{

    Map<String, Subject> map = new HashMap<String, Subject>();

   

    public Subject getSubject(String name){

        Subject subject = map.get(name);

        if(subject == null){

            subject = new Subject(name);

            map.put(name, subject);

        }

        return subject;

    }

}

class Subject{

    private String name;

    public Subject(String name){

        this.name = name;

        System.out.println("create : " + name);

    }

}


사용 방법

public static void main(String[] args) {

    Flyweight flyweight = new Flyweight();

    flyweight.getSubject("a");

    flyweight.getSubject("a");

    flyweight.getSubject("b");

    flyweight.getSubject("b");

}

구현의 내용은 단순하다. Flyweight 클래스는 관리해야 할 자원인 Subject에 대한 생성과 제공을 담당한다. 외부에서 특정 명칭(name)의 자원을 getSubject() 메소드를 통해 요청해 오면 우선 이미 생성된 자원인지를 검사한다. 그리고 이미 생성되어 있었으면 기존의 자원을 제공하고, 생성되지 않은 자원은 생성을 하여 자신의 map에 저장하고 난 후에 제공해 준다. 이 과정을 통해서 Flyweight 패턴이 중복된 자원의 생성을 관리할 수 있다.


또 다른 예제(Java 라이브러리 내의 Flyweight 패턴)

Flyweight 패턴은 실제 여러 곳에서 사용된다. 쓰레드 풀이나 객체 재사용 풀도 일종의 Flyweight 패턴이다. Java 라이브러리들 중에서도 이를 사용하는데, 매우 사용 빈도가 높은 Integer 클래스에도 이와 같은 패턴이 적용되어 있다. 아래는 Integer 클래스에서 사용하는 Flywight 패턴의 코드이다.

private static class IntegerCache {

    static final int low = -128;

    static final int high;

    static final Integer cache[];

    static { // static으로 실행되기 때문에 실행 이전에 생성이 완료됨.

        // high value may be configured by property

        int h = 127;

        String integerCacheHighPropValue =

            sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");

        if (integerCacheHighPropValue != null) {

            try {

               int i = parseInt(integerCacheHighPropValue);

               i = Math.max(i, 127);

               // Maximum array size is Integer.MAX_VALUE

               h = Math.min(i, Integer.MAX_VALUE - (-low) -1);

           } catch( NumberFormatException nfe) {

               // If the property cannot be parsed into an int, ignore it.

           }

        }

        high = h;

        cache = new Integer[(high - low) + 1]; // Flyweight 생성 부분

        int j = low;

        for(int k = 0; k < cache.length; k++)

            cache[k] = new Integer(j++);

        // range [-128, 127] must be interned (JLS7 5.1.7)

        assert IntegerCache.high >= 127;

    }

    private IntegerCache() {}

}

public static Integer valueOf(int i) {  // Flyweight 객체 제공 부분

    if (i >= IntegerCache.low && i <= IntegerCache.high)

        return IntegerCache.cache[i + (-IntegerCache.low)];

    return new Integer(i);

} 

이 소스에서는 IntegerCache라는 static 클래스를 통해서 Integer의 일정 범위를 미리 생성해 둔다. 전체 범위는 VM(Virtual Machine)에 따라서 달라질 수 있음을 알 수 있다. 하지만 보통은 -128에서 127까지 범위의 Integer 클래스를 배열 형식으로 만들어 둔다. 그리고 valueOf() 메소드가 호출 되었을 때 요청된 Integer 값이 -128에서 127 사이라면 이미 생성된 Integer 객체를 반환해 준다. 이 코드는 jre1.8.0_91 기준으로 Integer 클래스의 780 ~833 번째 라인에 들어 있는 코드이다.

'5.디자인패턴' 카테고리의 다른 글

Facade 패턴  (0) 2016.09.18
Command 패턴  (0) 2016.09.18
Chain Of Responsibility 패턴  (0) 2016.09.17
Composite 패턴  (0) 2016.09.17
Iterator 패턴  (0) 2016.09.17
Posted by 이세영2
,

Chain Of Responsibility 패턴은 책임의 사슬이라고 번역할 수 있는 패턴이다. 여러 객체가 각기 다른 객체의 맴버 객체로 연결이 되어 있고(그 구조는 상관이 없다. 선형일 수도 있고 트리일 수도 있다.), 어떤 작업에 대한 요청이 발생했을 때 스스로 해결할 수 있을 경우에만 그 작업에 대해 직접 수행하고, 그렇지 않은 경우 맴버 객체에게 작업을 넘긴다.

패턴의 구조가 명확하지 않은 만큼 구현의 방법 또한 다양하다. 일반적으로는 사슬을 구성하는 상위 타입은 동일하게 가져가고, 개별 동작은 하위 타입이 구현하는 형태를 띈다. 하지만 하위 타입 구현 없이 동일한 타입만으로 사슬을 구성하는 것이 가능하고, Composite 패턴과 같이 트리 형태의 사슬을 구성하는 것 또한 가능하다.


Chain Of Responsibility 패턴 클래스 다이어그램

Chain Of Responsibility 패턴의 구현

abstract class Boundary{

    protected int upper;

    protected int lower;

   

    protected Boundary nested = null;

    public void setNested(Boundary nested){ this.nested = nested; }

   

    public Boundary(int upper, int lower){

        this.upper = upper;

        this.lower = lower;

    }

               

    public void action(int value){

        if(isInBoundary(value) == true) individualAction();

        else if(nested != null) nested.action(value);

        else individualAction();

    }

    abstract protected void individualAction();

   

    private boolean isInBoundary(int value){

        if(value >= lower && value <= upper) return true;

        return false;

    }

}

class NormalVoltage extends Boundary{

    public NormalVoltage(int upper, int lower){

        super(upper, lower);

    }

   

    protected void individualAction(){

        System.out.println("normal operation");

    }

}

class WarningVoltage extends Boundary{

    public WarningVoltage(int upper, int lower){

        super(upper, lower);

    }

   

    protected void individualAction(){

        System.out.println("warning operation");

    }

}

class FaultVoltage extends Boundary{

    public FaultVoltage(int upper, int lower){

        super(upper, lower);

    }

   

    protected void individualAction(){

        System.out.println("fault operation");

    }

}

우선 책임의 사슬 패턴을 위해서 사슬을 구성하는 Boundary라는 상위 클래스를 선언한다. 이 클래스는 책임 사슬을 구성할 수 있도록 setNested() 메소드를 제공한다. 이 메소드는 동일한 Boundary 객체를 받아 맴버 객체로 설정해 준다. 만약 어떤 객체가 작업을 수행할 조건에 맞지 않으면 맴버 객체로 설정된 Boundary 객체에게 작업을 위임한다.

하위 클래스에는 3종류가 있다. 우선 정상 범위의 전압을 나타내는 NormalVoltage 클래스가 있고, 경고 상태와 고장 상태를 나타내는 WarningVoltage와 FaultVoltage 클래스가 있다. 이들 클래스는 각각 자신이 작업을 수행해야 할 경우에 호출될 individualAction() 메소드를 재정의 하고 있다.

아래 main() 메소드에서는 이들간의 관계를 설정하고 동작시키는 코드가 있다.


실행 방법

public static void main(String[] args) {

    Boundary voltage = new NormalVoltage(230, 210);

    Boundary warning = new WarningVoltage(240, 200);

    Boundary fault = new FaultVoltage(Integer.MAX_VALUE, Integer.MIN_VALUE);

    voltage.setNested(warning);

    warning.setNested(fault);

    voltage.action(220);

    voltage.action(235);

    voltage.action(245);

}

기본적으로 NormalVoltage 객체가 가장 바깥쪽에 있고, WarningVoltage 객체가 그 다음, 가장 안쪽에는 FaultVoltage 객체가 있다. action() 메소드를 통해 입력되는 입력 값을 최초로 받는 것은 가장 바깥쪽 객체인 NormalVoltage 객체이다. 이 객체는 상위 객체인 Boundary 객체가 정한 바와 같이 우선 입력된 값이 자신의 범위에 맞는지를 확인한다. 만약 맞으면 individualAction() 메소드를 호출하여 자신이 작업을 수행한다. 만약 맞지 않는다면 nested 객체가 있는지를 확인하고 있으면 작업을 위임하기 위해 nested 객체의 action() 메소드를 호출한다. 만약 nested 객체가 없다면 자신이 최종 작업 수행자이므로 자신의 individualAction() 메소드를 수행한다.


다시 이야기 하지만 이 예제는 일반적인 형태이긴 하나 꼭 하위 객체를 생성해야 하는 것은 아니다. 오히려 이 예제와 같은 경우라면 하위 객체를 생성하는 것보다는 Boundary 객체를 범용적으로 활용할 수 있도록 만드는 편이 좋다. 그렇게 하면 책임 사슬을 좀 더 유연하게 늘이거나 줄일 수 있다. 경고 레벨을 늘리고자 할 경우 그냥 Boundary 객체를 중간에 하나씩 추가해 주기만 하면 된다. 그러면 좀 더 촘촘한 간격으로 경고와 고장을 나타낼 수 있게 된다. 이렇게 하는 것이 Chain Of Responsibility 패턴을 사용하는 목적인 유연성을 좀 더 반영할 수 있을 것이다.

'5.디자인패턴' 카테고리의 다른 글

Command 패턴  (0) 2016.09.18
Flyweight 패턴  (0) 2016.09.18
Composite 패턴  (0) 2016.09.17
Iterator 패턴  (0) 2016.09.17
Enum Abstract Factory 패턴  (0) 2016.09.16
Posted by 이세영2
,