<aside> 📌

커맨드 패턴을 "브로드캐스트 + 자기 필터링"으로 잘못 쓰면 매 틱 불필요한 가상함수 호출이 누적된다. 이 페이지는 그 원인(가상함수 비용)과 올바른 구조(구독 기반 선별 실행), 그리고 커맨드 패턴이 실제로 적합한 케이스를 일반 지식으로 정리한 문서다.

</aside>

핵심 원칙: 선별 책임은 Invoker(명령 주체)에 있어야 한다

커맨드 패턴은 Invoker가 "누구를 실행할지"를 알고 있어야 한다. 각 커맨드가 스스로 자기 차례인지 판단하게 만들면, 실행 대상이 아닌 객체까지 전부 호출되어 가상함수(동적 바인딩) 비용이 낭비된다.

구분 비효율적 설계 권장 설계
구조 Invoker → 전체 N개 순회 → 커맨드가 스스로 필터 Invoker → 선별 후 실행 → 커맨드는 실행만 담당
호출 비용 가상함수 N회 호출 (불필요한 낭비) 가상함수 호출 최소화 (필요한 것만)
선별 책임 커맨드(Command)에 있음 인보커(Invoker)에 있음
한 줄 요약 "일단 다 부르고 본인이 판단" "인보커가 거른 뒤 실행만 시킴"

오용 패턴: "브로드캐스트 + 자기 필터링"

커맨드 패턴의 의도는 "필요한 커맨드만 선별 실행"이다. 그런데 아래 구조는 반대로 전체를 매번 순회한 뒤 각자 알아서 걸러낸다.

[잘못된 구조 - Invoker가 선별 책임을 포기]

CommandInvoker
└── commands_[MAX_COUNT]  // N개 전부 flat array
      │
      ├── onTrigger() 호출 (N회 전부)
      │     ├── Command[0]   // isActive() == false → return  ┐
      │     ├── Command[1]   // isActive() == false → return  │ 대다수 탈락
      │     ├── ...          // isActive() == false → return  ┘
      │     ├── Command[k]   // isActive() == true, 로직 없음 → return  ┐ 소수 탈락
      │     ├── ...          // isActive() == true, 로직 없음 → return  ┘
      │     └── Command[n]   // 실제 실행 ← 극소수
      │
      └── 결과: N번 가상함수 호출(vtable lookup)
잘못된 결정 비용
전체를 flat array에 넣고 매번 전체 순회 약 800회 루프
활성화 여부 필터를 객체 내부에서 처리 호출 자체는 막을 수 없음
가상함수(동적 바인딩)로 구현 vtable lookup 800회, 인라인 최적화 불가
커맨드 등록/해제 없이 정적 배열 유지 이벤트가 꺼져도 계속 호출됨

가상함수 호출 1회의 비용

obj->execute() 호출
  ├── 1. obj 포인터 역참조 → vtable 포인터 로드
  │        └── L1 miss: +4~5ns / L2 miss: +12ns / DRAM miss: +60~100ns
  ├── 2. vtable에서 함수 포인터 로드 (간접 참조 1회 추가)
  │        └── 또 다른 캐시 미스 가능성
  ├── 3. 간접 점프 (indirect call)
  │        └── 분기 예측 실패 시: +15~20 cycle 패널티
  └── 4. 함수 본체 실행
           └── isActive() == false → return (본체가 이게 전부)

호출 흐름 (약 800회 가정)

execute() × 800회 (가상함수)
├── isActive()==false → return   : 700회  → 최악 (순수 낭비)
├── isActive()==true, 로직 없음  :  90회  → 그래도 낭비
└── 실제 실행                    :  10회  → 권장
시나리오 콜당 비용 약 800회 합계
L1 캐시 올인 ~3 ns ~2.5 μs
L2 캐시 히트 ~10 ns ~8.2 μs
LLC 히트 (현실적) ~30 ns ~24.6 μs
DRAM 미스 (최악) ~80 ns ~65.5 μs

<aside> ⚠️

위 표는 이론값 기반 추정이다. 매 틱 도는 핫 루프라면 포인터 배열은 순차접근으로 프리페치가 잘 되고, 같은 커맨드 타입 반복이면 vtable·분기예측기도 데워져 실제론 L1/L2(약 3~10ns)에 가까울 수 있다. 절대 수치는 환경에 따라 달라지므로 비율보다 "구조적 낭비가 있다"는 점에 주목할 것.

</aside>


권장 구조: 구독 기반 선별 실행

트리거 종류별로 구독자 목록을 두고, 해당 트리거의 구독자만 실행한다. 나머지는 호출조차 되지 않는다.