[디자인 패턴] Command Pattern

전성훈·2023년 10월 31일
0

DesignPattern

목록 보기
1/18
post-thumbnail

주제: SIP, OCP를 준수하기 위해


커맨드 패턴이란 요청을 객체의 형태로 캡슐화하여 사용자가 보낸 요청을 나중에 이용할 수 있도록 메서드 이름, 매개변수 등 요청에 필요한 정보를 저장 또는 로깅, 취소할 수 있게 하는 패턴이다.

Command Pattern이란?

  • 요청 자체를 객체로 캡슐화하여 클라이언트와 수신자를 분리하는 패턴이다. 이 패턴을 사용하면 요청을 큐에 저장하거나 로그에 기록하고, 작업을 취소할 수 있다.
  • UI 로직과 비즈니스 로직을 연결할 때 커맨드 패턴은 이를 명령을 가진 인터페이스로 추상화한다.
  • Command 패턴은 다양한 입력 방식(저장 버튼, 키보드 단축키 등 등)과 저장 기능을 가진 비즈니스 로직 개체 간의 중간다리 역할을 한다. 그러면 UI 개체들은 어떤 비즈니스 로직이 요청을 수신하고 처리하는지 알 필요 없이 Command의 명령을 실행하기만 하면 된다.

패턴의 구조

command

  • Command
    - 명령을 수행하는 인터페이스를 정의한다.
  • ConcreteCommand
    - Command 인터페이스를 구현하고, Receiver 객체와 액션 간의 바인딩을 설정한다.
    - 즉, 인터페이스를 실제로 실행되는 개체이다.
  • Invoker
    - 내부의 Command를 이용해 요청을 시작하는 개체이다. 요청을 수신자에게 직접 보내는 대신 갖고 있는 Command의 명령을 실행한다.
    - ConcreteCommand를 통해 명령을 수행한다.
  • Receiver
    - 요청에 관련된 작업을 수행한다.
  • Client
    - 구체적인 명령을 만들고 객체들을 구성한다. ConcreteCommand를 만들어 수신자에 설정해주는 일을 한다.
  • 여기서 눈여겨봐야 할 부분은 호출자와 수신자의 의존성이 없다는 것이다. 두 개체간의 의존도가 떨어지기 때문에 기능의 변경이 있어도 호출자 개체를 수정 없이 그대로 사용할 수 있는 장점이 있다.

장단점

장점

  • 분리와 재사용
    - Command 패턴은 Invoker와 Receiver를 분리한다. 이로 인해 코드의 재사용성이 향상된다.
  • 명령 취소 및 재실행
    - Command 패턴은 명령의 실행을 취소하거나 재실행하는 기능을 쉽게 구현할 수 있다.
  • 명령 저장 및 로깅
    - 명령을 저장하고 로그를 남기는 기능을 쉽게 구현할 수 있다.
  • SOLID
    - 단일 책임 원칙(SIP) 에 따라 작업을 수행하는 클래스와 작업을 호출하는 클래스를 분리할 수 있다.
    - 개방/폐쇄 원칙(OCP) 에 따라 기존 코드를 수정하지 않고 앱에 새로운 명령을 도입할 수 있다.

단점

  • 코드의 복잡도 증가
    - 호출자와 수신자 사이에 인터페이스를 구현하기 때문에 코드가 복잡해질 수 있다.
    - 각각의 명령에 대해 별도의 ConcreteCommand 클래스를 생성해야 하므로, 클래스 수가 증가할 수 있다.

예시 코드

패턴의 활용 방법

struct TV { 
	func turnOn() { 
		print("TV가 켜졌습니다")
	}
}

struct HomeApp { 
	private let tv: TV
	
	init(tv: TV) { 
		self.tv = tv
	}
	
	func pressButton() { 
		tv.turnOn()
	}
}

func turnOnTV() { 
	let tv = TV()
	let homeApp = HomeApp(tv: tv)
	homeApp.touch()
}

turnOnTV()
// TV가 켜졌습니다
  • 이 코드는 기능의 확장이 필요로 하거나, 변경이 있을 때 많은 수정을 거칠 수 밖에 없다. 그렇게 되면 OCP에 위배된다. 이러한 문제를 커맨드 패턴을 이용해 해결할 수 있다.
import UIKit

// Command
protocol Command {
    func execute()
}

// Concrete Command
struct turnOnTVCommand: Command {
    private var tv: TV // Receiver
    
    init(tv: TV) {
        self.tv = tv
    }
    
    func execute() {
        tv.turnOn()
    }
}

struct changeChannelCommand: Command {
    private var tv: TV // Receiver
    private var channel: String
    
    init(tv: TV, channel: String) {
        self.tv = tv
        self.channel = channel
    }
    
    func execute() {
        tv.change(channel)
    }
}

// Receiver
struct TV {
    func turnOn() {
        print("TV가 켜졌습니다.")
    }
    
    func change(_ channel: String) {
        print("TV \(channel)번 채널을 틀었습니다.")
    }
}

// Invoker
final class HomeApp {
    private var redButton: Command?
    private var numberButton: Command?
    
    func setCommand(redButton: Command, numberButton: Command) {
        self.redButton = redButton
        self.numberButton = numberButton
    }
    
    func pressRedButton() {
        // Command execute() 호출
        redButton?.execute()
    }
    
    func pressNumberButton() {
        numberButton?.execute()
    }
}

let homeApp = HomeApp()
let newTV = TV()
let turnOnTV = turnOnTVCommand(tv: newTV)
let changeChannelOfTV = changeChannelCommand(tv: newTV, channel: "14")

homeApp.setCommand(redButton: turnOnTV, numberButton: changeChannelOfTV)

homeApp.pressRedButton()
homeApp.pressNumberButton()
  • 수신자는 TV가 켜지거나, 입력받은 채널로 돌려주는 실제 기능을 하는 TV 개체이다. 호출자는 내부에 redButton과 numberButton을 TV가 아닌 Command 타입으로 갖고 있다.
  • 이렇게 프로토콜 타입을 지정해주면 Command를 채택하는 모든 개체가 버튼안에 들어갈 수 있다.
  • 그래서 버튼을 누르는 메서드 내부에서 Command의 execute()를 호출하면 된다.
  • 호출자는 버튼 내의 내부 구현을 알 필요가 없어진다.

사용자 프로필 업데이트 예

import UIKit

// Command
protocol Command {
    func execute()
}

// Network Service
protocol NetworkService {
    func updateProfile(data: ProfileData, completion: @escaping (Result<ProfileData, Error>) -> Void)
}

// Receiver
class ProfileNetworkService: NetworkService {
    func updateProfile(data: ProfileData, completion: @escaping (Result<ProfileData, Error>) -> Void) {
        // 프로필을 업데이트 하기 위한 네트워크 요청 
    }
}

// Profile Data
struct ProfileData {
    let name: String
    let email: String
}

// Update Profile Command
class UpdateProfileCommand: Command {
    private let service: NetworkService
    private let data: ProfileData

    init(service: NetworkService, data: ProfileData) {
        self.service = service
        self.data = data
    }

    func execute() {
        service.updateProfile(data: data) { result in
            switch result {
            case .success(let updatedData):
                print("Profile updated successfully: \(updatedData)")
            case .failure(let error):
                print("Failed to update profile: \(error)")
            }
        }
    }
}

// Invoker
class ProfileUpdateViewController: UIViewController {
    private let profileService: NetworkService
    private var pendingCommands: [Command] = []

    init(profileService: NetworkService) {
        self.profileService = profileService
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func updateUserProfile(with data: ProfileData) {
        let command = UpdateProfileCommand(service: profileService, data: data)
        command.execute()
        pendingCommands.append(command)
    }

    func retryPendingCommands() {
        for command in pendingCommands {
            command.execute()
        }
        pendingCommands.removeAll()
    }
}
  • Receiver는 NetworkServe와 그 구현체인 ProfileNetworkService가 이에 해당한다. UpdateProfileCommandNetworkService를 통해 프로필 업데이트 요청을 보낸다.
  • Invoker는 ProfileUpdateViewController가 이에 해당한다.
  • 이러한 요소들을 통해 Command 패턴은 작업을 객체로 캡슐화하고, 이를 필요에 따라 저장, 전달, 실행할 수 있게 한다. 이는 작업의 재사용, 작업의 취소 및 재실행, 작업의 로깅 등 다양한 기능을 가능하게 한다.

출처(참고문헌)

제가 학습한 내용을 요약하여 정리한 것입니다. 내용에 오류가 있을 수 있으며, 어떠한 피드백도 감사히 받겠습니다.

감사합니다.

0개의 댓글