Ch.6 커맨드 패턴

호출 캡슐화하기

About

메소드 호출을 캡슐화하여 캡슐화를 더 높은 수준으로 끌어올려 보자.

Requirements

예제로는 만능 IOT 리모콘이 나온다.

  • 프로그래밍이 가능한 7개의 슬롯과 각 슬롯에 할당된 기능을 켜고 끄는 ON/OFF 스위치가 달려 있다.

  • 각 슬롯은 서로 다른 가정용 기기에 연결할 수 있다.

  • 리모콘에는 작업 취소 버튼도 장착되어 있다.

  • 제어할 수 있는 클래스를 같이 요구사항에 동봉하는데, 어떤 기기의 인터페이스는 다른 기기와 매우 다르다.

슬슬 이런 리모콘(universal TV remotes)이 생각나기 시작한다.

Flow

다음과 같은 로직으로 돌아간다고 볼 수 있다.

  1. 클라이언트는 커맨드 객체를 생성한다. 커맨드 객체는 리시버에 전달할 일련의 행동으로 구성된다.

    커맨드 객체에는 행동리시버(Receiver)의 정보가 같이 들어있다.

  2. 커맨드 객체에서 제공하는 메소드는 execute() 하나 뿐이다. 이 메소드는 행동을 캡슐화하며, 리시버에 있는 특정 행동을 처리한다.

  3. 클라이언트는 인보커(Invoker) 객체setCommand() 메소드를 호출하는데, 이때 커맨드 객체를 넘겨준다. 그 커맨드 객체는 나중에 쓰이기 전까지 인보커 객체에 보관된다.

  4. 인보커에서 커맨드 객체의 execute() 메소드를 호출하면, 리시버에 있는 행동 메소드가 호출된다.

커맨드 객체 만들기

interface Command {
  execute: () => void
}
  • 커맨드 객체에는 execute()라는 이름을 가진 메소드 하나만 정의한다.

class LightOnCommand implements Command {
  light: Light
  
  constructor(light: Light) {
    this.light = light
  }
  
  execute(): void {
    light.on()
  }
}

제공된 Light 클래스에는 on()off() 메서드가 있으니 execute 메서드를 위와 같이 구현할 수 있다.

커맨드 객체 사용하기

class SimpleRemoteControl {
  slot: Command
  
  constructor() {}
  
  setCommand(command: Command): void {
    this.slot = command
  }
  
  buttonWasPressed(): void {
    this.slot.execute()
  }
}
function main() {
  const remote: SimpleRemoteControl = new SimpleRemoteControl() // 인보커 생성. 필요한 작업을 요청할 때 사용할 커맨드를 인자로 받는다.
  const light: Light = new Light() // 리시버 생성
  const lightOnControl: LightOnControl = new LightOnControl(light) // 커맨드 객체 생성 및 리시버 전달
  
  remote.setCommand(lightOn) // 커맨드 객체를 인보커에게 전달
  remote.buttonWasPressed() // 버튼을 눌러본다.
}

main()

커맨드 패턴의 정의

커맨드 패턴(Command Pattern)을 사용하면 요청 내역을 객체로 캡슐화해서 객체를 서로 다른 요청 내역에 따라 매개변수화 할 수 있다. 이러면 요청을 큐에 저장하거나 로그로 기록하거나 작업 취소 기능을 사용할 수 있다.

  • 아직 커맨드 객체를 써서 큐와 로그를 구현하거나 작업을 취소하는 방법은 배우지 못했다.

  • 기본적인 커맨드 패턴을 제대로 사용할 수 있으면 메타 커맨드 패턴(Meta Command Pattern)도 그리 어렵지 않게 구현할 수 있다. 메타 커맨드 패턴을 사용하면 여러 개의 명령을 매크로로 한번에 실행할 수 있다.

The Code

커맨드 패턴 활용하기

커맨드로 컴퓨테이션(computation)의 한 부분(리시버와 일련의 행동)을 패키지로 묶어서 일급 객체 형태로 전달할 수도 있다.

  • 그러면 클라이언트 애플리케이션에서 커맨드 객체를 생성한 뒤 오랜 시간이 지나도 그 컴퓨테이션을 호출할 수 있다. 심지어 다른 스레드에서 호출할 수도 있다.

  • 이 점을 활용해 커맨드 패턴을 스케줄러나 스레드 풀, 작업 큐와 같은 다양한 작업에 활용할 수 있다.

작업 큐를 떠올려 보자.

  • 큐 한쪽 끝은 커맨드를 추가할 수 있도록 되어 있고, 다른 쪽 끝에는 커맨드를 처리하는 스레드들이 대기하고 있다.

  • 각 스레드는 우선 execute() 메소드를 호출하고 호출이 완료되면 커맨드 객체를 버리고 새로운 커맨드 객체를 가져온다.

여기서 작업 큐 클래스는 계산 작업을 하는 객체들과 완전히 분리되어 있다.

  • 한 스레드가 한동안 금융 관련 계산을 하다가 잠시 후에는 네트워크로 뭔가를 내려받을 수도 있다.

  • 작업 큐 객체는 전혀 신경 쓸 필요가 없다.

  • 큐에 커맨드 패턴을 구현하는 객체를 넣으면 그 객체를 처리하는 스레드가 생기고 자동으로 execute() 메소드가 호출된다.

커맨드 패턴 더 활용하기

어떤 애플리케이션은 모든 행동을 기록해 두었다가 애플리케이션이 다운되었을 때 그 행동을 다시 호출해서 복구할 수 있어야 한다.

  • 커맨드 패턴을 사용하면 store(), load() 메소드를 추가해 이 기능을 구현할 수 있다.

  • 자바에서는 이런 메소드를 객체 직렬화로 구현할 수도 있지만, 직렬화와 관련된 제약 조건 때문에 그리 쉽지는 않다.

로그 기록은 어떤 명령을 실행하면서 디스크에 실행 히스토리를 기록하고, 애플리케이션이 다운되면 커맨드 객체를 다시 로딩해서 execute() 메소드를 자동으로 순서대로 실행하는 방식으로 작동한다.

지금까지 예로 든 리모컨에는 이런 로그 기록이 무의미하다.

  • 하지만 데이터가 변경될 때마다 매번 저장할 수 없는 방대한 자료구조를 다루는 애플리케이션에 로그를 사용해서 마지막 체크 포인트 이후로 진행한 모든 작업을 저장한 다음 시스템이 다운되었을 때 최근 수행된 작업을 다시 적용하는 방법으로 사용할 수 있다.

스프레드시트 애플리케이션을 예로 들어 보자.

  • 매번 데이터가 변경될 때마다 디스크에 저장하지 않고, 특정 체크 포인트 이후의 모든 행동을 로그에 기록하는 방식으로 복구 시스템을 구축할 수 있다.

  • 더 복잡한 애플리케이션에서는 이런 테크닉을 확장해서 일련의 작업에 트랜잭션을 활용해서 모든 작업이 완벽하게 처리되도록 하거나, 아무것도 처리되지 않게 롤백하도록 할 수 있다.

Last updated