본문 바로가기

소프트웨어/디자인패턴

[소프트웨어/디자인패턴] 커맨드 패턴(Command Pattern)

커맨드 패턴은 명령을 추상화한 패턴이다.

Context

보통 램프를 크고 켜는 버튼을 만든다고 생각한다면, 버튼 내에 램프 필드가 존재하고 이를 이용하는 Pressed라는 메서드를 구현함으로써 아래 다이어그램과 같이 구현할 수 있다.


이를 코드로 나타내면 비교적 간단하게 프로그래밍할 수 있다.

class Lamp {
    public void turnOn() {
        System.out.println("Lamp On");
    }
}

class Button {
    private Lamp theLamp;

    public Button (Lamp lamp) {
        theLamp = lamp;
    }

    public void pressed() {
        theLamp.turnOn();
    }
}

Problem

변경에 대해 안전한가? (OCP를 준수하는가)

먼저, 버튼이 Lamp를 켜는 것에서 Alarm을 울리는 것으로 기능을 변경하고자 한다. 일반적으로 아래 다이어그램처럼 생각할 것이다.

단순히 보자면 그냥 기존의 Lamp에서 Alarm클래스로 변경이 되었을 뿐이다, 이것이 무엇이 문제일까? 일단 아래와 같이 코딩을 해보았다.

class Alarm {
    public void start() {
        System.out.println("Alarming....");
    }
}

class Button {
    private Alarm alarm;

    public Button(Alarm alarm) {
        this.alarm = alarm;
    }

    public void pressed() {
        alarm.start();
    }
}

클래스 Alarm이 추가 되었고 기존의 Button의 코드가 변경이 되었다. 기본적으로 SOLID 중 OCP는 기능의 변경, 확장에 대해선 열려있으나, 기존 코드의 수정에 대해서는 닫혀 있어야한다. 하지만 위 예제에서는 기존의 Button클래스가 변경되었으니 OCP원칙을 위배했다고 할 수 있다. (Button의 기능을 추가한 것이 아니다, 변경한 것이다.)

기능의 확장에 대해서 안전한가?

기존 Button의 기능을 추가하고 싶다. 기존의 Lamp에 대해서만 켜는 것이 아닌 Alarm도 켰으면 좋겠다. 이를 위해선 현재 Button이 어떤 것을 담당하고 있는지 알고 있어야한다, 현재 ModeLamp인지 Alarm인지 식별을 하는 방식으로 구현이 가능하다. 현재 Mode구분하여 실행하면 된다. 우선 이와같은 내용을 UML로 확인하면

이를 이용하여 코드로 구현해보자

class Lamp {
    public void turnOn() {
        System.out.println("Lamp on");
    }
}

class Alarm {
    public void start() {
        System.out.println("Alarming...");
    }
}

enum Mode { LAMP, ALARM }

class Button {
    private Lamp lamp;
    private Alarm alarm;
    private Mode mode;

    public Button(Lamp lamp, Alarm alarm) {
        this.lamp = lamp;
        this.alarm = alarm;
        this.mode = Mode.LAMP;
    }

    public void setMode(Mode mode) {
        this.mode = mode;
    }

    public void pressed() {
        switch (mode) {
            case ALARM:
                alarm.start();
                break;
            case LAMP:
                lamp.turnOn();
                break;
        }
    }
}

역시 수많은 코드의 변경이 발생했다, 버튼 하나의 기능 추가했다고 해서 Button클래의 전반적인 구조와 의존 대상이 현격하게 증가했다. 기능이 점점 추가될수록 이러한 일이 많아 질 것이다.

참고. Mode는 상태가 아니다, 하지만 상태처럼 볼 수는 있다. UML의 상태 다이어그램처럼 어떠한 이벤트에 의해 상태는 전이되어야 한다, 하지만 Button을 누른다고 해서 상태가 전이되지는 않는다.

Solution

대부분의 클라이언트 프로그램 프로젝트에서는 프로젝트의 일정이 중간 쯤 오면 이러한 문제 안고있다. Command 패턴은 이러한 문제에 직면했을 때 적용하기 좋은 디자인 패턴이다. 버튼 특정한 기능을 수행하라고 Button이나 Lamp의 특정한 기능을 호출한다. 이 호출하는 것 즉 명령을 인터페이스로 추상화하여 호출자(Button)과 호출을 수신하는 수신자(Button, Lamp)의 직접 의존관계를 제거해버리면 된다. 그리고 호출자(Button)은 인터페이스로 추상화한 명령(Command)를 호출하여 간접적으로 수신자의 기능을 실행시킬 수 있다. 이와 같은 내용을 UML로 설계해보면 아래와 같이 나온다.


호출자(Invoker, Button)는 오직 호출(명령)을 추상화한 인터페이스(Command)만을 의존하고 있고, 인터페이스의 구현체(Concrete Command, LampOnCommand or AlarmStartCommand)들 호출자가 간접 의존하여 실행시킨다. 이를 통해 실제 수진자(Receiver, Alarm or Lamp)가 호출을 받고 실행한다. 이를 구현한 코드는 아래와 같다.

class Button {
    private Command command;

    public Button(Command command) {
        this.command = command;
    }

    public void setCommand(Command command) {
        this.command = command;
    }

    public void pressed() {
        command.excute();
    }
}

interface Command {
    public void excute();
}

class LampOnCommand implements Command {
    private Lamp lamp;

    public LampOnCommand(Lamp lamp) {
        this.lamp = lamp;
    }

    @Override
    public void excute() {
        lamp.turnOn();
    }
}

class AlarmStartCommand implements Command{
    private Alarm alarm;

    public AlarmStartCommand(Alarm alarm) {
        this.alarm = alarm;
    }

    @Override
    public void excute() {
        alarm.start();
    }
}

class Lamp {
    public void turnOn() {
        System.out.println("turn on lamp");
    }
}

class Alarm {
    public void start() {
        System.out.println("start alarm");
    }
}

자 여기서 변경을 시도해봄으로써 실제 OCP를 만족하는지 확인할 수 있다.

고객 : Button을 누를 때, LampAlarm을 켜고, Window를 열게 해주세요.

참으로 짜증나는 SR이다. Solution에서 아래처럼 Window 클래스를 생성하고 Lamp를래스와 함께 AlarmStartCommand에 의존성을 추가하고 실행명령을 전달하면 된다. 해당 내용을 아래와 같이 표현할 수 있다.

이를 기반한 코드이다.

class Button {
    private Command command;

    public Button(Command command) {
        this.command = command;
    }

    public void setCommand(Command command) {
        this.command = command;
    }

    public void pressed() {
        command.excute();
    }
}

interface Command {
    public void excute();
}

class LampOnCommand implements Command {
    private Lamp lamp;

    public LampOnCommand(Lamp lamp) {
        this.lamp = lamp;
    }

    @Override
    public void excute() {
        lamp.turnOn();
    }
}

class AlarmStartCommand implements Command{
    private Alarm alarm;
    private Lamp lamp;
    private Window window;

    public AlarmStartCommand(Alarm alarm, Lamp lamp, Window window) {
        this.alarm = alarm;
        this.lamp = lamp;
        this.window = window;
    }

    @Override
    public void excute() {
        alarm.start();
        lamp.turnOn();
        window.open();
    }
}

class Lamp {
    public void turnOn() {
        System.out.println("turn on lamp");
    }
}

class Alarm {
    public void start() {
        System.out.println("start alarm");
    }
}

class Window {
    public void open() {
        System.out.println("open window");
    }

    public void close() {
        System.out.println("close window");
    }
}

하지만 호출자(Button)의 코드는 단 하나의 변경도 없던걸 통해서 OCP를 만족하는 설계를 증명할 수 있다.

Summary

커맨드 패턴은 실행될 기능을 캡슐화함으로써 기능의 실행을 요구하는 호출자클래스와 실제 기능을 실행하는 수신자 클래스 사이의 의존성을 제거한다.