← Posts

GoF · Behavioral · Command

Command

요청을 나중에 실행하거나 되돌리거나 기록하고 싶을 때. 요청 자체를 객체로 캡슐화하는 행위 패턴.

목차
  1. 요청을 저장하고, 취소하고, 기록하고 싶다
  2. 커맨드 = 요청을 객체로
  3. Command 인터페이스
  4. Receiver에 위임한다
  5. Invoker는 커맨드만 안다
  6. undo로 되돌린다
  7. 큐잉과 로깅
  8. 실무: 작업 큐와 트랜잭션적 작업
  9. 정리

요청을 저장하고, 취소하고, 기록하고 싶다

텍스트 에디터에서 글자를 입력한다고 하자. 가장 단순하게 짜면 메서드를 그 자리에서 부른다.

java
document.insert("안녕", 0);   // 0번 위치에 "안녕" 삽입

동작은 한다. 그런데 요구가 붙기 시작한다. **되돌리기(Ctrl+Z)**를 넣고 싶다. 방금 무슨 일을 했는지 어딘가에 남아 있어야 되돌릴 수 있는데, 메서드 호출은 부르는 순간 끝나고 사라진다. 무엇을 했는지 남지 않으니 되돌릴 근거가 없다.

기록도 마찬가지다. “어떤 요청이 언제 실행됐는지” 로그로 남기거나, 요청을 큐에 쌓아 나중에 실행하고 싶어도, 호출은 저장할 수 있는 물건이 아니다. 동사는 저장이 안 된다.

커맨드 = 요청을 객체로

커맨드 패턴이 이걸 푼다.

요청(무엇을 하라) 자체를 객체로 캡슐화한다. 그러면 요청을 변수에 담고, 큐에 쌓고, 스택에 넣어 되돌리고, 로그로 남길 수 있다.

“삽입해라”라는 동작을 그 자리에서 실행하는 대신, “삽입 요청”이라는 객체를 만든다. 이제 요청은 저장할 수 있는 물건이 된다. 담아 두었다가 나중에 실행하고, 스택에 넣었다가 되돌리고, 파일에 적어 기록한다. 동사를 명사로 바꾸는 것 - 이게 커맨드의 핵심 발상이다.

Command 인터페이스

모든 요청이 따를 규격을 세운다.

java
public interface Command {
    void execute();   // 실행한다
    void undo();      // 되돌린다 (선택)
}

execute는 “이 요청을 실행하라”, undo는 “방금 한 걸 되돌려라”다. 되돌리기가 필요 없는 요청이라면 undo는 없어도 된다. 중요한 건 모든 요청이 같은 모양(execute)을 갖는다는 점이다. 무슨 요청이든 겉모습이 똑같으니, 그걸 다루는 쪽은 내용물을 몰라도 실행할 수 있다.

Receiver에 위임한다

커맨드 구현은 스스로 일하지 않는다. **실제 동작을 아는 객체(Receiver)**에 위임한다. 여기선 Document가 Receiver다.

java
public class InsertTextCommand implements Command {
    private final Document document;   // Receiver: 실제로 일하는 쪽
    private final String text;
    private final int position;

    public InsertTextCommand(Document document, String text, int position) {
        this.document = document;
        this.text = text;
        this.position = position;
    }

    public void execute() {
        document.insert(text, position);   // 실제 삽입은 Document가
    }

    public void undo() {
        document.delete(position, text.length());   // 삽입을 되돌리면 삭제
    }
}

InsertTextCommand무엇을·어디에 넣을지(text, position)를 자기 안에 품고 있다. 그래서 만들어 둔 뒤 나중에 실행해도 그때의 요청 그대로 재현된다. 요청에 필요한 정보가 객체 안에 다 들어 있으니, 요청이 시점에서 독립한다.

Invoker는 커맨드만 안다

커맨드를 실행하는 쪽을 **인보커(Invoker)**라 부른다. 인보커는 커맨드를 받아 execute를 부를 뿐, 그게 무슨 일인지 모른다.

java
public class Editor {
    private final Deque<Command> history = new ArrayDeque<>();

    public void run(Command command) {
        command.execute();      // 무슨 요청인지 모른 채 실행
        history.push(command);  // 되돌리기용으로 쌓아 둔다
    }

    public void undo() {
        if (!history.isEmpty()) {
            history.pop().undo();   // 가장 최근 것을 되돌린다
        }
    }
}

Editor는 삽입인지 삭제인지 서식 변경인지 관심 없다. Command라는 규격만 알고, executeundo만 부른다. 그래서 새 요청(붙여넣기, 서식 변경)이 생겨도 Editor는 안 고친다 - 커맨드 구현만 하나 추가하면 된다. 요청을 내는 쪽, 실행하는 쪽, 실제로 일하는 쪽이 이렇게 셋으로 갈린다.

diagramdiagram

undo로 되돌린다

커맨드가 객체로 남으니, 실행한 것들을 스택에 쌓아 두면 되돌리기가 자연스럽게 나온다.

java
Editor editor = new Editor();

editor.run(new InsertTextCommand(doc, "안녕", 0));
editor.run(new InsertTextCommand(doc, "하세요", 2));

editor.undo();   // "하세요" 삽입을 되돌림 (가장 최근 것)
editor.undo();   // "안녕" 삽입을 되돌림

각 커맨드가 자기를 어떻게 되돌리는지(undo)를 스스로 안다. 삽입의 반대는 삭제, 삭제의 반대는 복원. 인보커는 스택에서 가장 최근 커맨드를 꺼내 undo만 부르면 된다. 되돌린 것을 다른 스택에 옮겨 두면 **다시 실행(redo)**도 같은 방식으로 나온다. 요청을 객체로 만든 순간, 되돌리기는 거의 공짜로 따라온다.

큐잉과 로깅

되돌리기 말고도, 요청이 객체가 되면 할 수 있는 게 많아진다.

  • 큐잉(지연 실행) - 커맨드를 큐에 쌓아 두었다가 나중에, 혹은 다른 스레드에서 꺼내 실행한다. 요청을 내는 시점과 실행하는 시점을 분리하는 것이다.
  • 로깅 - 실행한 커맨드를 순서대로 기록해 두면, 시스템이 죽었다 살아났을 때 그 기록을 되짚어 상태를 복원할 수 있다.
java
Queue<Command> queue = new LinkedList<>();
queue.offer(new InsertTextCommand(doc, "나중에", 0));   // 지금은 담아만 둔다

// 다른 시점 / 다른 곳에서
while (!queue.isEmpty()) {
    queue.poll().execute();   // 이제 실행
}

담고, 쌓고, 미루고, 적는다. 전부 요청이 객체라서 가능한 일이다.

실무: 작업 큐와 트랜잭션적 작업

커맨드는 실무에 넓게 깔려 있다. 자바의 **Runnable**이 사실상 커맨드다. “해야 할 일”을 run() 하나로 감싼 객체이고, 이걸 ExecutorService에 넘기면 스레드 풀이 큐에 쌓아 두었다가 실행한다.

java
Runnable task = () -> report.generate();   // 요청을 객체로
executor.submit(task);                     // 큐에 넣어 나중에 실행

ExecutorService는 인보커다. 무슨 일인지 모른 채 큐에서 꺼내 실행한다. 작업 큐, undo 스택, 매크로(여러 커맨드를 묶어 한 번에 실행), 트랜잭션적 작업(실패 시 지금까지 실행한 커맨드를 역순으로 undo해 되감기)이 모두 이 패턴 위에 선다. GUI 프레임워크에서 버튼·메뉴·단축키가 같은 동작을 공유하는 것도, 그 동작을 하나의 커맨드 객체로 만들어 여기저기 바인딩하기 때문이다.

정리

커맨드란요청 자체를 객체로 캡슐화한다
문제메서드 호출은 그 자리에서 사라져 저장·취소·기록이 안 된다
고침요청을 Command 구현으로, execute/undo를 규격으로 세운다
역할 분리Invoker는 커맨드만 알고, Receiver가 실제 동작을 한다
효과큐잉(지연 실행), undo 스택, 로깅, 매크로가 자연히 따라온다
실무작업 큐(Runnable), undo 스택, 트랜잭션적 되감기

동사인 요청을 명사인 객체로 바꿔, 저장하고 되돌리고 기록할 수 있게 만드는 것 - 그게 커맨드다.

관련 글