2 분 소요

커맨드 패턴

정의

Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.

-GoF design pattern

요구사항(요청, 명령)을 객체로 캡슐화하여 서로 다른 요구사항을 가진 클라이언트를 매개변수화시킬 수 있고 요구사항을 큐에 넣어 로그를 남기거나 작업을 취소할 수 있는 기능을 가능하게 한다.

구조

img1

Invoker

요청을 받고 이를 실행시키는 역할을 수행한다. Command 인터페이스만 알고 있으며 실제로 Command가 어떻게 실행되는지는 알지 못한다.

Command

execute() 메소드를 선언한다.

ConcreteCommand

Command 인터페이스를 구현하며 맴버 변수로 Receiver를 가지고 있다. execute()메소드 안에서 Receiver 메소드를 호출하여 로직이 실행된다.

Receiver

실제 명령이 실행되는 부분이다.

특징

장점

1. 실제로 실행하는 객체(Receiver)와 실행을 요청하는 객체(Invoker)를 분리하여 결합도를 낮춘다.

Invoker는 Reciver를 알지 못한 채로 Command의 execute()메소드를 통해 실행을 하게 된다.

2. 기존의 코드를 수정하지 않고도 새로운 명령을 추가할 수 있다. (OCP 원칙)

새로운 명령이 추가되도 Invoker를 수정하지 않고 Revicer와 Command만 구현하면 된다.

단점

1. 새로운 명령이 생길때마다 클래스가 추가되기 때문에 코드 복잡성이 증가할 수 있다.

-> 이제 예제를 보면서 이해해보자!

예제

다음과 같은 기능을 가진 리모컨을 생각해보자.

리모컨 기능

  1. 전등 ON/OFF
  2. TV ON/OFF

이를 표현하기 위해서는 리모컨, 전등, TV 클래스가 필요할 것이다. 아래 코드를 보자.

Remote Class

img1

리모컨 클래스이다. 불끄기, 불켜기, Tv켜기, Tv끄기 메소드를 가진다.

Light & Tv Class

img1

만약 이 상황에서 새로운 요구사항이 생겼다고 가정해보자.

추가된 요구사항

  • 리모컨을 통해 프로젝트를 켜고 끄는 기능을 추가해주세요.

이를 만족시키기 위해서는 Remote 클래스에 맴버 변수로 Project를 추가하고 Project를 켜고 끄는 메소드를 추가해야 한다. 이는 기존 코드인 Remote를 수정하는 것이기 때문에 OCP를 위반하게 된다. 커맨드 패턴을 적용하면 요청을 호출하는 Remote 클래스와 실제 실행이 되는 Tv, Light 클래스 사이의 결합도를 낮추고 새로운 요구사항(프로젝트 Up/Down)이 생겼을 때 기존 코드(Remote 클래스)를 손보지 않고 기능을 추가할 수 있게 된다.

코드를 통해서 확인해보자!

커맨드 패턴을 적용한 코드

Command

img1

명령을 추상화한 Command 인터페이스이다.

Project (Receiver)

img1

새로 추가된 명령을 수행하는 Project 클래스이다. 위의 클래스 다이어그램에서 설명했던 Receiver 역할을 수행하는 클래스이다.

앞서 봤던 Light, Tv도 모두 Receiver이다. (Light, Tv는 코드가 동일하니 위의 코드를 참고하자.)

LightOnCommand/LightOffCommand (Concrete Command)

img1

Command 인터페이스를 구현하고 execute()메소드 안에서 Light(Receiver)의 메소드를 호출한다.

이를 통해 Remote 클래스(Invoker)와 Light 클래스(Receiver) 사이의 결합도를 낮추게 된다.

TvOnCommand/TvOffCommand (Concrete Command)

img1

위에 설명한 Light의 경우와 완전히 동일하다.

ProjectOnCommand/ProjectOffCommand (Concrete Command)

img1

마찬가지로 Light, Tv와 완전히 동일하니 설명은 생략한다.

Remote (Invoker)

img1

Remote 클래스는 Command 인터페이스에만 의존한다. 때문에 실제로 각 ConcreteCommand가 어떤 Recevier를 호출하는지 알지 못한다.

커맨드를 추가하는 setCommand(), On/Off 버튼을 누르는 onButtonPush(), offButtonPush()메소드를 가지고 있다.

이 함수 내부에서는 Command의 execute()를 호출하게 된다.

코드를 보면 알 수 있듯이 새로운 기능을 추가함에 있어서 기존 코드를 수정할 필요가 없다 (OCP원칙). 그저 새로운 Receiver (여기서는 Project)와 ConcreteCommand(ProjectOnCommand, ProjectOffComand)만 새로 만들면 된다.

정리

커맨드 패턴을 사용하면 실행을 요청하는 객체와 실제 실행하는 객체 사이의 결합도를 낮출 수 있다.

때문에 새로운 요구사항이 발생해도 유연하게 대처할 수 있다. 하지만 관리해야 하는 객체의 수가 늘어나기 때문에 무분별한 사용은 코드의 복잡도를 증가시킬 수 있다.

무작정 적용하기보다는 꼭 필요한 곳에만 사용하도록 하자.

참고

  • 헤드 퍼스트 디자인 패턴