스택큐힙리스트

명령을 객체로! 커맨드 패턴으로 유연한 실행 로직 만들기 🚀 본문

개발

명령을 객체로! 커맨드 패턴으로 유연한 실행 로직 만들기 🚀

스택큐힙리스트 2025. 7. 28. 10:02
반응형

왜 또 커맨드 패턴인가?

버튼 하나에 기능을 자꾸 갈아끼워야 할 때마다 if-else 덩어리를 늘리는 대신, “명령 자체를 객체로 포장”하면 된다. 요청(함수 이름·인자·수행 시점)을 통째로 캡슐화해 큐에 쌓거나, 로그로 남기거나, 실행 ↔ 취소까지 자유자재로 다룰 수 있다는 얘기다. 국내 인기 블로그들도 “요청을 객체로 만들어 느슨한 결합을 얻는다”는 점을 1순위 장점으로 꼽는다.


패턴 한 줄 정의

Invoker(요청자)가 Command(명령 객체)를 실행하면, 명령은 Receiver(실제 작업자)에게 일을 시킨다.
Invoker는 “누가 어떻게 일하느냐”에 전혀 관여하지 않는다. 때문에 실행 시점 지연, 일괄 처리, undo/redo, 트랜잭션 롤백 같은 고급 기능을 끼워 넣기 쉽다.


핵심 구성 요소

  • Command 인터페이스: execute()·undo() 같은 동작 규약
  • ConcreteCommand: 실제 로직과 Receiver 참조 보관
  • Receiver: 명령을 수행할 도메인 객체
  • Invoker: 명령을 받아 실행·취소·재실행 등 타이밍 제어
  • Client: 누가 어떤 ConcreteCommand를 Invoker에 심을지 결정

코틀린 예제 – 리모컨 & 가전

// Command
fun interface Command { fun execute() }

// Receiver
class Light {
    fun on()  = println("💡 불 켜짐")
    fun off() = println("💡 불 꺼짐")
}

// ConcreteCommand
class LightOnCommand(private val light: Light) : Command {
    override fun execute() = light.on()
}

// Invoker
class RemoteControl {
    private val slots = mutableMapOf<Int, Command>()
    fun setCommand(slot: Int, cmd: Command) { slots[slot] = cmd }
    fun press(slot: Int) = slots[slot]?.execute()
}

// 사용
fun main() {
    val light = Light()
    val remote = RemoteControl()
    remote.setCommand(0, LightOnCommand(light))
    remote.press(0)   // 💡 불 켜짐
}

명령이 객체이므로 remote.setCommand()만으로 버튼 역할을 핫스왑할 수 있다. undo()를 추가해 실행 내역 스택을 관리하면 스마트홈 앱에서도 그대로 쓸 수 있다.


실전에서 빛나는 순간

  • Undo / Redo: 포토샵, IDE 단축키
  • 매크로·배치 작업: 여러 명령을 List로 묶어 한 번에 실행
  • CQRS/이벤트 소싱: “쓰기 명령(Command) vs 읽기(Query)” 분리 구조의 근간
  • 스프링 부트: ApplicationRunner, CommandLineRunner가 대표적인 Command 구현체다.

장점 🔥

  • 요청, 실행 시점을 분리해 스케줄링·트랜잭션 재시도가 간단
  • OCP 준수: 새 명령 클래스만 만들면 Invoker 수정 없음
  • 로깅·모니터링이 쉬워 운영 관점에서도 유리

주의할 점 ⚠️

  • 명령 종류가 폭증하면 클래스 수도 급격히 늘어난다.
  • 간단한 호출이면 람다 전달이 더 짧고 명료할 때도 있다.
반응형
Comments