Java/Spring

Command 패턴

코딩공부 2025. 3. 17. 23:39

1️⃣ Command 패턴이란?

Command 패턴은 요청(명령)을 객체로 캡슐화하여 실행, 취소, 저장 등을 관리할 수 있도록 하는 디자인 패턴
즉, "어떤 작업을 실행하는 코드"를 하나의 객체로 만들고, 이를 실행/취소/보관할 수 있도록 하는 패턴

 

📍 Command 패턴의 핵심 개념

  1. 명령을 객체(Command)로 만들어 독립적으로 다룸
  2. 명령을 실행하는 대상(Receiver)과 실행을 요청하는 객체(Invoker)를 분리 
  3. 명령을 저장하고 나중에 실행 가능
  4. Undo(실행 취소) 기능을 쉽게 구현

2️⃣ Command 패턴의 구조

Command 패턴은 다음과 같은 5가지 주요 컴포넌트로 구성

🔹 1. Command (명령 인터페이스)

  • 실행할 작업을 정의하는 인터페이스
  • 모든 명령은 execute() 메서드를 구현
  • 실행 취소(Undo)가 필요하다면 undo() 메서드도 추가 가능

🔹 2. ConcreteCommand (구체적인 명령)

  • Command 인터페이스를 구현하여 실제 동작을 수행하는 클래스를 만듬
  • 실행할 작업을 Receiver 객체에 전달

🔹 3. Receiver (작업을 실제 수행하는 객체)

  • 명령을 수행하는 실제 객체
  • Command 객체는 Receiver를 호출하여 작업을 실행

🔹 4. Invoker (명령을 실행하는 객체)

  • Command 객체를 실행하는 역할
  • 사용자는 Invoker를 통해 Command를 실행하거나 취소 가능

🔹 5. Client (클라이언트)

  • Command, Invoker, Receiver를 생성하고 연결하는 역할

 

 

3️⃣ Command 패턴 예제 

💡 전등 켜고 끄기

  • 스마트 홈에서 전등을 켜고 끄는 기능을 만들고 싶다.
  • 버튼을 누르면 전등이 켜지고(on), 다시 누르면 꺼진다(off).
  • 명령을 객체로 만들고 나중에 실행/취소할 수도 있어야 한다.

📌 Command 패턴으로 구현하기

/** 1️⃣ Command 인터페이스 (모든 명령의 공통 인터페이스) */
interface Command {
    fun execute()  // 명령 실행
    fun undo()     // 실행 취소 (Undo)
}

/** 2️⃣ Receiver (실제 동작을 수행하는 객체) */
class Light {
    fun turnOn() {
        println("💡 전등이 켜졌습니다!")
    }

    fun turnOff() {
        println("🔦 전등이 꺼졌습니다!")
    }
}

/** 3️⃣ ConcreteCommand (전등 켜기 명령) */
class LightOnCommand(private val light: Light) : Command {
    override fun execute() {
        light.turnOn()
    }

    override fun undo() {
        light.turnOff()
    }
}

/** 4️⃣ ConcreteCommand (전등 끄기 명령) */
class LightOffCommand(private val light: Light) : Command {
    override fun execute() {
        light.turnOff()
    }

    override fun undo() {
        light.turnOn()
    }
}

/** 5️⃣ Invoker (사용자가 버튼을 누르면 명령을 실행) */
class RemoteControl {
    private var command: Command? = null

    fun setCommand(command: Command) {
        this.command = command
    }

    fun pressButton() {
        command?.execute()
    }

    fun pressUndo() {
        command?.undo()
    }
}

/** 6️⃣ 클라이언트 코드 */
fun main() {
    val light = Light()  // 전등 객체 생성
    val lightOnCommand = LightOnCommand(light)  // 전등 켜기 명령 생성
    val lightOffCommand = LightOffCommand(light)  // 전등 끄기 명령 생성

    val remote = RemoteControl()  // 리모컨(Invoker) 생성

    // 전등 켜기 버튼 설정 및 실행
    remote.setCommand(lightOnCommand)
    remote.pressButton()  // 💡 전등이 켜졌습니다!
    
    // 실행 취소
    remote.pressUndo()  // 🔦 전등이 꺼졌습니다!

    // 전등 끄기 버튼 설정 및 실행
    remote.setCommand(lightOffCommand)
    remote.pressButton()  // 🔦 전등이 꺼졌습니다!

    // 실행 취소
    remote.pressUndo()  // 💡 전등이 켜졌습니다!
}

 

🔹 실행 결과

 

💡 전등이 켜졌습니다!
🔦 전등이 꺼졌습니다!
🔦 전등이 꺼졌습니다!
💡 전등이 켜졌습니다!

 

"전등을 켜고 끄는 기능"을 버튼으로 쉽게 조작할 수 있고, 실행 취소 기능도 간단하게 구현 가능!

 

 

💡실행 취소(Undo) 기능

👉 텍스트 편집기에서 Ctrl+Z(실행 취소) 기능 구현
👉 명령을 스택에 저장하고 undo()를 호출하면 이전 상태로 되돌림

 

 

import java.util.*

/** Command 인터페이스 */
interface Command {
    fun execute()
    fun undo()
}

/** Receiver (실제 텍스트를 수정하는 객체) */
class TextEditor {
    var text: String = ""

    fun addText(newText: String) {
        text += newText
        println("📝 현재 텍스트: $text")
    }

    fun removeText(count: Int) {
        text = text.dropLast(count)
        println("↩️ 실행 취소 후 텍스트: $text")
    }
}

/** ConcreteCommand (텍스트 추가 명령) */
class AddTextCommand(private val editor: TextEditor, private val text: String) : Command {
    override fun execute() {
        editor.addText(text)
    }

    override fun undo() {
        editor.removeText(text.length)
    }
}

/** Invoker (사용자가 실행하는 인터페이스) */
class EditorInvoker {
    private val commandStack = Stack<Command>()

    fun executeCommand(command: Command) {
        command.execute()
        commandStack.push(command)
    }

    fun undoLastCommand() {
        if (commandStack.isNotEmpty()) {
            val command = commandStack.pop()
            command.undo()
        } else {
            println("❌ 실행 취소할 명령이 없습니다.")
        }
    }
}

/** 클라이언트 코드 */
fun main() {
    val editor = TextEditor()
    val invoker = EditorInvoker()

    val command1 = AddTextCommand(editor, "Hello ")
    val command2 = AddTextCommand(editor, "Kotlin!")

    invoker.executeCommand(command1) // 📝 현재 텍스트: Hello 
    invoker.executeCommand(command2) // 📝 현재 텍스트: Hello Kotlin!

    invoker.undoLastCommand() // ↩️ 실행 취소 후 텍스트: Hello 
    invoker.undoLastCommand() // ↩️ 실행 취소 후 텍스트:
}

 

🔹 실행 결과

 

📝 현재 텍스트: Hello 
📝 현재 텍스트: Hello Kotlin!
↩️ 실행 취소 후 텍스트: Hello 
↩️ 실행 취소 후 텍스트:

 

💡 요청을 큐에 저장했다가 나중에 실행 (Job Queue)

👉 메시지 큐(MQ)나 비동기 작업 실행 시스템에서 활용
👉 웹 서버에서 사용자의 요청을 저장해두고, 특정 조건에서 실행

 

import java.util.*

/** Receiver (실제 작업을 수행하는 객체) */
class DataProcessor {
    fun process(data: String) {
        println("📊 데이터 처리 중: $data")
    }
}

/** Command 인터페이스 */
interface Job {
    fun execute()
}

/** ConcreteCommand (데이터 처리 작업) */
class DataProcessingJob(private val processor: DataProcessor, private val data: String) : Job {
    override fun execute() {
        processor.process(data)  // Receiver의 메서드를 호출하여 실행
    }
}

/** Invoker (Job Queue) */
class JobQueue {
    private val jobQueue: Queue<Job> = LinkedList()

    fun addJob(job: Job) {
        jobQueue.add(job)
        println("📌 작업이 큐에 추가됨: $job")
    }

    fun processJobs() {
        while (jobQueue.isNotEmpty()) {
            val job = jobQueue.poll()
            job.execute()
        }
    }
}

/** 클라이언트 코드 */
fun main() {
    val processor = DataProcessor()  // Receiver 객체 생성
    val queue = JobQueue()

    queue.addJob(DataProcessingJob(processor, "사용자 데이터 분석"))
    queue.addJob(DataProcessingJob(processor, "로그 파일 정리"))
    queue.addJob(DataProcessingJob(processor, "추천 시스템 업데이트"))

    println("🚀 큐에 있는 작업을 실행합니다.")
    queue.processJobs()
}

 

🔹 실행 결과

📌 작업이 큐에 추가됨: DataProcessingJob@xxxx
📌 작업이 큐에 추가됨: DataProcessingJob@xxxx
📌 작업이 큐에 추가됨: DataProcessingJob@xxxx
🚀 큐에 있는 작업을 실행합니다.
📊 데이터 처리 중: 사용자 데이터 분석
📊 데이터 처리 중: 로그 파일 정리
📊 데이터 처리 중: 추천 시스템 업데이트

 

 

💡 매크로 기능 (여러 명령을 하나로 묶기)

👉 여러 개의 명령을 묶어서 한 번에 실행
👉 게임에서 "콤보 스킬"을 하나의 명령으로 실행하는 경우

 

/** Command 인터페이스 */
interface MacroCommand {
    fun execute()
}

/** 개별 명령 */
class JumpCommand : MacroCommand {
    override fun execute() {
        println("🦘 캐릭터가 점프했습니다!")
    }
}

class AttackCommand : MacroCommand {
    override fun execute() {
        println("⚔️ 캐릭터가 공격했습니다!")
    }
}

class BlockCommand : MacroCommand {
    override fun execute() {
        println("🛡️ 캐릭터가 방어했습니다!")
    }
}

/** Macro Command (여러 명령을 묶어서 실행) */
class ComboCommand(private val commands: List<MacroCommand>) : MacroCommand {
    override fun execute() {
        commands.forEach { it.execute() }
    }
}

/** 클라이언트 코드 */
fun main() {
    val jump = JumpCommand()
    val attack = AttackCommand()
    val block = BlockCommand()

    val combo = ComboCommand(listOf(jump, attack, block))

    println("🔥 콤보 기술 실행!")
    combo.execute()
}

 

4️⃣ Command 패턴의 장점

1. 실행 요청과 실행 방법을 분리할 수 있다.

  • Invoker(버튼)는 Receiver(전등)가 어떻게 동작하는지 몰라도 됨.
  • 다른 기기(에어컨, TV)를 추가해도 Invoker 코드는 바뀌지 않음.

2. 실행 취소(Undo) 기능을 쉽게 추가할 수 있다.

  • undo() 메서드를 사용하면 이전 상태로 되돌릴 수 있음.

3. 여러 개의 명령을 묶어서 실행할 수 있다. (매크로 기능)

  • 여러 개의 명령을 List<Command>로 저장하고 한 번에 실행 가능.

4. 요청을 저장하고 나중에 실행할 수 있다. (Job Queue, Task Scheduling)

  • 명령을 큐에 저장해 두고 특정 조건에서 실행할 수도 있음.

5. 기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있다. (OCP 원칙 충족)

  • Receiver(전등)나 Invoker(리모컨)을 수정하지 않고 새로운 명령을 추가 가능.

 

5️⃣ Command 패턴을 언제 사용할까?

 

상황 Command 패턴 적용 이유
1. 버튼을 눌렀을 때 특정 동작 실행 실행과 요청을 분리하여 확장성을 높임
2. 실행 취소(Undo) 기능이 필요할 때 실행 취소를 쉽게 구현 가능
3. 여러 개의 명령을 조합하여 실행 (매크로 기능) 명령을 묶어서 한 번에 실행 가능
4. 요청을 저장하고 나중에 실행 (Job Queue) 메시지 큐나 작업 예약 시스템에 유용

 

🔥 결론

Command 패턴을 사용하면 "요청을 객체로 캡슐화하여 실행, 취소, 조합, 저장할 수 있음".
버튼 하나로 전등을 켜고 끄는 것처럼 쉽게 명령을 실행할 수 있음.
실행 취소(Undo), 여러 명령 묶기(매크로), Job Queue 등 다양한 곳에서 활용 가능.

즉, "명령을 관리해야 하는 경우" Command 패턴이 필수! 🚀