저작권 안내: 저작권자표시 Yes 상업적이용 No 컨텐츠변경 No

스프링5 입문

JSP 2.3

JPA 입문

DDD Start

인프런 객체 지향 입문 강의

필자는 요즘 안드로이드 코드를 만져보고 있다. 안드로이드를 공부하면서, 그리고 기존에 만들어 놓은 코드들을 보면서 그리고 책에 예제로 나와 있는 코드를 보면서 뭔가 정리하고 싶은 게 생겼는데, 그 부분은 바로 Handler의 사용과 관련된 부분이다. 특히 비동기로 서버에서 데이터를 읽어와 UI에 결과를 반영해주는 코드는 정말 최악인데, 이 때의 Handler 처리 과정을 정리해보면 다음과 같은 방식으로 이루어진다.




뭔가 복잡하다. 위 그림은 관련된 코드를 그림 하나로 표현한 것이어서 그나마 덜 복잡해 보이고는거고, 실제 코드에서는 handleMessage() 안에 상단히 많은 case 문이 위치하고, handler 코드와 someMethod() 사이에 많은 다른 코드들이 위치하게 된다. 그래서 버튼을 눌렀을 때 기능이 어떻게 동작하는지 확인하려면 코드를 이리 저리 탐색하면서 돌아다녀야 하고, 이는 코드 분석을 힘들게 만들어준다.


위 코드의 문제점들을 나열해보면 다음과 같은 것들이 있다.

  • 처리 결과를 Message를 이용해서 Handler에 전달할 때, 각 메시지를 구분하기 위해 정수값(message.what)을 사용한다. 
  • 비동기 방식 동작 코드에 Handler가 전달되어 불필요한 의존이 발생한다.
  • 무엇보다도 응집도가 떨어져 분석을 어렵게 만든다.

Handler는 결과에 따라 다른 행동을 하기 위해서 int 값을 사용한다. 아래 코드는 전형적인 Handler의 handleMessage() 메서드의 구현 모습이다.


Handler handler = new Handler() {

    public void handlerMessage(Message msg) {

        switch(msg.what) {

            case 0: // <--- 0 의 의미는?

                // msg.obj 사용

                // UI 처리 코드

                break;

            case 1: // <-- 새로운 조건 추가될 때 마다 case 비교 구문 추가

                // UI 처리 코드

            ...

            case SOME_CONSTANT: // <-- 새로운 조건 추가될 때 마다 상수 추가하는 번거로움

            ....

        }

    }

}


위 case에서 0과 1은 무엇을 의미할까? 의미를 부여하기 위해 상수를 사용할 수도 있다. 하지만, 그런다고 문제가 끝나는 것은 아니다. 새로운 조건이 추가되면 그 때 마다 case가 하나 추가되고 상수이름을 생각해서 추가해 주어야 한다. 실수도 서로 다른 상수가 같은 값을 갖게 되면 디버깅 하느라 (헛)수고 좀 해야 한다.


또 다른 문제는 비동기 처리를 위해 Handler 객체가 이곳 저곳으로 전달된다는 것이다. 예를 들어, HTTP 요청을 비동기로 보내고 그 결과를 받기 위해 HTTP 요청을 처리하는 객체에 Handler를 전달하고 (앞서 그림에서 과정1), 응답이 도착하면 Handler에 메시지를 보내는 방법으로 응답 결과를 UI에 반영한다. (앞서 그림에서 과정4와 과정5) HTTP 요청을 처리하는 객체는 사실 Handler와는 전혀 상관이 없는 객체이다. 그런데, 비동기로 들어오는 응답을 UI에 반영하기 위해 부득이 Handler 객체에 대한 의존이 발생하는 것이다.


Handler 객체가 날아다니기 시작할 때의 또 다른 문제점은 실제 handler.sendMessage()를 수행하는 Handler가 어떤 Handler인지 찾아다녀야 한다는 점이다. 예를 들어, HTTP 요청 처리 객체를 여러 액티비티에서 호출한다고 해 보자. 이 때, HTTP 요청 처리 객체 내부적으로 사용되는 handler가 어느 액티비티 객체의 멤버인지 확인하려면 코드를 탐색해야 하는 (헛)수고를 하게 된다.


낮아지는 응집도(cohesion)!


결국 앞에서 Handler의 사용으로 인해 발생하는 문제점은 코드의 응집도를 확 낮춰준다는 것이다. 이 말은 관련된 코드가 이곳 저곳으로 퍼진다는 것을 뜻하는데, 예를 들어, 뭔가 버튼 클릭시 서버에서 데이터를 읽어와 UI에 반영하는 코드는 다음과 같이 퍼지게 된다.

  • 버튼 클릭시 호출되는 메서드: HTTP 요청 처리 객체에 handler를 주고 비동기로 처리를 요청한다.
  • HTTP 요청 객체: 응답 처리 결과를 handler를 통해 전달한다.
  • handler의 handlerMessage() 메서드: case 문으로 메시지를 확인하고, 응답 처리 결과를 이용해서 UI를 조작한다.

뭔가 하나의 처리 흐름을 이해하기 위해 관련된 코드들이 위와 같이 세 곳에 퍼지게 된다. 이는 코드를 산만하게 이곳 저곳에 배치시켜서 가독성을 바닥으로 확 떨어뜨리고 유지보수 하기 어렵게 만들어준다. 게다가 Handler가 각각의 메시지를 구분할 수 있도록 나름대로 상수나 코드 값을 불필요하게 정의해 주어야 한다. (이런 거 은근 머리 아프다.)


커맨드와 콜백을 활용한 해결


앞에서 비동기 처리 상황에서 Handler를 사용하기 위해 발생했던 응집도 저하 문제를 어떻게 해결하면 좋을까 곰곰히 생각하다가, 다음의 두 가지를 생각해봤다.

  • 콜백
  • 커맨드

두 가지가 앞의 문제들을 어떻게 없애주는지 살펴보자.


1단계: 콜백 전달해서 호출해 달라고 요청하기


Handler가 이곳 저곳으로 전달되는 이유는 비동기로 처리한 결과를 UI로 받아야 하기 때문이다. 하지만, Handler가 다른 객체에 파라미터로 전달되는 순간부터 한 기능과 관련된 코드가 이곳 저곳으로 퍼지게 되고 이는 심각한 응집도 저하를 불러 일으킨다. 이를 방지하기 위한 한 가지 방법이 콜백을 사용하는 것이다. 우선, 다음과 같이 간단한 콜백 인터페이스를 만든다.


public interface AsyncCallBack<T> {

    public void call(T result);

}


이제 뭔가 비동기로 데이터를 읽어오는 코드가 위 콜백을 통해서 결과를 전달하도록 변경해 보자. 예를 들면, 아래와 같이 코드가 만들어질 것이다.


public class AsyncJob {

    public void loadSomeData(final AsyncCallBack<SomeData> callback) {

        new Thread() {

            // 데이터를 외부에서 로딩

           SomeData data = ....;

           callback.call(data);

        }.start();

    }

}


버튼이 클릭되었을 때 위 기능을 사용해야 한다면, 이제 Handler를 전달하는 대신 아래와 같이 콜백 객체를 전달하도록 코드가 바뀐다.


public void onClick(View v) {

    asyncJob.loadSomeData(

        new AsyncCallBack<SomeData> () {

            public void call(SomeData result) {

                // 여기서 뭔가 handler에 결과 전달함

                Message msg = handler.obtainMessage();

                msg.what = 0;

                msg.obj = result;

                handler.sendMessage(msg); // TODO: 아직도 데이터 로딩 후 UI 조작 코드가 분리됨 

            }

        }

    );

}


handler = new Handler() {

    public void handleMessage(Message msg) {

        switch(msg.what) {

            case 0:

                SomeData someData = (SomeData) msg.obj;

                // UI 조작

        }

    }

}


위와 같이 바꾸면, 이제 비동기 작업을 수행하는 코드에 Handler 객체를 전달할 필요가 없어진다. 위 코드의 경우 AsyncJob.loadSomeData() 메서드는 비동기로 데이터를 읽어오면 파라미터로 전달된 콜백 메서드를 호출한다. 즉, 콜백 메서드에서 Handler에 보낼 메시지를 생성하고 전달하게 되는 것이다.


하지만, 아직도 데이터를 읽어와서 UI를 변경한다는 하나의 작업과 관련된 코드가 onClick() 메서드와 Handler의 handleMessage() 메서드에 분리되어 있다. 이는 커맨드를 이용해서 해결할 수 있다.


2단계: 커맨드 패턴을 활용해서 Handler 처리 코드에서 what 제거하기


두 번째 과정은 커맨드를 사용하는 것이다. 우선 다음과 같은 간단한 커맨드 인터페이스를 정의한다.


public interface Command {

    void execute();

}


그리고, 다음과 같이 위 Command 만을 전문적으로 처리하는 CommandHandler 클래스를 정의한다.


public class CommandHandler {

    private Handler handler = new Handler() {

        public void handleMessage(Message msg) {

            if (msg.obj instanceof Command) {

                ((Command) msg.obj).execute();

            }

        }

    }

    public void send(Command command) {

        Message message = handler.obtainMessage();

        message.obj = command;

        handler.sendMessage(message);

    }

}


CommandHandler의 handler는 switch 문이 없다. 단지 Message를 통해서 전달받은 Command 객체의 execute() 메서드를 호출하는 것만 한다. CommandHandler의 send() 메서드는 Message에 Command 객체를 담아 handler에 전송한다.


이제 Handler 대신 CommandHandler를 사용하도록 코드를 변경해 보자. 


// 어떤 작업

final SomeData data = getSomeData();


// Command 전송

commandHandler.send(new Command() {

    public void execute() {

        // UI 변경

        changeUI(data);

    }

});


Handler를 사용하는 코드와 비교해 보자. 위와 같이 함으로써 다음과 같은 변화가 발생했다.

  • Handler.handleMessage() 메서드의 switch 문에서 사용할 상수 값 불필요
  • 작업 결과물을 UI에 반영하는 코드가 한 곳으로 모임
    • Handler를 사용하는 경우, Handler에 메시지를 보내는 코드와 Handler의 handleMessage() 메서드에서 분리되서 들어감
즉, 개발자는 Handler가 메시지를 구분하기 위해서 사용할 상수값(앞서 봤던 0, 1, SOME_CONSTANT 등)을 고민해서 만들 필요가 없고, 그 상수값을 맞출 필요가 없다. 그냥 알맞은 Command 구현 객체만 만들어서 전달해주면 된다.

그리고 응집도가 높아졌다. Handler를 사용하게 되면 데이터를 읽어오는 코드와 읽어온 데이터를 사용하는 코드가 분리되어 코드의 응집도가 떨어지고 이로 인해 코드 분석이나 변경이 어렵게 되는데, 위의 경우는 한 곳에 몰려 있어서 좀 더 빠르게 코드를 분석하고 이해할 수 있게 된다.

두 가지를 합친 결과 코드

두 가지를 합치면 앞서의 onClick() 메서드가 다음과 같이 바뀐다.

public void onClick(View v) {

    asyncJob.loadSomeData( // 1. 데이터 로딩 실행

        new AsyncCallBack<SomeData> () { 

            public void call(final SomeData result) { // 2. 로딩된 데이터 수신

                commandHandler.send(

                    new Command() {

                        public void execute() {

                            changeUI(result); // 3. 수신한 데이터를 이용 UI 변경

                        }

                    }

                );

            }

        }

    );

}


최초에 봤던 코드와 비교해 보자. 앞서 Handler를 전달하는 방식의 코드에서는 데이터가 처리되는 과정을 살펴보려면 이벤트 처리 코드(onClick과 같은), 비동기로 데이터 읽어오는 코드, 그리고 Handler 코드를 살펴봐야 했다. 또한, 그 과정에서 Handler가 각각의 메시지를 식별할 수 있도록 상수도 정의해 주어야 했다.

이랬던 것들이, 콜백과 커맨드를 사용함으로써 위 코드와 같이 응집도를 확 높일 수 있게 되었다. 관련 코드가 한 곳에 모여 있기 때문에 이제 데이터의 처리 과정을 확인하기 위해 이곳 저곳 흩어져 있는 코드를 살펴볼 필요가 없어 졌고, 메시지 구분 식별값 등 불필요한 고민을 하지 않아도 된다. 물론, 임의 객체 사용으로 인해 코드가 약간 복잡해 보이지만, 코드의 응집도가 낮아져서 코드 분석을 어렵게 하는 비용과 비교해보면 약간의 복잡한 코드가 비용이 (훨씬~~~) 작다.

자바에 클로저만 있었어도 더 간결한 코드를 얻을 수 있겠지만 지금은 이 정도만으로도 만족할 만한 코드 결과물을 얻을 수 있게 되었다.


관련자료



Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 하늘섬 2012.09.14 09:44 신고  댓글주소  수정/삭제  댓글쓰기

    이런한 경우에능 안드로이드 프래임웍에서 제공하는 AsyncTask를 사용하시면 매우 편리합니다.

  2. bluepoet 2013.03.19 19:54 신고  댓글주소  수정/삭제  댓글쓰기

    이번 스타리그 앱의 쓰레드작업 부분을 이걸로 적용해봐야겠네요.

    무엇보다 코드 가독성과 응집도가 높아진게 맘에 듭니다.

    좋은글 잘 읽었습니다^^

  3. 응집도 2013.04.23 12:28 신고  댓글주소  수정/삭제  댓글쓰기

    결합도를 낮추고 응집도를 높히는 작업을 하신듯 하네요~^^

커맨드 패턴이 무엇이며, 자바에서 어떻게 구현되는 지 알아본다.

커맨드(Command) 패턴

프로그래밍을 하다보면 사용자가 선택한(또는 입력한) 명령어에 따라 그에 알맞은 처리를 해야 할 때가 있다. 예를 들어, 워드프로세서를 생각해보자. 사용자들은 복사(copy), 잘라내기(cut), 붙여넣기(paste) 기능을 사용한다. 이 때 복사, 잘라내기, 붙여넣기 등은 모두 한번의 명령어에 해당한다. 사용자들은 메뉴나 툴바의 아이콘 또는 키보드 단축키를 사용함으로써 워드 프로세서에 이 명령들을 실행할 것을 요청하며, 워드 프로세서는 사용자가 전달한 명령어에 알맞은 기능을 실행한다. 그리고 대부분의 워드 프로세서는 사용자가 실행한 명령을 취소할 수 있는 '명령 취소(Undo)' 기능을 제공하고 있다. 이러한 취소 기능을 제공하기 위해서는 사용자가 실행한 명령어들을 순서대로 저장할 수 있어야 한다. 이처럼 명령어를 실행하고, 실행한 명령어를 저장하고, 실행한 명령을 취소하고, 재실행하고 또는 그러한 명령어를 처리할 때 사용되는 패턴이 바로 커맨드(Command) 패턴이다.

커맨드 패턴은 사용자가 요구하는 명령어를 객체에 캡슐화(encapsulation)하여 저장한다. 각각의 명령어에 해당하는 객체는 그 명령어에 해당하는 기능을 실행하며, 필요에 따라 '명령 취소' 기능을 제공한다. 명령어와 관련된 사항이 객체에 캡슐화되어 있기 때문에, 사용자들은 단순히 그 명령어 객체를 생성해서 사용하기만 하면 된다. 또한, 이러한 명령어 객체들은 모두 공동의 상위 클래스를 갖고 있다. 이 상위 클래스는 각 명령어 객체의 클래스가 구현해야 할 메소드를 정의하고 있다. 예를 들어, 워드 프로세서에서 사용되는 '복사', '잘라내기', '붙여넣기'에 해당하는 명령어 클래스를 각각 CopyCommand, CutCommand, PasteCommand 라고 하자. 그리고 이 세 클래스가 상속받는 추상 클래스를 AbstractCommand 라고 하자. 이 경우 전체적인 클래스 사이의 관계는 다음 그림과 같을 것이다.


AbstractCommand 클래스는 모든 명령어 클래스가 상속받아야 할 클래스로서 추상 메소드인 execute()와 undo()를 선언하고 있다. 메소드의 이름에서 알 수 있듯이 execute() 메소드는 명령어에 해당하는 기능을 실행하기 위해 호출되는 메소드이며, 따라서 execute() 메소드는 실제 기능을 구현하고 있다. 반면에 undo() 메소드는 '명령 취소'에 해당하는 기능을 제공한다. AbstractCommand 추상 클래스를 상속받는 클래스들은 그 클래스에 알맞도록 execute() 메소드와 undo() 메소드를 구현하면 된다. 예를 들어, CopyCommand 클래스의 execute() 메소드는 사용자가 설정한 블럭에 속한 글자들을 클립보드에 복사할 것이며, undo() 메소드는 클립보드에 저장되어 있는 글자들을 삭제할 것이다.

이제 남은 것은 각각의 명령어 객체를 저장하고 관리해주는 관리자 클래스인 CommandManager 클래스가 필요하다. 이 클래스는 실행되는 명령어를 차례대로 저장하고 있으며, 사용자가 '명령취소'를 요청할 경우 가장 최근에 저장된 명령어 객체의 undo() 메소드를 호출해주는 역할을 한다. 즉, 전체적인 클래스 사이의 관계는 다음과 같다.

 
여기서 Invoker는 워드프로세서의 경우 사용자가 메뉴를 선택하거나 툴바를 클릭했을 때 발생하는 이벤트를 처리해주는 이벤트 리스너(예를 들어, ActionListener나 ItemListener 등)가 되며, ConcreteCommand 클래스는 CopyCommand와 CutCommand와 같이 실제 명령을 실행(구현)하는 명령어 객체를 나타낸다. CommandManager는 AbstractCommand를 관리하는 클래스이다.

커맨드 패턴의 구현

이제 커맨드 패턴이 실제로 어떻게 구현되는 지 살펴보자. 가장 먼저 AbstractCommand 추상 클래스의 코드를 살펴보자.

public abstract class AbstractCommand {
    public final static CommandManager manager = new CommandManager();
    
    /**
     * 이 객체가 캡슐화하고 있는 명령을 수행한다.
     */
    public abstract boolean execute();
    /**
     * execute()를 통해서 수행된 작업을 취소한다.
     */
    public abstract boolean undo();
}

위 코드를 살펴보면 CommandManager의 인스턴스를 static 필드로 갖고 있는 것을 알 수 있다. 이는 Invoker가 단순히 ConcreteCommand 클래스의 인스턴스를 생성만 하면 되도록 하기 위해서이다. 이에 대한 내용은 ConcreteCommand 클래스의 구현 방법을 알아볼 때 설명하기로 한다. 위 코드에서 execute()와 undo() 메소드는 모두 boolean을 리턴값으로 갖는다. execute() 메소드는 실제 명령이 올바르게 수행된 경우 true를 리턴하고, 그렇지 않은 경우 false를 리턴한다. 비슷하게 undo() 메소드는 '명령 취소'가 성공했을 경우 true를 리턴하고, 그렇지 않을 경우 false를 리턴한다.

이제 ConcreteCommand 클래스가 어떻게 구현되었는 지 살펴보자. 예를 들어, PasteCommand 클래스의 경우 다음과 같은 코드를 가질 것이다.

public class PasteCommand extends AbstractCommand {
    ....
    public PasteCommand(Document document, int position) {
        this.document = document;
        this.position = position;
        ....
        manager.executeCommand(this);
    }
    public boolean execute() {
        try {
            document.insertStringCommand(position, pasteString);
        } catch(Exception ex) {
            return false;
        }
        return true;
    }
    public boolean undo() {
        try {
            document.deleteStringCommand(position, pasteString.length() );
        } catch(Exception ex) {
            return false;
        }
        return true;
    }
}

위 코드를 보면 PasteCommand 클래스는 AbstractCommand 클래스에 있는 두 추상 메소드인 execute()와 undo()를 구현한 것을 알 수 있다. 특이할 만한 점은 PasteCommand의 생성자이다. 생성자의 마지막 줄을 보면 CommandManager의 executeCommand() 메소드를 호출하는 것을 알 수 있다. 즉, 명령어 객체를 사용하는 객체들은 CommandManager에 대한 자세한 내용을 알 필요 없이 단순히 명령어 객체를 생성하기만 하면 된다. 예를 들어, '복사' 메뉴와 관련된 java.awt.MenuItem을 생각해보자. 이 MenuItem과 관련된 코드 부분은 다음과 같을 것이다.

Menu menu = new Menu ("편집");
MenuItem pasteMenuItem = new MenuItem("붙여넣기");
menu.add(pasteMenuItem);
pasteMenuItem.addActionListener(new PasteActionListener());

위 코드는 전형적인 메뉴 생성 방법을 나타내고 있다. 위 코드에서 마지막 줄에 있는 PasteActionListener는 ActionListener를 implements한 클래스로서 다음과 같다.

public class PasteActionListener implements ActionListener {

    public void actionPerformed(ActionEvent e) {
        // 현재 document와 position을 구한다.
        .....
        new PasteCommand(document, position); // 명령어 객체 생성
    }
}

이제 사용자들이 메뉴에서 "붙여넣기"를 클릭하면 PasteActionListener의 actionPerformed() 메소드가 호출되고, 이어서 PasteCommand 객체가 생성된다. 그러면 PasteCommand 클래스의 생성자에서 CommandManager의 executeCommand() 메소드를 호출하게 된다. 지금까지의 코드 만으로도 실제 사용자의 입력을 받는 MenuItem 객체와 실제 내부적으로 "붙여넣기"를 처리하는 PasteCommand 객체와 상관이 없다는 것을 알 수 있다. 이제 CommandManager 클래스를 살펴보자. CommandManager 클래스는 다음과 같다.

class CommandManager {
    private static final int MAX_HISTORY_LENGTH = 50;

    private LinkedList history = new LinkedList();
    private LinkedList redoList = new LinedList();

    // 인자로 받은 AbstractCommand를 실행한다.
    // 만약 command가 Undo나 Redo의 인스턴스일 경우에는
    // 각각 '명령 취소'와 '취소 명령 재실행'을 수행한다.
    public void executeCommand(AbstractCommand command) {
        if (command instanceof Undo) {
            undo();
            return;
        }
        if (command instanceof Redo) {
            redo();
            return;
        }
        if (command.execute()) {
            addToHistory(command);
        } else { // execute()가 false를 리턴한 경우, 즉 명령어가 올바르게 실행되지 않은 경우
            // 명령어가 실패했다는 것을 알린다.
        }
    }
    // 바로 이전에 실행한 명령어를 취소한다.
    private void undo() {
        if (history.size() > 0 ) { // 사용자가 실행한 명령어가 있을 경우
            AbstractCommand undoCommand = (AbstractCommand) history.removeFirst();
            undoCommand.undo();
            redoList.addFirst(undoCommand);
        }
    }
    // 바로 이전에 취소한 명령어를 다시 실행한다.
    private void redo() {
        if (redoList.size() > 0) {
            AbstractCommand redoCommand = (AbstractCommand) redoList.removeList();
            redoCommand.execute();
            history.addFirst(redoCommand);
        }
    }
    // history에 사용자가 실행한 명령어를 저장한다.
    private void addToHistory(AbstractCommand command) {
        history.addFirst(command);
        if (history.size() > MAX_HISTORY_LENGTH)
            history.removeLast();
    }
}

먼저 CommandManager 클래스가 가지고 있는 첫번째 필드인 history는 사용자가 입력한 명령어를 순서대로 저장하는 리스트이며, 두번째 필드 redoList 는 사용자가 '실행 취소'한 것을 저장하는 리스트이다. undo() 메소드와 redo() 메소드는 실제로 '명령 취소'와 '취소 명령 재실행'을 수행하고, addToHistory() 메소드는 최근에 수행한 명령어를 history에 수행한다.

이제, CommandManager 클래스의 메소드 중에서 실제로 다른 객체들이 사용하는 executeCommand() 메소드를 살펴보자. 이 메소드는 명령어 객체(즉, AbstractCommand를 상속받은 클래스)를 파라미터로 넘겨 받고 그 command를 실행한다. 여기서 command.execute() 메소드를 실행하는 부분을 살펴보자.

        if (command.execute()) {
            addToHistory(command);
        } else { // execute()가 false를 리턴한 경우, 즉 명령어가 올바르게 실행되지 않은 경우
            // 명령어가 실패했다는 것을 알린다.
        }

여기서 AbstractCommand의 execute() 메소드는 boolean을 리턴하며(기억이 안 난다면, 앞에 있는 AbstractCommand 클래스의 소스 코드를 살펴보라), 만약 true를 리턴하며 addToHistory() 메소드를 사용하여 history 필드에 추가하고, false를 리턴하면 명령어가 실패했음을 알린다.

CommandManager 클래스에서 아직까지 설명하지 않은 것이 있다면 Undo와 Redo이다. Undo와 Redo는 인터페이스이며, 그 정의는 단순히 다음과 같다.

interface Undo {
}

interface Redo {
}

즉, Undo와 Redo는 Serializable 인터페이스와 마찬가지로 단순히 이 두 인터페이스를 implements 한 클래스의 타입이 Redo와 Undo라는 것을 보여준다. '명령 취소'와 관련된 명령어 클래스는 UndoCommand 이며 다음과 같다.

class UndoCommand extends AbstractCommand implements Undo {
    public UndoCommand() {
        manager.execute(this);
    }
    public boolean execute() { return false; }
    public boolean undo() { return false; }
}

RedoCommand 역시 UndoCommand와 비슷하다. 실제로 사용자가 '명령 취소'를 클릭하면 이를 처리하는 이벤트 리스너는 단순히 다음과 같은 코드를 실행하면 된다.

new RedoCommand();

커맨드 패턴을 사용함으로써 사용자로부터 요청을 받는 객체(즉, 메뉴나 툴바)와 실제로 사용자가 필요로 하는 기능을 구현한 객체를 완전히 분리할 수 있게 되었다. 따라서 사용자로부터 요청을 받는 객체는 실제 내부 로직이 어떻게 되는 지 알 필요가 없으며, 내부 로직을 구현한 명령어 객체 역시 사용자로부터 어떻게 요청을 받는 지 알 필요가 없다. 즉, 이 두 객체 사이에 의존관계가 없는 것이다. 따라서 요청을 받는 객체를 변경할 필요 없이 손쉽게 새로운 명령어 객체를 추가할 수 있다.

또 다른 구현 방법

여기서 AbstractCommand가 추상 클래스로 선언된 것은 static final 필드로 CommandManager를 갖고 있기 때문이다. 실제로 명령어 객체를 순서대로 저장하고 명령어 객체의 실행을 취소하는 것 등의 작업이 필요하지 않다면 CommandManager가 필요 없을 것이다. 이런 경우 추상 클래스 대신 인터페이스를 사용하여 AbstractCommand를 대신할 수 있다. 만약 AbstracCommand 대신 사용할 인터페이스가 CommandIF 라고 한다면, CommandIF 인터페이스는 다음과 같이 정의될 것이다.

public interface CommandIF {
    public boolean execute();
}

이제, 명령어 클래스들은 AbtractCommand를 상속받는 대신 CommandIF 인터페이스를 구현할 것이다. 예를 들어, PasteCommand 클래스의 경우 다음과 같이 바뀔 것이다.

public class PasteCommand implements CommandIF {
    .....
    public PasteCommand(Document document, int position) {
        this.document = document;
        this.postion = position;
        // CommandManager와 관련된 부분이 없다!!
    }
    public boolean execute() {
        .....
    }
}

이제, PasteCommand 클래스를 사용하는 PasteListener 클래스의 actionPerformed() 메소드는 다음과 같이 변경된다.

public class PasteActionListener implements ActionListener {

    public void actionPerformed(ActionEvent e) {
        // 현재 document와 position을 구한다.
        .....
        CommandIF command = new PasteCommand(document, position); // 명령어 객체 생성
        command.execute(); // 명령어 실행
    }
}

직접 execute() 메소드를 호출해주는 것을 알 수 있다.

CommandFactory를 이용하여 객체간의 관련성 감소시키기

앞에 있는 PasteActionLister 클래스는 AbstractCommand 클래스를 사용하는 경우나 CommandIF 인터페이스를 사용하는 경우 모두 actionPerformed() 메소드에서 반드시 PasteCommand 클래스를 사용하고 있다. 이제 CopyActionListener 클래스를 생각해보자. 이 클래스는 CopyCommand 클래스를 사용한다는 것을 제외하면 PasteActionListener 클래스와 완전히 동일하다. CutActionListener 역시 마찬가지이다. 그렇다면 어떻게 하면 될까? 가장 먼저 생각할 수 있는 것이 이러한 ActionListener를 다음과 같이 하나로 묶어주는 것이다.

public class CommonActionListener implements ActionListener {

    public void actionPerformed(ActionEvent e) {
        MenuItem mi = (MenuItem)e.getSource();
        CommandIF command = null;
        if (mi.getLabel().equals("복사") ) {
            command = new CopyCommand(...);
            command.execute();
        } else if (mi.getLabel().equals("붙여넣기") ) {
            command = new PasteCommand(...);
            command.execute();
        } else if (mi.getLabel().equals("잘라내기") ) {
            command = new CutCommand(...);
            command.execute();
        }
    }
}

물론, 이것만으로도 MenuItem과 실제 명령어 객체(CommandIF 인터페이스를 구현한 객체 또는 AbstractCommand 추상클래스를 상속받은 객체)와의 관련성을 없앤 채로 CommonActionListener를 사용하여 이 두 객체를 연결시킬 수 있다. 이를 좀더 객체지향적으로 접근한 것이 바로 CommandFactory 클래스이다. CommandFactory 클래스는 CommonActionListener 클래스의 actionPerformed() 메소드에서 if .. else .. 부분을 처리해주며 코드 형태는 다음과 같다.

public class CommandFactory {
    public CommandIF createCommand(String key) {
        CommandIF command = null;
        if (key.equals("복사") ) {
            command = new CopyCommand(...);
        } else if (key.equals("붙여넣기") ) {
            command = new PasteCommand(...);
        } else if (key.equals("잘라내기") ) {
            command = new CutCommand(...);
        }
        return command;
    }
}

실제로, CommandFactory 클래스는 팩토리(Factory) 패턴을 간단하게 구현한 것으로서 팩토리 패턴에 대한 자세한 내용은 패턴 관련 서적을 참조하기 바란다. 이제 CommandFactory 클래스를 사용하면 CommonActionListener는 다음과 같이 간단하게 바뀌게 된다.

public class CommonActionListener implements ActionListener {
    
    private CommandFactory factory = new CommandFactory();
    
    public void actionPerformed(ActionEvent e) {
        MenuItem mi = (MenuItem)e.getSource();
        CommandIF command = factory.getCommand(mi.getLabel());
        command.execute();
    }
}

CommonActionListener 클래스의 소스 코드가 간단해지긴 했지만, 왜 굳이 CommandFactory 클래스를 생성하는 지 의아해 할지도 모르겠다. 위만 보더라도 CommonActionListener에서 모든 걸 다 처리할 수 있기 때문이다. 하지만, 메뉴 뿐만 아니라 단축키를 통해서 사용자들에게 "복사", "잘라내기", "덧붙이기" 기능을 제공하길 원한다고 가정해보자. 이 경우, CommandFactory 객체를 사용하지 않는다면 키보드 이벤트를 처리하는 클래스는 다음과 같은 형태를 취할 것이다.

public class ShortCutListener implements KeyListener {
    public void keyReleased(KeyEvent e) {
        int keyCode = e.getKeyCode();
        int modifier = e.getModifiers();
        CommandIF command = null;
        if (keyCode == .. && modifier ... ) {
            command = new CopyCommand(...);
        } else if (...) {
            command = new CopyCommand(...);
        } else {
            command = new CopyCommand(...);
        }
        command.execute();
    }
    ....
}

ShortCutListener 클래스가 CommonActionListener 클래스와 거의 비슷한 것을 알 수 있다. 즉, 코드가 중복되는 것이다. 이 경우 Command 클래스가 추가되면 ShortCutListener 클래스와 CommonActionListener가 모두 영향을 받게 된다. 또한, CopyCommand 클래스 대신 Copy2Command 클래스를 사용하는 경우에도, 이 두 Listener 클래스는 영향을 받게 된다. 반면에 CommandFactory 객체를 사용하게 되면, Command 클래스가 추가되거나 변경되거나 삭제된다고 해도 오직 CommandFactory 클래스만 영향을 받게 된다. 즉, 두 Listener 클래스는 오직 CommandIF 인터페이스와 CommandFactory 클래스만 알면 될 뿐, 실제 Command 객체들에 대한 자세한 내용을 알 필요가 없는 것이다. 다시 말해서, 객체간의 관련성이 감소하게 된다. (객체 지향적인 설계에서 객체 사이의 관련성을 줄이는 것은 매우 중요하다!)

결론

지금까지 커맨드 패턴의 구현에 대해서 알아보았다. 커맨드 패턴을 통해서 사용자들은 명령어를 입력받는 객체가 그 명령어의 실제 구현에 대해서 알 필요 없이 명령어 객체를 사용하기만 하면 되었다. 또한, 명령어를 입력받는 객체와 처리하는 명령어 객체 사이에서 커맨드 팩토리를 사용함으로써 객체 사이의 관련성을 최대한 줄일 수 있게 되었다. 실제로 커맨드 패턴의 핵심은 명령어를 처리하는 부분을 객체로 캡슐화 함으로써 실제 내부 구현을 다른 객체들로부터 분리하는 것에 있다.

관련링크:
Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 지니랜드 2009.03.24 10:12 신고  댓글주소  수정/삭제  댓글쓰기

    CommandFactory에 createCommand라는 메소드를 만들고, 아래서는
    factory.getCommand로 쓰셨네요. 둘 중 하나를 수정하셔야 할 듯하네요.^^;

    좋은 글 감사합니다.

  2. 패턴공부중 2013.02.06 06:33 신고  댓글주소  수정/삭제  댓글쓰기

    상세하고 좋은 글입니다.
    위키찾다가 빈약한 설명에 실망하고 들어와서 봤는데
    감사합니다.

  3. 감사합니다 2014.05.16 14:53 신고  댓글주소  수정/삭제  댓글쓰기

    좋은 정보 감사합니다

  4. 감사합니다 2016.01.18 10:48 신고  댓글주소  수정/삭제  댓글쓰기

    정리 잘되서 이해하기쉽습니다^^