뒤로가기

커맨드 패턴: 요청을 객체로 캡슐화하는 디자인 패턴

design-pattern

커맨드 패턴은 요청을 객체의 형태로 캡슐화하여 사용자가 보낸 요청을 나중에 이용할 수 있도록 매개변수화하고, 큐에 넣거나 로그로 기록하거나 작업 취소 기능을 지원할 수 있게 해주는 소프트웨어 디자인 패턴입니다.

이 패턴의 핵심은 요청의 세부사항을 캡슐화하여 호출자와 수신자 사이의 결합도를 낮추고, 실행 취소, 재실행, 로깅 등의 기능을 쉽게 구현할 수 있다는 점입니다.

커맨드 패턴의 주요 구성 요소

  • Command: 실행될 기능에 대한 인터페이스를 정의합니다.
  • ConcreteCommand: Command를 구현하는 클래스로, 실제 작업을 Receiver에게 위임합니다.
  • Invoker: 기능의 실행을 요청하는 호출자 클래스입니다.
  • Receiver: ConcreteCommand에서 실제로 기능을 실행하는 수신자 클래스입니다.

Kotlin을 활용한 구현

Command 추상 클래스

abstract class Command {
    abstract fun execute(): String
    abstract fun undo(): String
}

모든 커맨드가 구현해야 할 기본 구조를 정의합니다. execute() 메서드는 커맨드를 실행하고, undo() 메서드는 실행을 취소합니다.

Receiver 클래스

class Kimchi() {
    fun cookKimchi(): String {
        return "Cook Kimchi"
    }
}

Kimchi 클래스는 실제 작업을 수행하는 Receiver 역할을 합니다. 커맨드 패턴에서는 비즈니스 로직이 Receiver에 위치하며, ConcreteCommand는 이를 호출하는 역할만 수행합니다.

ConcreteCommand 클래스

class CommandKimchi(private val kimchi: Kimchi) : Command() {
  private val prevCook: String = "None"
 
  override fun execute(): String {
    return kimchi.cookKimchi()
  }
 
  override fun undo(): String {
    return prevCook
  }
}

CommandKimchi는 구체적인 커맨드를 구현합니다. 이 클래스는 Kimchi 객체를 받아 실제 작업을 위임하며, 이전 상태를 저장하여 undo() 기능을 지원합니다.

NoCommand 클래스

class NoCommand : Command() {
  override fun execute(): String {
    return "No command"
  }
 
  override fun undo(): String {
    return "No command"
  }
}

NoCommand는 Null Object 패턴을 구현한 것으로, 커맨드가 설정되지 않았을 때의 기본 동작을 정의합니다. 이를 통해 null 체크를 제거하고 코드를 단순화할 수 있습니다.

Invoker 클래스

class InvokeCommand : Command() {
  private var commend: Command = NoCommand()
 
  fun setCommend(command: Command) {
    commend = command
  }
 
  override fun execute(): String {
    return commend.execute()
  }
 
  override fun undo(): String {
    return commend.undo()
  }
}

InvokeCommand는 Invoker 역할을 합니다. 이 클래스는 실제 커맨드 객체를 가지고 있으며, 클라이언트의 요청에 따라 해당 커맨드를 실행하거나 취소합니다.

테스트 코드

class CommandPatternTest {
    @Test
    fun testInvokeCommand() {
        val invoker = InvokeCommand()
 
        // 초기 상태 테스트 (NoCommand)
        assertEquals("No command", invoker.execute())
        assertEquals("No command", invoker.undo())
 
        // Kimchi 커맨드 설정 및 테스트
        val kimchi = Kimchi()
        val kimchiCommand = CommandKimchi(kimchi)
        invoker.setCommend(kimchiCommand)
 
        assertEquals("Cook Kimchi", invoker.execute())
        assertEquals("None", invoker.undo())
    }
}

커맨드 패턴의 장점

확장성

새로운 커맨드를 추가할 때 기존 코드를 수정할 필요가 없습니다. Command 인터페이스를 구현하는 새로운 클래스만 추가하면 됩니다.

단일 책임 원칙 준수

각 커맨드 클래스는 하나의 작업만 담당하므로, 코드의 책임이 명확하게 분리됩니다.

개방-폐쇄 원칙 준수

기존 코드를 수정하지 않고 새로운 커맨드를 추가할 수 있어 시스템의 확장성이 향상됩니다.

실행 취소와 재실행 지원

undo() 메서드를 통해 작업 취소 기능을 쉽게 구현할 수 있으며, 커맨드 히스토리를 유지하면 재실행도 가능합니다.

요청의 큐잉과 로깅

커맨드 객체를 저장하거나 로그로 남길 수 있어 복잡한 연산을 관리하고 추적하기 용이합니다.

실제 활용 사례

텍스트 에디터

실행 취소/재실행 기능을 구현할 때 각 편집 작업을 커맨드 객체로 캡슐화하여 히스토리를 관리합니다.

작업 큐 시스템

백그라운드에서 수행할 작업들을 커맨드 객체로 만들어 큐에 저장하고 순차적으로 실행합니다.

트랜잭션 시스템

데이터베이스 작업을 커맨드로 캡슐화하여 롤백과 커밋을 쉽게 관리할 수 있습니다.

결론

커맨드 패턴은 요청을 객체로 캡슐화하여 클라이언트와 수신자 사이의 의존성을 줄이고, 유연한 시스템 설계를 가능하게 합니다. 실행 취소, 재실행, 로깅과 같은 고급 기능을 구현해야 하는 경우 커맨드 패턴을 활용하면 효과적입니다.

이 패턴을 통해 개발자는 복잡한 작업을 단순한 인터페이스로 추상화하고, 시스템의 확장성과 유지보수성을 향상시킬 수 있습니다.

관련 아티클