메뉴 닫기

Java Command 패턴 사례, 실제 코드로 배우는 행위 패턴의 핵심

Java Command 패턴 사례, 실제 코드로 배우는 행위 패턴의 핵심

💡 유지보수와 확장성 모두 잡는 Command 패턴, 실무 적용 예제 공개

Java 디자인 패턴 중 Command 패턴은 객체지향 설계에서 자주 사용되는 강력한 행위 패턴입니다.
명령을 객체로 캡슐화해 호출자와 수신자를 분리하고, 요청의 큐잉·로깅·취소까지 유연하게 처리할 수 있죠.
특히 복잡한 기능이 많은 애플리케이션에서는 코드의 가독성과 유지보수성을 동시에 확보할 수 있는 해법이 됩니다.
이번 글에서는 Command 패턴의 기본 개념과 함께, 실무에서 바로 활용 가능한 실제 Java 코드 예제를 중심으로 설명합니다.
개발 현장에서 마주치는 요구사항을 어떻게 구조적으로 해결할 수 있는지 함께 살펴보겠습니다.

아래에서는 패턴의 정의, 구현 방법, 그리고 장단점을 차례대로 살펴본 뒤, 실제 서비스 코드에서 Command 패턴을 적용한 사례를 코드와 함께 분석합니다.
이를 통해 단순한 이론이 아닌, 현장에서 통하는 적용법을 이해할 수 있을 것입니다.
또한 적용 시 고려해야 할 사항과 유지보수 팁까지 공유하니, 중급 개발자부터 초보자까지 모두 유익하게 참고하실 수 있습니다.



🔗 Command 패턴이란?

Command 패턴은 요청(명령)을 객체로 캡슐화하여, 요청의 발신자와 수신자를 분리하는 행위 패턴입니다.
이 패턴의 핵심은 실행될 동작을 캡슐화한 Command 객체를 만들고, 실행 시점을 호출자(Invoker)가 결정하며, 실제 동작은 수신자(Receiver)가 수행한다는 점입니다.
이를 통해 실행할 동작을 변경하거나 기록하고, 실행 취소(undo) 또는 재실행(redo) 기능을 쉽게 구현할 수 있습니다.

예를 들어, 리모컨을 생각해 보면 이해가 쉽습니다.
리모컨의 버튼(Invoker)은 단순히 어떤 명령을 실행하라는 요청만 전달하고, 실제로 TV 전원을 켜거나 끄는 로직은 TV(Receiver)에 있습니다.
Command 객체는 버튼과 TV 사이에서 ‘켜기’나 ‘끄기’ 같은 요청을 하나의 객체로 포장해 전달하는 역할을 합니다.

📌 구조적 특징

Command 패턴은 일반적으로 다음과 같은 구성 요소로 이루어집니다.

  • 🛠️Command : 실행될 동작을 정의하는 인터페이스 또는 추상 클래스
  • ⚙️ConcreteCommand : Command 인터페이스를 구현하고, Receiver 객체와 연동하여 실제 동작 수행
  • 🔌Invoker : Command 객체를 실행시키는 주체
  • 💡Receiver : 실제 작업을 수행하는 객체

📌 간단한 예시 코드

CODE BLOCK
// Command 인터페이스
interface Command {
    void execute();
}

// Receiver
class Light {
    void turnOn() {
        System.out.println("불을 켭니다.");
    }
}

// ConcreteCommand
class LightOnCommand implements Command {
    private Light light;
    public LightOnCommand(Light light) {
        this.light = light;
    }
    public void execute() {
        light.turnOn();
    }
}

// Invoker
class RemoteControl {
    private Command command;
    public void setCommand(Command command) {
        this.command = command;
    }
    public void pressButton() {
        command.execute();
    }
}

위 코드에서 RemoteControl은 Invoker, Light는 Receiver, LightOnCommand는 ConcreteCommand로 동작합니다.
이처럼 Command 패턴은 요청의 실행과 발신을 깔끔하게 분리하여 확장성과 유지보수성을 높여줍니다.

🛠️ Java에서의 Command 패턴 구조

Java에서 Command 패턴을 구현할 때는 인터페이스 기반의 추상화와 클래스 간의 역할 분리가 핵심입니다.
각 구성 요소는 명확한 책임을 가지며, 변경이나 확장이 필요한 경우에도 다른 구성 요소에 영향을 최소화하도록 설계됩니다.
아래 구조는 전형적인 Command 패턴의 클래스 다이어그램을 Java 환경에 맞춰 해석한 것입니다.

📌 클래스 다이어그램 구성

구성 요소 설명
Command 실행할 동작을 정의하는 인터페이스 또는 추상 클래스
ConcreteCommand Command 인터페이스를 구현하여 Receiver와 연결해 실제 동작 수행
Receiver 명령이 수행될 실제 객체
Invoker Command 객체를 실행하는 주체, 실행 시점을 제어

📌 Java 구현 예시

CODE BLOCK
// Command 인터페이스
public interface Command {
    void execute();
    void undo();
}

// Receiver
public class Document {
    public void open() {
        System.out.println("문서를 엽니다.");
    }
    public void close() {
        System.out.println("문서를 닫습니다.");
    }
}

// ConcreteCommand
public class OpenDocumentCommand implements Command {
    private Document document;
    public OpenDocumentCommand(Document document) {
        this.document = document;
    }
    public void execute() {
        document.open();
    }
    public void undo() {
        document.close();
    }
}

// Invoker
public class CommandManager {
    private Command command;
    public void setCommand(Command command) {
        this.command = command;
    }
    public void executeCommand() {
        command.execute();
    }
    public void undoCommand() {
        command.undo();
    }
}

위 구조는 확장성유지보수성 모두를 고려한 전형적인 Command 패턴 설계입니다.
다양한 명령을 객체 단위로 관리할 수 있어, 새로운 기능을 추가하거나 기존 동작을 수정할 때 최소한의 코드 변경만으로도 대응 가능합니다.



⚙️ Command 패턴의 장점과 단점

Command 패턴은 요청을 객체로 캡슐화함으로써 호출자와 수신자의 결합도를 낮추고, 실행 시점 제어·로깅·되돌리기 같은 부가 요구를 유연하게 붙일 수 있습니다.
실무에서는 버튼 클릭, 메뉴 실행, 메시지 처리, 작업 큐 같은 사용자 동작을 명령 단위로 표준화할 때 특히 강력합니다.
반면, 명령의 종류가 많아질수록 클래스 수가 늘고 보일러플레이트가 증가하는 부담도 존재합니다.
아래에서 실제 개발 관점의 이점과 주의점을 정리합니다.

✅ 장점

  • 🧩호출자(Invoker)와 수신자(Receiver)의 결합도 감소로 테스트·교체가 쉬움
  • 🧾요청을 객체로 다루므로 로깅·감사 추적·재처리(재시도) 구현이 간단
  • ↩️undo/redo 지원: 상태 스냅샷 또는 반대 동작 정의로 쉽게 확장
  • 🧵작업 큐/스케줄링/비동기 처리, 우선순위 제어, 일괄 실행에 유리
  • 📦새로운 명령 추가가 OCP에 부합(기존 코드 최소 변경)

⚠️ 단점

이슈 영향 및 대응
클래스 폭증 명령 종류 증가 시 보일러플레이트가 많아짐 → 팩토리/레지스트리·제네릭 기법으로 생성 단순화
상태 관리 복잡성 undo를 위해 스냅샷/반대 연산이 필요 → Memento와 함께 사용
과도한 추상화 단순 호출에도 레이어가 늘어 학습·디버깅 비용 증가 → 단순 케이스는 직접 호출 유지
성능 오버헤드 명령 객체 생성·큐잉 비용 → 풀링·일괄 실행·불변 객체로 최적화

⚠️ 주의: 단순한 CRUD 서비스에 명령 클래스를 무분별하게 도입하면 코드가 불필요하게 비대해질 수 있습니다.
명령이 큐잉, 트랜잭션 롤백, 다중 수신자, 복합 실행 같은 요구를 충족해야 할 때 도입 효과가 극대화됩니다.

🎯 적합한 사용 시나리오

다음 상황에서 Command 패턴의 채택 가치가 높습니다.

  • 🖱️UI 버튼/메뉴/단축키 같은 사용자 액션의 표준화가 필요할 때
  • 🧰되돌리기(undo)·재실행(redo)·매크로(Composite) 기능을 제공해야 할 때
  • 📬비동기 작업, 큐 기반 처리, 재시도/지연 실행이 필요한 워크플로우
  • 🔐명령 감사 로그·권한 체크를 중앙에서 수행해야 할 때

💡 TIP: 명령이 많아질 때는 Command 레지스트리(Map)로 키-명령 바인딩을 관리하고, 공통 단계를 템플릿 메서드로 올려 보일러플레이트를 줄이세요.
실패 복구가 중요한 도메인은 Memento와 결합해 되돌리기를 안전하게 구현하는 것이 좋습니다.

🔌 실무 적용 사례와 코드 예제

Command 패턴은 이론적으로만 배우면 감이 잘 오지 않을 수 있습니다.
따라서 여기서는 실제 현업 코드에 적용된 사례를 바탕으로, 어떻게 구조화하고 동작시키는지 살펴보겠습니다.
아래 예제는 작업 요청 큐가 있는 주문 처리 시스템을 구현한 코드입니다.
요청이 들어오면 Command 객체로 변환되어 큐에 저장되고, 작업 관리자가 순차적으로 실행하는 구조입니다.

📌 주문 처리 시스템 예시

CODE BLOCK
// Command 인터페이스
public interface OrderCommand {
    void execute();
}

// Receiver
public class OrderService {
    public void placeOrder(String product) {
        System.out.println(product + " 주문이 접수되었습니다.");
    }
    public void cancelOrder(String product) {
        System.out.println(product + " 주문이 취소되었습니다.");
    }
}

// ConcreteCommand - 주문 생성
public class PlaceOrderCommand implements OrderCommand {
    private OrderService service;
    private String product;
    public PlaceOrderCommand(OrderService service, String product) {
        this.service = service;
        this.product = product;
    }
    public void execute() {
        service.placeOrder(product);
    }
}

// ConcreteCommand - 주문 취소
public class CancelOrderCommand implements OrderCommand {
    private OrderService service;
    private String product;
    public CancelOrderCommand(OrderService service, String product) {
        this.service = service;
        this.product = product;
    }
    public void execute() {
        service.cancelOrder(product);
    }
}

// Invoker
import java.util.LinkedList;
import java.util.Queue;
public class OrderManager {
    private Queue<OrderCommand> orderQueue = new LinkedList<>();
    public void addOrder(OrderCommand command) {
        orderQueue.add(command);
    }
    public void processOrders() {
        while (!orderQueue.isEmpty()) {
            orderQueue.poll().execute();
        }
    }
}

// 실행 예시
public class Main {
    public static void main(String[] args) {
        OrderService service = new OrderService();
        OrderManager manager = new OrderManager();
        
        manager.addOrder(new PlaceOrderCommand(service, "노트북"));
        manager.addOrder(new PlaceOrderCommand(service, "스마트폰"));
        manager.addOrder(new CancelOrderCommand(service, "노트북"));
        
        manager.processOrders();
    }
}

이 예제에서 OrderManager는 Invoker 역할을 하며, 주문 명령을 큐에 쌓아 두었다가 순차적으로 실행합니다.
PlaceOrderCommandCancelOrderCommand는 각각 주문 생성과 취소를 담당하는 ConcreteCommand입니다.
이 구조는 주문이 폭주하는 상황에서도 안전하게 처리가 가능하며, 새로운 주문 유형을 쉽게 확장할 수 있습니다.

💎 핵심 포인트:
Command 패턴을 큐 기반 구조와 결합하면 비동기 처리, 재시도 로직, 우선순위 제어를 쉽게 구현할 수 있습니다.
특히 마이크로서비스 환경에서 메시지 큐(Kafka, RabbitMQ)와 함께 쓰면 확장성이 극대화됩니다.

📌 실무 적용 시 고려 사항

  • 📦명령 객체의 직렬화 지원 여부 확인 (네트워크 전송·저장용)
  • 📜실행 로그 및 감사 추적 로직을 Invoker 또는 Command 레벨에서 통합
  • 🛡️재시도·롤백 같은 예외 처리 정책을 설계 단계에서 반영
  • 🔄메시지 큐, 스케줄러 등 외부 시스템과의 통합 시 지연·실패 시나리오 대비



💡 Command 패턴 적용 시 유의사항

Command 패턴은 강력하지만 모든 상황에 무조건 적용하는 것은 바람직하지 않습니다.
패턴의 도입으로 얻는 이점과 그에 따른 복잡성 증가를 비교해보고, 비즈니스 요구 사항에 부합하는지 반드시 검토해야 합니다.
다음은 실무에서 Command 패턴을 적용할 때 유용한 체크포인트입니다.

📌 적용 전 점검 사항

  • 🛠️명령을 객체 단위로 관리할 필요성이 충분한가?
  • 📈기능 추가·변경이 빈번하여 유지보수성이 중요한가?
  • 📋undo/redo 기능이나 작업 이력 관리가 필요한가?
  • 🔄동기·비동기, 큐 기반 처리 등 실행 시점 제어가 필요한가?
  • ⚠️패턴 도입으로 인한 클래스 증가와 복잡성을 감당할 수 있는가?

📌 성능 및 유지보수 팁

Command 패턴의 장점을 극대화하려면 설계 단계에서부터 성능과 유지보수성을 함께 고려해야 합니다.

💡 TIP: 반복적으로 생성되는 명령 객체는 객체 풀링을 적용하면 메모리 사용량과 GC 부담을 줄일 수 있습니다.
또한 명령의 상태를 불변(immutable)으로 설계하면 동시성 문제를 예방할 수 있습니다.

⚠️ 피해야 할 실수

잘못된 접근 대안
모든 메서드를 무조건 Command로 감싸기 작업 추적·취소·지연 실행이 필요한 핵심 동작에만 적용
Invoker 내부에서 Receiver 직접 호출 Invoker는 오직 Command 실행만 담당하도록 유지
undo 로직 없이 Command만 구현 필요 시 Memento 패턴과 결합하여 복구 기능 완비

💎 핵심 포인트:
Command 패턴은 OCP(개방-폐쇄 원칙)를 충실히 따르지만, 남용하면 오히려 코드 복잡도를 높입니다.
도입 전 반드시 요구사항과 구조를 검토하고, 유지보수 비용 대비 효과가 충분한지 판단하세요.

자주 묻는 질문 (FAQ)

Command 패턴은 언제 사용해야 하나요?
동작을 객체로 캡슐화해 호출자와 수신자를 분리해야 하거나, undo/redo, 작업 큐, 로깅 같은 기능이 필요한 경우에 적합합니다.
Command 패턴과 Strategy 패턴의 차이점은 무엇인가요?
Strategy 패턴은 알고리즘을 교체 가능하게 만드는 데 중점을 두고, Command 패턴은 요청 자체를 객체로 만들어 실행 시점과 실행 방법을 유연하게 제어합니다.
Java에서 Command 패턴 구현 시 필수 인터페이스는 무엇인가요?
일반적으로 execute() 메서드를 가진 Command 인터페이스를 정의하며, 필요 시 undo() 메서드도 함께 포함합니다.
Command 객체를 직렬화하려면 어떻게 해야 하나요?
Serializable 인터페이스를 구현하면 됩니다. 단, 직렬화 대상 필드는 네트워크 전송·저장 요구사항에 맞게 선택적으로 지정해야 합니다.
Command 패턴은 멀티스레드 환경에서 안전한가요?
Command 객체를 불변(immutable)으로 설계하면 스레드 안전성을 높일 수 있습니다. 또한 Invoker와 큐를 동기화 처리해야 합니다.
Command 패턴과 Event Sourcing을 함께 사용할 수 있나요?
가능합니다. 명령을 이벤트로 기록하고, 이벤트 스토어를 통해 시스템 상태를 재구성하는 방식으로 확장할 수 있습니다.
실무에서는 Command 패턴을 어떻게 테스트하나요?
각 ConcreteCommand를 단위 테스트하고, Invoker가 예상대로 Command를 호출하는지 목(Mock) 객체로 검증합니다.
Command 패턴을 남용하면 어떤 문제가 생기나요?
불필요하게 많은 클래스가 생겨 코드 복잡도가 증가하고, 유지보수 비용이 커질 수 있습니다. 요구사항이 단순한 경우에는 직접 호출 방식을 사용하는 것이 좋습니다.

🚀 Command 패턴으로 구현한 유연하고 확장 가능한 시스템 설계

Command 패턴은 요청을 객체로 캡슐화함으로써 호출자와 수신자의 결합도를 낮추고, 동작을 재사용 가능하며 유연하게 확장할 수 있는 구조를 제공합니다.
Java 환경에서 이 패턴을 적용하면 작업 큐, 로깅, undo/redo, 스케줄링과 같은 기능을 손쉽게 구현할 수 있으며, 복잡한 비즈니스 로직을 구조적으로 관리할 수 있습니다.
이번 글에서 소개한 구조와 코드 예제는 실제 서비스 개발에서 충분히 활용 가능한 수준이며, 유지보수성과 확장성을 동시에 확보할 수 있습니다.
다만, 패턴 도입으로 인한 복잡성 증가를 고려해, 명령 객체가 반드시 필요한 핵심 기능 위주로 적용하는 것이 좋습니다.


🏷️ 관련 태그 : Java디자인패턴, Command패턴, 객체지향설계, 행위패턴, 소프트웨어아키텍처, 코드리팩토링, 패턴적용사례, Java프로그래밍, 유지보수성, 시스템설계