커맨드 패턴 가이드: 요청을 객체로 만들면 큐와 재시도가 쉬워지는 이유
Dev
마지막 업데이트

커맨드 패턴 가이드: 요청을 객체로 만들면 큐와 재시도가 쉬워지는 이유


많은 프로그램은 “무언가를 해야 하면 지금 함수 호출해서 실행한다”라는 단순한 방식으로 시작합니다.

처음에는 그걸로 충분합니다.

하지만 요청 자체가 자기 생애를 가져야 하는 순간이 옵니다.

예를 들면 액션이:

  • 나중에 실행되어야 하거나
  • 큐에 들어가야 하거나
  • 실패 시 재시도되어야 하거나
  • 비즈니스 이력으로 남아야 하거나
  • 취소나 undo 비슷한 흐름을 지원해야 할 수 있습니다

이 시점부터는 단순 즉시 함수 호출만으로는 표현력이 부족해집니다. 이럴 때 커맨드 패턴이 도움이 됩니다.

이 글에서는 아래 내용을 다룹니다.

  • 커맨드 패턴이 실제로 무엇을 바꾸는지
  • 요청을 객체로 감싸는 것이 왜 유용한지
  • 어떤 상황에서 특히 잘 맞는지
  • 큐, 재시도, undo 흐름과 어떻게 연결되는지
  • 언제는 오히려 불필요한 의식이 되는지

짧게 요약하면 이렇습니다. 커맨드 패턴은 실행할 요청을 하나의 객체로 다뤄서, 그 요청을 저장하고, 큐에 넣고, 기록하고, 다시 실행하고, 다른 실행자에게 넘길 수 있게 만드는 방식입니다.

왜 직접 호출만으로는 부족해지는가

즉시 함수 호출은 실행 시점과 강하게 붙어 있습니다.

예를 들어:

sendInvoiceEmail(userId);

이 한 줄 안에는 사실 여러 결정이 묶여 있습니다.

  • 무엇을 할지
  • 언제 할지
  • 누가 실행할지
  • 실패하면 어떻게 처리할지

단순한 로직에서는 괜찮습니다. 하지만 요구사항이 “나중에 보내기”, “큐에 넣기”, “메일 서버 타임아웃이면 재시도하기”로 바뀌는 순간, 기존 호출 모양은 금방 답답해집니다.

커맨드 패턴은 여기서:

  • 요청 자체
  • 실제 일을 수행하는 객체
  • 언제 어디서 실행할지 정하는 코드

를 분리해 줍니다.

이 분리 때문에 커맨드 패턴이 작업 큐, 스케줄러, 액션 히스토리 같은 설계에서 자주 등장합니다.

커맨드 패턴이 실제로 바꾸는 것

커맨드 패턴의 핵심은 “실행할 것”을 객체로 표현한다는 점입니다.

그 객체는 보통 아래를 품습니다.

  • 어떤 액션을 실행할지
  • 실행에 필요한 데이터
  • 실행을 시작하는 공통 인터페이스

요청이 독립 객체가 되는 순간, 우리는 그 요청을 데이터처럼 다룰 수 있게 됩니다.

그래서 이런 것들이 쉬워집니다.

  • 저장하기
  • 큐잉하기
  • 로깅하기
  • 검사하기
  • 배치 실행하기
  • 재시도하기
  • 다른 실행자에게 넘기기

핵심 변화는 여기 있습니다. 커맨드 패턴이 마법을 부리는 것이 아니라, 요청에 즉시 호출 이상의 정체성을 부여하는 것입니다.

보통 등장하는 역할들

전형적인 커맨드 패턴 설명에는 세 역할이 자주 나옵니다.

  • Command: 실행 메서드를 가진 객체
  • Receiver: 실제 작업을 수행하는 객체
  • Invoker: 세부 구현을 몰라도 커맨드를 실행시키는 쪽

작은 프로젝트에서는 이 역할이 매우 가볍게 표현될 수 있고, 큰 시스템에서는 요청 생성과 요청 실행을 분리하는 유용한 경계가 됩니다.

감각적으로 보면:

  • command는 “무엇을 할지”를 담고
  • receiver는 “어떻게 할지”를 알고
  • invoker는 “언제 실행할지”를 결정합니다

이 분리 때문에 커맨드 패턴은 지연 실행과 잘 맞습니다.

실전 TypeScript 예시

예를 들어 전자상거래 시스템에서 환불 요청이 검토를 거친 뒤 백그라운드 워커에서 처리된다고 해봅시다.

interface Command {
  execute(): Promise<void>;
}

class RefundService {
  async refund(orderId: string, amount: number): Promise<void> {
    console.log(`Refund ${amount} for order ${orderId}`);
  }
}

class RefundOrderCommand implements Command {
  constructor(
    private refundService: RefundService,
    private orderId: string,
    private amount: number
  ) {}

  async execute(): Promise<void> {
    await this.refundService.refund(this.orderId, this.amount);
  }
}

class CommandQueue {
  private items: Command[] = [];

  add(command: Command): void {
    this.items.push(command);
  }

  async runAll(): Promise<void> {
    for (const command of this.items) {
      await command.execute();
    }
  }
}

const refundService = new RefundService();
const queue = new CommandQueue();

queue.add(new RefundOrderCommand(refundService, 'ORD-1024', 39.99));
await queue.runAll();

예시는 단순하지만 장점은 분명합니다.

  • 환불 요청이 독립 객체가 되고
  • 실행을 나중으로 미룰 수 있고
  • invoker는 환불 내부 세부를 몰라도 되고
  • 같은 요청을 로깅하거나 재시도하거나 큐에 실을 수 있습니다

커맨드 패턴이 없으면 이런 관심사가 한 함수 호출 주변에 뒤엉키기 쉽습니다.

”실행을 데이터처럼 다룬다”라고 생각하면 쉽다

커맨드 패턴을 가장 이해하기 쉬운 방식 중 하나는, 실행 자체를 이동 가능한 단위로 바꾼다고 생각하는 것입니다.

보통 실행 가능한 동작은 호출 위치에 붙어 있습니다. 하지만 command가 되면 요청 객체가 시스템 안을 이동할 수 있습니다.

그러면 이런 설계가 쉬워집니다.

  • 버튼 클릭이 command를 만들고
  • 큐가 command를 보관하고
  • 워커가 command를 실행하고
  • 감사 로그가 command를 기록하고
  • 재시도 시스템이 command를 다시 실행합니다

즉 커맨드 패턴은 “지금 당장 실행”보다 “시간을 두고 다뤄야 하는 작업”에서 힘이 강해집니다.

어떤 상황에서 특히 잘 맞나

커맨드 패턴은 요청이 독립적인 정체성과 생애를 가져야 할 때 매력이 커집니다.

대표적인 예시는:

  • undo나 replay가 필요한 UI 액션
  • 백그라운드 잡
  • 작업 큐
  • 액션 히스토리를 남기는 워크플로우
  • 메뉴 시스템이나 단축키 매핑
  • 서로 다른 액션에 같은 실행 래퍼를 적용해야 하는 경우

예를 들어 “이메일 보내기”, “환불 실행”, “검색 인덱스 재생성”은 서로 다른 작업이지만, command 인터페이스를 쓰면 같은 큐나 스케줄러로 흘려보내기 쉬워집니다.

왜 큐와 재시도와 그렇게 잘 맞나

큐는 “무엇을 나중에 실행할지”가 명확할수록 다루기 쉽습니다.

커맨드 객체는 바로 그 경계를 제공합니다.

큐 친화적인 요청이 보통 필요로 하는 것은:

  • 나중에 실행해도 충분한 데이터
  • 예측 가능한 실행 인터페이스
  • 로깅되거나 직렬화될 수 있는 형태

커맨드 패턴은 이걸 한 단위로 묶어 줍니다.

그래서 재시도와도 잘 연결됩니다. 일시적 오류 때문에 실패했다면, 시스템은 통제된 규칙 아래 같은 command를 다시 실행할 수 있습니다.

물론 모든 큐가 교과서적인 GoF 커맨드 패턴을 그대로 구현할 필요는 없습니다. 중요한 것은 형태보다 아이디어입니다. 즉 호출자와 분리된 실행 단위로 요청을 다루는 사고방식입니다.

Strategy, Event, Job과는 어떻게 다른가

커맨드 패턴과 비슷하게 들리는 개념들이 몇 가지 있습니다.

구분해 두면 더 선명해집니다.

Strategy는 알고리즘이나 동작 방식을 바꾸는 데 초점이 있습니다.

  • “가격 계산 전략 중 무엇을 쓸까?”

Command는 실행 요청을 포장하는 데 초점이 있습니다.

  • “이 환불 요청을 나중에 실행해라.”

Event는 대개 이미 일어난 사실을 나타냅니다.

  • “주문이 배송되었다.”

Job은 스케줄러나 워커 시스템에서 실행 단위를 뜻하는 경우가 많습니다.

  • “이 백그라운드 작업을 실행해라.”

실무에서는 이들이 겹칠 수 있습니다. 잡 큐 안에 command 비슷한 객체가 들어갈 수 있고, UI 액션이 command를 만든 뒤 이후 event를 발행할 수도 있습니다. 그래도 설계에서 강조하는 지점은 다릅니다.

Undo, 감사 로그, 지연 실행

커맨드 패턴이 undo 예시와 자주 함께 소개되는 데는 이유가 있습니다.

액션이 객체로 모델링되면 아래 같은 메타데이터를 붙이기 쉬워집니다.

  • 누가 요청했는지
  • 언제 요청했는지
  • 어떤 입력으로 실행했는지
  • 성공했는지 실패했는지
  • 어떻게 되돌릴 수 있는지

모든 command가 진짜 undo를 지원하는 것은 아니지만, 액션 이력을 더 명시적으로 다루기 쉬워지는 건 맞습니다.

그래서 비즈니스 시스템에서 추적 가능성이 중요할 때 유용합니다. 정식 undo 스택이 없어도, 액션을 독립 객체로 다루면 로그와 감사 흐름이 더 명확해지는 경우가 많습니다.

비용과 트레이드오프도 있다

커맨드 패턴은 유용하지만 공짜는 아닙니다.

대표적인 비용은:

  • 클래스나 객체 수가 늘어나고
  • 작은 기능에는 과한 추상화가 될 수 있고
  • 한 번에 읽히는 직관성이 떨어질 수 있고
  • 단순한 시스템에서는 보일러플레이트가 늘어난다는 점입니다

그래서 초보자가 패턴을 배우고 나면 모든 액션을 command로 감싸고 싶어지기도 하는데, 보통은 그렇게까지 할 필요가 없습니다.

이 패턴은 요청에 진짜 생애주기가 있을 때 값을 합니다. 구조 이름이 멋있어서가 아닙니다.

언제는 쓰지 않는 편이 낫나

보통 아래 조건이면 커맨드 패턴이 없어도 됩니다.

  • 액션이 항상 즉시 실행되고
  • 큐, 재시도, 히스토리, 지연 실행 요구가 없고
  • 직접 함수 호출이 이미 충분히 명확하고
  • wrapper를 씌워도 운영상 이득이 거의 없는 경우

이때는 이런 질문이 도움이 됩니다.

“이 요청은 저장되거나, 지연되거나, 다시 실행되거나, 공통 실행자에게 전달될 필요가 있는가?”

아니라면 더 단순한 설계가 낫습니다.

자주 하는 실수

팀들이 자주 부딪히는 문제는:

  • 한 줄짜리 사소한 액션까지 모두 command로 감싸는 것
  • command 안에 너무 많은 책임을 몰아넣는 것
  • command 생성과 비즈니스 검증을 뒤섞는 것
  • 과한 간접화로 중요한 부작용이 안 보이게 만드는 것
  • 더 가벼운 방법으로도 될 문제에 딱딱한 계층 구조를 강요하는 것입니다

또 하나의 흔한 실수는 교과서 구조를 그대로 베끼면서, 실제 코드가 어떤 문제를 푸는지는 묻지 않는 것입니다.

좋은 패턴 사용은 이름에서 시작하지 않고, 시스템에 걸리는 압력에서 시작합니다.

빠르게 점검하는 체크리스트

커맨드 패턴이 도움이 될지 판단할 때는 아래 항목을 보면 좋습니다.

  1. 요청이 호출자와 독립적으로 존재해야 한다
  2. 지연 실행이 중요하다
  3. 재시도나 취소가 중요할 수 있다
  4. 액션 히스토리나 감사 가능성이 중요하다
  5. 여러 액션이 공통 실행 인터페이스를 공유해야 한다
  6. 큐, 워커, 스케줄러가 설계에 들어 있다

대부분 거짓이라면 더 단순한 설계로도 충분할 가능성이 큽니다.

FAQ

Q. 커맨드 패턴은 UI 프로그램에서만 쓰나요?

아닙니다. 백엔드 작업, 워커 시스템, 자동화, 비즈니스 워크플로우에서도 자주 등장합니다.

Q. 큐에 들어가는 모든 작업이 command인가요?

꼭 그렇지는 않지만, 많은 큐 작업은 “요청을 실행 단위로 포장한다”는 같은 아이디어를 공유합니다.

Q. Strategy 패턴과는 어떻게 다른가요?

Strategy는 “어떤 방식으로 할지”를 바꾸고, Command는 “실행 요청 자체”를 포장합니다.

Q. 꼭 클래스가 있어야 하나요?

항상 그런 것은 아닙니다. 교과서 문법보다 중요한 것은 핵심 아이디어입니다. 어떤 코드베이스에서는 구조화된 payload를 가진 함수 조합만으로도 command 비슷한 모델을 표현할 수 있습니다.

Q. 초보자가 특히 조심할 점은 뭔가요?

과한 설계를 경계하면 좋습니다. 실행 시점, 실행 주체, 요청 생애주기가 문제의 일부일 때만 패턴을 쓰는 편이 좋습니다.

먼저 읽어볼 가이드

검색 유입이 많은 핵심 글부터 이어서 보세요.

광고