🎯 커맨드 패턴이란 무엇일까??
요청을 객체의 형태로 캡슐화하여 매개변수화하는 행동 디자인 패턴
수행할 동작을 객체 안에 캡슐화해 요청자(호출자)와 수신자(실행자) 사이를 분리합니다.
어떤 작업 => 객체가 되는거고 필요에 따라 객체에게 전달.
📺 리모콘 예시로 이해하기
버튼 → 누르기 → 명령 → TV (On/Off)
- 버튼은 TV에 대해 아무것도 모름 (삼성TV인지, LG TV인지)
- 연결된 명령을 통해 TV를 제어
- 요청자와 수신자가 완전히 분리
🔧 해결하는 문제들
1. 코드 결합도 감소
- 요청자와 수신자 간의 직접 연결 제거
2. 작업의 지연 및 예약 실행
- 명령을 즉시 실행하지 않고 저장했다가 나중에 실행
3. 작업 취소 기능
- 이전 상태를 저장하여 실행 취소 기능을 쉽게 구현
4. 작업 로깅
- 모든 변경사항을 추적하고 필요시 재실행 가능
구성 요소
- Command (명령): 작업 수행에 필요한 인터페이스를 선언합니다.
- ConcreteCommand (구체적 명령): Command 인터페이스를 구현하고 Receiver와 작업을 연결합니다.
- Invoker (호출자): 명령 객체를 저장하고 실행을 요청합니다.
- Receiver (수신자): 실제 작업을 수행하는 객체입니다.
- Client (클라이언트): ConcreteCommand를 생성하고 Receiver를 설정합니다.
// Command: 명령 인터페이스
protocol Command {
func execute()
func undo()
}
// Receiver: 실제 작업 수행
class Light {
var isOn = false
func turnOn() {
isOn = true
print("💡 불이 켜졌습니다!")
}
func turnOff() {
isOn = false
print("🌑 불이 꺼졌습니다.")
}
}
// ConcreteCommand: 구체적인 명령 구현
class LightOnCommand: Command {
private let light: Light
init(light: Light) {
self.light = light
}
func execute() {
light.turnOn()
}
func undo() {
light.turnOff()
}
}
// Invoker: 명령 실행을 요청
class RemoteControl {
private var command: Command?
func setCommand(_ command: Command) {
self.command = command
}
func pressButton() {
command?.execute()
}
func pressUndoButton() {
command?.undo()
}
}
// Client: 객체들을 조합하여 사용
let light = Light() // 수신자 생성
let lightOn = LightOnCommand(light: light) // 명령 생성
let remote = RemoteControl() // 호출자 생성
remote.setCommand(lightOn) // 명령 설정
remote.pressButton() // 실행 → 💡 불이 켜졌습니다!
remote.pressUndoButton() // 취소 → 🌑 불이 꺼졌습니다.
언제사용할까?
1. 📱 UI와 비즈니스 로직 분리
복잡한 UI 시스템에서 다양한 요소가 동일한 작업을 트리거해야 할 때
- UI Layer와 비즈니스 로직 레이어를 깔끔하게 분리
- 테스트 및 유지보수 용이성 향상
2. ⏰ 작업 지연, 대기열, 스케줄링
- API 제한: 작업을 대기열에 넣고 순차적으로 처리
- 네트워크 불안정: 모바일 앱에서 작업을 로컬 저장 후 연결 복원 시 실행
3. 💰 트랜잭션 시스템 구현
금융 앱이나 다단계 프로세스에서 롤백이 필요한 경우
// 송금 트랜잭션 처리
class TransferMoneyTransaction: Command {
private let sourceAccount: Account
private let targetAccount: Account
private let amount: Decimal
private let commands: [Command]
init(sourceAccount: Account, targetAccount: Account, amount: Decimal) {
self.sourceAccount = sourceAccount
self.targetAccount = targetAccount
self.amount = amount
// 트랜잭션을 구성하는 개별 커맨드들
self.commands = [
DeductAmountCommand(account: sourceAccount, amount: amount),
AddAmountCommand(account: targetAccount, amount: amount),
LogTransactionCommand(sourceId: sourceAccount.id, targetId: targetAccount.id, amount: amount)
]
}
func execute() {
// 모든 단계를 실행하되, 하나라도 실패하면 롤백
for command in commands {
do {
try command.execute()
} catch {
// 오류 발생 시 이전에 실행된 모든 커맨드 취소
for i in stride(from: commands.firstIndex(where: { $0 === command })! - 1, through: 0, by: -1) {
commands[i].undo()
}
throw error
}
}
}
func undo() {
// 역순으로 모든 커맨드 취소
for command in commands.reversed() {
command.undo()
}
}
}
4. 작업 기록 및 로깅이 필요할 때 📝
특히 효과적인 상황:
- 감사 추적이 필요한 앱: 사용자의 중요한 작업을 기록해야 하는 경우
- 진단 목적: 문제 해결을 위해 사용자 작업 순서를 재현해야 하는 경우
커맨드 패턴 사용의 주의사항
효과적인 커맨드 패턴 구현을 위한 주의사항:
- 과도한 사용 지양: 모든 작업을 커맨드로 만들면 코드가 불필요하게 복잡해질 수 있습니다.
- 메모리 관리: 많은 커맨드를 저장할 경우 메모리 사용량이 증가할 수 있으므로 적절한 제한이 필요합니다.
- 직렬화 고려: 커맨드를 저장하거나 전송할 경우 직렬화/역직렬화 메커니즘이 필요합니다.
- 복잡한 상태 관리: 상태 변경이 복잡한 경우 메멘토 패턴과 결합하는 것이 좋습니다
요약
요청을 객체의 형태로 캡슐화하는 행동 디자인 패턴입니다. 특정 작업을 객체로 변환해서 요청자와 실행자를 분리하는 패턴이죠. 텍스트 에디터의 undo/redo 기능이나 네트워크 연결이 불안정할때 작업을 대기열에 저장했다가 나중에 실행하는 경우에 유용합니다. 코드의 결합도를 낮추면서도 새로운 명령을 추가하기도 용이해 확장성이 좋습니다.
참고자료
https://refactoring.guru/ko/design-patterns/command
커맨드 패턴
/ 디자인 패턴들 / 행동 패턴 커맨드 패턴 다음 이름으로도 불립니다: 액션, 트랜잭션, Command 의도 커맨드는 요청을 요청에 대한 모든 정보가 포함된 독립실행형 객체로 변환하는 행동 디자인
refactoring.guru
'디자인패턴' 카테고리의 다른 글
데코레이터패턴 (1) | 2025.05.19 |
---|---|
파사드 패턴(Facade Pattern) (0) | 2025.03.25 |
팩토리 패턴 (0) | 2025.03.25 |
Adaptor Pattern (구조적 디자인패턴) (0) | 2025.03.24 |