Command 패턴-명령을 클래스로 만든다

클래스가 일을 수행할 때 자신의 클래스나 다른 클래스의 메소드를 호출한다. 메소드를 호출한 결과는 객체 상태로 반영이 되지만, 일을 수행한 이력은 어디에도 남지 않는다. 만약 실행하고 싶은 일을 메소드를 호출하는 동적인 처리로서 표현하는 것이 아니라, 이 일을 실행하시오! 라는 명령을 나타내는 클래스의 인스턴스의 집합으로 관리하게 되면 똑같은 명령을 재실행할 수도 있고, 여러 개의 명령을 모아 새로운 명령으로도 재사용할 수 있다. 이와 같은 ‘명령’ 을 Command 패턴이라고 한다.

메소드(작업 요청)을 객체의 형태로 캡슐화 하는 것이 가장 큰 목적이다.

예) 간단한 그림 그리기 소프트, 마우스를 끌면 빨간 점이 연결되어 그림이 그려지고 clear 버튼을 누르면 점이 지워진다.

사용자가 마우스를 끌때마다 ‘이 위치에 점을 그려라’ 라는 명령이 DrawCommand 클래스의 인스턴스로 생성된다. 이 인스턴스를 저장했다가 필요에 따라서 재사용한다.

1 2

패키지 이름 해설
command Command ‘명령’을 표현하는 인터페이스
command MacroCommand ‘여러 개의 명령을 모은 명령’을 나타내는 클래스
drawer DrawCommand ‘그림 그리기 명령’을 표현한 클래스
drawer Drawable ‘그리기 대상’을 표현한 인터페이스
drawer DrawCanvas ‘그리기 대상’을 구현한 인터페이스
Anonymous Main 동작 테스트용 클래스


Command 인터페이스

‘명령’을 표현하기 위한 인터페이스

Command 인터페이스는 execute 메소드를 가지며, 호출했을때 구체적으로 일어나는 일은 Command 인터페이스를 구현한 클래스가 결정한다.


public interface Command {

    /**
     * 실행한다.
     */

    public abstract void execute();
}

MacroCommand 클래스

여러개의 명령을 하나로 모은 명령을 나타내는 클래스

Command 인터페이스를 구현하고 있다.

execute() : commands 필드에 보관되어 있는 인스턴스의 execute() 메소드를 실행

append() : MacroCommand 클래스에 새로운 Command를 추가

if문의 조건은 만약 자기 자신을 실수 add해버리면, execute 메소드는 영원히 끝나지 않기 때문에, 사용

(java.util.stack 클래스의 push 메소드는 인스턴스에 마지막 요소를 추가한다는 의미)

undo() : commands의 마지막 명령어를 사게하는 메소드, pop을 통해 요소를 꺼낸다음 인스턴스에서 제거

clear() : 모든 명령을 삭제하는 메소드


public class MacroCommand implements Command {

    /**
     * Command 인터페이스의 execute() 구현
     *
     * @param     commands  다수의 command를 모아두기 위한 필드
     */

    private Stack commands = new Stack();

    public  void execute() {
        Iterator it = commands.iterator();

        while(it.hasNext()) {
            ((Command)it.next()).execute();
        }
    }

    /**
     * MacroCommand 클래스에 새로운 Command를 추가 (Command를 구현한 클래스의 인스턴스 추가)
     */

    public void append(Command cmd) {
        if(cmd != this) {
            commands.push(cmd);
        }
    }

    /**
     * commands의 최후의 명령을 삭제
     */

    public void undo() {
        if(!commands.empty()) {
            commands.pop();
        }
    }

    /**
     * 모든 명령을 삭제
     */

    public void clear(){
        commands.clear();
    }
}

Drawable 인터페이스

그림 그리기 대상을 표현하는 인터페이스

draw() : 그림을 그리는 메소드


public interface Drawable {

    /**
     * 그림 그린다.
     */

    public abstract void draw(int x, int y);
}

DrawCanvas 클래스

Drawable 인터페이스를 구현하고 있는 클래스

history 필드 : 그림을 그리도록 하는 명령의 집합

DrawCanvas 생성자 : 폭, 높이와 그림 내용을 받아서 DrawCanvas인스턴스를 초기화한다.

paint 메소드 : DrawCanvas를 다시 그릴 필요가 생겼을때, history에 기록되어 있는 명령의 집합을 재실행 시킨다.

draw 메소드 : Drawable 인터페이스 구현을 위해 정의되어 있는 메소드로, 색을 지정하고 원을 표시한다.


public class DrawCanvas extends Canvas implements Drawable {

    /**
     * DrawCanvas 클래스의 생성자
     *
     * @param     color  그림 그리는 색
     * @param     radius  그림 그리기를 할 점의 반경
     * @param     history  그림을 그리도록 하는 명령의 집합 필드
     */

    private Color color = Color.red;
    private int radius = 6;
    private MacroCommand history;

    public DrawCanvas(int width, int height, MacroCommand history) {
        setSize(width, height);
        setBackground(Color.white);
        this.history = history;
    }

    /**
     * 이력 전체를 다시 그리기
     */

    public void paint(Graphics g) {
        history.execute();
    }

    /**
     * 그리기
     */

    @Override
    public void draw(int x, int y) {
        Graphics g = getGraphics();
        g.setColor(color);
        g.fillOval(x - radius, y - radius, radius * 2, radius * 2);
    }
}

DrawCommand 클래스

Command 인터페이스를 구현한 클래스로, 그림 그리기 명령을 표현한다.

DrawCommand 생성자 : Drawable 인터페이스를 구현한 클래스의 인스턴스와 Point 클래스의 인스턴스를 인수로 넘겨 필드에 대입한다. => 이 위치에 점을 그려라!! 라는 명령을 생성

execute() : drawable 필드의 draw 메소드를 호출함으로서, 명령을 실행한다.


public class DrawCommand implements Command {

    /**
     * DrawCommand 클래스의 생성자, 명령 생성
     *
     * @param     drawable  그림 그리기를 실행할 대상을 보관하는 필드
     * @param     position  그림 그리기를 행할 위치를 나타내는 필드
     */

    protected Drawable drawable;
    private Point position;

    public DrawCommand(Drawable drawable, Point position) {
        this.drawable = drawable;
        this.position = position;
    }

    /**
     * 명령 실행
     */

    @Override
    public void execute() {
        drawable.draw(position.x, position.y);
    }
}

Main 클래스

예제 프로그램을 작동시키기 위한 클래스

history 필드 : 그림의 이력을 보관

canvas 필드 : 그림을 그리는 영역 , 초기 사이즈로 400*400을 제공

clearButton 필드 : 그린 점을 지우는 제거 버튼

main생성자: 마우스 클릭 등의 이벤트를 받아들이는 리스너를 설정, 그림 그리기할 컴포넌트를 배치하고 있다.

  • 가로로 컴포넌트를 나열할 박스 buttonBox 생성
  • buttonBox 위에 clearButton 생성
  • 세로로 컴포넌트를 나열할 박스 mainBox 생성
  • mainBox위에 buttonBox와 canvas 나열
  • JFrame 위에 mainBox 나열 (getContentPane 컨테이너 이용)

actionPerformed() : ActionListener 인터페이스 구현하기 위한 것으로, clearButton이 눌러졌을때 그림 이력을 제거 후 다시 그리기 실행

mouseDragged() : MouseMotionListener 인터페이스를 구현하기 위한 것으로, 마우스를 끌었을때, 이 점을 그려라는 명령을 생성함

windowClosing() : WindowListener 인터페이스를 구현하기 위한 것으로, 종료 처리


public class HBNMain extends JFrame implements ActionListener, MouseMotionListener, WindowListener {

    /**
     * HBNMain 클래스의 생성자
     *
     * @param     history  그림 그리기 이력
     * @param     canvas  그림 그리기 영역
     * @param     clearButton  제거 버튼
     */

    private MacroCommand history = new MacroCommand();
    private DrawCanvas canvas = new DrawCanvas(400, 400, history);
    private JButton clearButton = new JButton("clear");

    public HBNMain(String title) {
        super(title);

        this.addWindowListener(this);
        canvas.addMouseMotionListener(this);
        clearButton.addActionListener(this);

        Box buttonBox = new Box(BoxLayout.X_AXIS);
        buttonBox.add(clearButton);
        Box mainBox = new Box(BoxLayout.Y_AXIS);
        mainBox.add(buttonBox);
        mainBox.add(canvas);
        getContentPane().add(mainBox);

        pack();
        setVisible(true);
    }

    /**
     * ActionListener 인터페이스 구현
     */

    @Override
    public void actionPerformed(ActionEvent e) {
        if(e.getSource() == clearButton) {
            history.clear();
            canvas.repaint();
        }
    }

    /**
     * MouseMotionListener 인터페이스 구현
     */

    @Override
    public void mouseDragged(MouseEvent e) {
        Command cmd = new DrawCommand(canvas, e.getPoint());
        history.append(cmd);        // 실행 이력에 추가
        cmd.execute();        // 실행
    }

    @Override
    public void mouseMoved(MouseEvent e) { }

    @Override
    public void windowOpened(WindowEvent e) { }

    /**
     * WindowListener 인터페이스 구현
     */

    @Override
    public void windowClosing(WindowEvent e) {
        System.exit(0);
    }

    @Override
    public void windowClosed(WindowEvent e) { }

    @Override
    public void windowIconified(WindowEvent e) { }

    @Override
    public void windowDeiconified(WindowEvent e) { }

    @Override
    public void windowActivated(WindowEvent e) { }

    @Override
    public void windowDeactivated(WindowEvent e) { }

    /**
     * Main 클래스의 인스턴스 생성 및 실행
     */

    public static void main(String[] args) {
        new HBNMain("Command Pattern Sample");
    }
}


Command 패턴에 등장하는 역할
  • Command(명령)의 역할

명령의 인터페이스(API)를 정의하는 역할

예) Command 인터페이스


  • ConcreateCommand(구체적인 명령)의 역할

Command 인터페이스를 실제로 구현하고 있는 역할

예) MacroCommand 클래스, DrawCommand 클래스


  • Receiver(수신자)의 역할

Command 명령을 실행할때 대상이 되는 역할, 명령을 받아들이는 사람!

예)DrawCanvas클래스


  • Client(의뢰자) 의 역할

ConcreateCommand를 생성하고, Receiver를 할당하는 역할

예) Main클래스- 마우스 끌기에 맞춰 DrawCommand 인스턴스 생성 및 DrawCanvas의 인스턴스를 생성자에게 전달


  • invoker(기동자)의 역할

명령을 처음 실행하는 역할, Command에서 정의되어 있는 인터페이스를 호출

예) Main 클래스, DrawCanvas 클래스 - Command 인터페이스의 execute를 호출


command pattern 활용
  • 요청을 로그에 기록하기

어떤 애플리케이션에서 명령을 실행하면서 디스크에 실행 히스토리를 기록해 애플리케이션이 다운되면,

command 객체를 다시 로딩하고 execute()메소드를 자동으로 실행하면 애플리케이션이 다운되었을경우, 복구할 수 있다.

ex) DB의 commit, rollback연산


참고자료