Mobile & App Stack/Java Software Architecture & Patterns

커맨드 패턴 완벽 정리: Undo/Redo 기능을 만드는 가장 스마트한 방법

임베디드 친구 2025. 1. 2. 15:56
728x90
반응형

문서 편집기에서 Ctrl + Z를 눌러 작업을 취소하거나, 게임에서 스킬 버튼을 눌러 기술을 발동시키는 기능들은 어떻게 구현될까요? 단순히 함수를 호출하는 것만으로는 '되돌리기'나 '예약 실행' 같은 복잡한 요구사항을 처리하기 어렵습니다.

오늘은 요청 자체를 하나의 객체로 만들어 관리하는 커맨드 패턴(Command Pattern)에 대해 깊이 있게 알아보겠습니다.

Generated by Gemini AI.


1. 커맨드 패턴이란?

커맨드 패턴은 "요청을 객체의 형태로 캡슐화"하여, 사용자가 보낸 요청을 나중에 실행하거나 큐에 저장하고, 실행 취소(Undo)까지 할 수 있게 돕는 행동 패턴입니다.

쉽게 이해하는 비유: 식당의 주문 시스템

  • Client (손님): 메뉴를 고르고 주문을 합니다.
  • Command (주문서): 손님이 주문한 내역이 적힌 종이입니다. (요청의 캡슐화)
  • Invoker (점원): 주문서를 받아 주방으로 전달합니다.
  • Receiver (요리사): 주문서에 적힌 대로 실제로 요리를 합니다.

점원(Invoker)은 요리 방법(Receiver의 로직)을 몰라도 '주문서(Command)'만 전달하면 작업이 수행됩니다.


2. 커맨드 패턴의 5가지 핵심 요소

  1. Command (인터페이스): 모든 명령 객체가 구현해야 할 인터페이스로, 보통 execute() 메서드를 포함합니다.
  2. ConcreteCommand (구체적 명령): Receiver 객체와 행동을 연결하여 실제 기능을 호출합니다.
  3. Receiver (수신자): 실제로 비즈니스 로직을 수행하는 객체입니다. (예: 전등, 모터)
  4. Invoker (호출자): 명령 객체를 보유하고 있으며, 명령의 execute()를 호출하여 작업을 요청합니다.
  5. Client (클라이언트): ConcreteCommand를 생성하고 Receiver를 설정합니다.

3. Java 실무 예제: 스마트 리모컨 만들기

전등을 켜고 끄는 간단한 리모컨 시스템을 통해 실행과 취소(Undo)를 구현해 보겠습니다.

Step 1. Command 인터페이스와 Receiver

Java
 
// Command 인터페이스
interface Command {
    void execute();
    void undo(); // 취소 기능을 위한 메서드
}

// Receiver: 실제 로직을 수행하는 '전등'
class Light {
    public void on() { System.out.println("💡 전등이 켜졌습니다."); }
    public void off() { System.out.println("🌑 전등이 꺼졌습니다."); }
}

Step 2. ConcreteCommand 구현

Java
 
// 전등 켜기 명령
class LightOnCommand implements Command {
    private Light light;

    public LightOnCommand(Light light) { this.light = light; }

    @Override
    public void execute() { light.on(); }

    @Override
    public void undo() { light.off(); } // 켜기의 반대는 끄기
}

// 전등 끄기 명령
class LightOffCommand implements Command {
    private Light light;

    public LightOffCommand(Light light) { this.light = light; }

    @Override
    public void execute() { light.off(); }

    @Override
    public void undo() { light.on(); } // 끄기의 반대는 켜기
}

Step 3. Invoker와 Client

Java
 
// Invoker: 리모컨 버튼
class RemoteControl {
    private Command slot;

    public void setCommand(Command command) { this.slot = command; }

    public void pressButton() { slot.execute(); }

    public void pressUndo() { slot.undo(); }
}

// Client 코드
public class Main {
    public static void main(String[] args) {
        Light livingRoomLight = new Light(); // 수신자
        Command lightOn = new LightOnCommand(livingRoomLight); // 명령 객체
        
        RemoteControl remote = new RemoteControl(); // 호출자
        
        remote.setCommand(lightOn);
        remote.pressButton(); // 실행: 💡 전등이 켜졌습니다.
        remote.pressUndo();   // 취소: 🌑 전등이 꺼졌습니다.
    }
}

4. 커맨드 패턴을 쓰면 좋은 점 (장점)

  • 디커플링(Decoupling): 요청하는 객체(Invoker)와 수행하는 객체(Receiver)가 서로를 전혀 몰라도 되므로 코드의 결합도가 낮아집니다.
  • 실행 취소(Undo/Redo): 명령 객체 내부에 이전 상태를 저장하거나 반대 동작을 정의함으로써 복잡한 취소 로직을 쉽게 관리할 수 있습니다.
  • 매크로 명령: 여러 개의 커맨드를 리스트에 담아 한 번에 실행하는 '매크로' 기능을 쉽게 만들 수 있습니다.
  • 확장성(OCP): 기존 코드를 수정하지 않고 새로운 명령(ConcreteCommand) 클래스를 추가할 수 있습니다.

5. 실제 사용 사례

  • GUI 툴바: 버튼마다 각기 다른 커맨드 객체를 할당하여 클릭 이벤트를 처리합니다.
  • 데이터베이스 트랜잭션: 수행할 쿼리들을 객체화하여 기록해 두었다가, 문제가 생기면 롤백(Undo)하는 용도로 사용합니다.
  • 스케줄러/작업 큐: 명령들을 리스트에 쌓아두고 하나씩 꺼내어 순차적으로 처리합니다.

결론

커맨드 패턴은 "명령을 데이터처럼 취급"할 수 있게 해줍니다. 이로 인해 작업의 순서를 바꾸거나, 로그를 남기거나, 실행을 취소하는 등의 제어가 가능해집니다. 유연하고 확장 가능한 시스템을 설계하고 싶다면 커맨드 패턴을 적극 고려해 보세요!


포스팅이 도움이 되셨다면 공감과 구독 부탁드립니다! 궁금한 점이나 의견은 댓글로 남겨주시면 정성껏 답변해 드리겠습니다.

반응형