본문 바로가기

Backend/Java Design Pattern

[JAVA 디자인 패턴] 커맨드 패턴 (Command Pattern)

728x90

* 커맨드 패턴 (Command pattern)

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

 

 

 

기본구조

* 커맨드 패턴이 왜 필요한지에 대해서 알아보자.

 

 눌리면 특정 기능을 수행하는 버튼을 생각해보자. 버튼 눌렸을때 불이 켜지는 프로그램을 개발하려면, 
버튼을 눌려졌음을 인식하는 Button 클래스, 불을 켜는 기능을 하는 Lamp 클래스, 버튼을 눌렸을 댸 램프를 켜려면, Button 클래스는 Lamp 객체를 참조 해야 한다.

 

이 버튼을 만들고자 한다면 이렇게 만들 수 있을 것이다.

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

class Button{
	private Lamp theLamp;
	
	public Button(Lamp theLamp) {
		this.theLamp = theLamp;
	}
	
	public void pressed() {
		theLamp.turnOn();
	}
}

public class Main {

	public static void main(String[] args) {
		Lamp lamp = new Lamp();
		Button lampButton = new Button(lamp);
		lampButton.pressed();
	}
}

 

그렇다면, 다음과 같은 요구사항이 있으면 어떨까?

 

(1) 누군가 버튼을 눌렀을 때 램프가 켜지는 대신 다른 기능을 실행하게 하려면, 어떤 변경 작업을 해야 하는가?

 -> 버튼을 눌렀을 때 알람이 시작되게 하려면? 

(2) 버튼을 누르는 동작에 따라 다른 기능을 실행하게 하려면 어떤 변경 작업을 해야 하는가? 

-> 버튼을 처음 눌렀을 때, 램프를 켜고 , 두번째 눌렀을 때 알람을 동작하게 하려면?

 

(1) 을 참고하여 다른 기능을 수행하고자, Alarm을 울리는 클래스를 만들어 Button이 눌리면, 알람이 울리게 만들었다. 

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

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

class Button{
	private Alarm theAlarm;
	
	public Button(Alarm theAlarm) {
		this.theAlarm = theAlarm;
	}
	
	public void pressed() {
		theAlarm.start();
	}
}

public class Main {
	public static void main(String[] args) {
		Alarm alarm = new Alarm();
		Button alarmButton = new Button(alarm);
		alarmButton.pressed();
	}
}

이렇게 다른 기능을 만들고자 Button클래스의 변경을 수행하였다. 

하지만, Lamp를 turnOn() 하는 대신, Alarm을 start()하고자 Button 클래스의 변경은 OCP를 위배하여 구현한 것이기 때문에, 적절하게 설계가 되지 않았다고 볼 수 있다. 

 

(2) 를 참고하여 버튼을 누르는 동작에 따라 다른 기능을 실행하게 하려면 기능이 실행되는 시점에 필요한 프로그램을 선택할 수 있어야 한다. 예를 들어 버튼을 처음 눌럿을 때 램프를 켜고 두 번 눌렀을 때는 알람을 동작하게 할 경우 Button 클래스는 2가지 기능(램프 켜기와 알람 동작)을 모두 구현할 수 있어야 한다.  
 

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

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

enum Mode {
	LAMP, ALARM
}

class Button {
	private Alarm theAlarm;
	private Lamp theLamp;
	private Mode theMode;

	public Button(Lamp theLamp, Alarm theAlarm) {
		this.theLamp = theLamp;
		this.theAlarm = theAlarm;
	}

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

	public void pressed() {
		switch (theMode) {
		case LAMP: // 램프 모드면 램프를 켬
			theLamp.turnOn();
			break;
		case ALARM:
			theAlarm.start();
			break;
		}
	}
}

public class Main {

	public static void main(String[] args) {
		Lamp lamp = new Lamp();
		Alarm alarm = new Alarm();
		Button button = new Button(lamp, alarm);

		button.setMode(Mode.LAMP);
		button.pressed();

		button.setMode(Mode.ALARM);
		button.pressed();
	}

}


 이 경우도 마찬가지로 버튼을 눌렀을 때의 기능을 변경하기 위해 다시 Button 클래스의 코드를 수정했기 때문에 OCP를 위반한 구현방식이다. 이러한 수정은 버튼을 눌렀을 때 필요한 기능을 새로 추가할 때마다 반복적으로 발생할 것이다. 

 

=> 즉 (1), (2) 구현한 방식 모두 Button 클래스의 코드변경이 이루어져야만 기능을 만들 수 있으므로, Button 클래스의 재사용성은 떨어지게 된다. 

 

이렇게 새로운 기능을 추가하거나 변경해도, Button 클래스를 재사용할 수 있도록 할 수 있는것이 Command Pattern인 것이다. Button 클래스의 변경없이 구현하고자 어떻게 해야되는지 알아보자. 기존의 알람기능, 램프기능에 램프는 키고 끄는 기능을 추가하여 만들었다. 먼저 UML에 대해서 보자.

 

UML

 

 구현 방식은 새로운 기능을 추가하거나 변경하더라도 Button클래스는 그대로 사용하려면  Button 클래스의 pressed 메서드에서 구체적인 기능을 직접 구현하는 대신 버튼을 눌렀을 때 실행될 기능을 Button 클래스 외부에서 제공받아 캡슐화해 pressed 메서드에서 호출하는 방법을 사용한다. 

 

예를 들어 램프를 켜는 경우는 theLamp.turnOn 메서드를 호출하고 알람이 동작하는 경우, theAlarm.start 메서드를 호출하도록 pressed메서드를 수정해야 한다. 

 

Button 클래스는 램프 켜기 또는 알람 동작 등의 기능을 실행할 때 Lamp 클래스의 turnOn 메서드나 Alarm 클래스의 start 메서드를 직접 호출하지 않는다.  대신 미리 약속된 Command 인터페이스의 execute 메서드를 호출한다. 그리고 LampOnCommand 클래스에서는 execute 메서드를 구현해 램프 켜는 기능을 구현한다. 

 

-> 즉 LampOnCommand 클래스는 execute 메서드에서 Lamp 클래스의 turnOn메서드를 호출해 램프 켜는 기능을 구현한다. AlarmStartCommand 클래스도 마찬가지이다.

 

코드에 대해서 살펴보겠다. 

 

* Command.java

public interface Command {
	public abstract void execute();
}

 

* AlarmOnCommand.java

public class AlarmOnCommand implements Command {
	private Alarm theAlarm;
	
	public AlarmOnCommand(Alarm theAlarm) {
		this.theAlarm = theAlarm;
	}
	@Override
	public void execute() {
		theAlarm.start();
	}
}

 

* LampOnCommand.java

public class LampOnCommand implements Command {
	private Lamp theLamp;
	
	public LampOnCommand(Lamp theLamp) {
		this.theLamp = theLamp;
	}
	
	@Override
	public void execute() {
		theLamp.turnOn();
	}

}

 

* LampOffCommand.java

public class LampOffCommand implements Command {
	private Lamp theLamp;
	
	public LampOffCommand(Lamp theLamp) {
		this.theLamp = theLamp;
	}
	
	@Override
	public void execute() {
		theLamp.turnOff();
	}
}

 

* Alarm.java

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

 

* Lamp.java

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

 

* Button.java

public class Button {
	private Command theCommand;
	
	public Button(Command theCommand) {
		setCommand(theCommand);
	}
	
	public void setCommand(Command newCommand) {
		this.theCommand = newCommand;
	}
	
	public void pressed() {
		theCommand.execute();
	}
}

 

* 실행할 Main클래스

public class Main {

	public static void main(String[] args) {
		Lamp lamp = new Lamp();
		Command lampOnCommand = new LampOnCommand(lamp);
		Button button1 = new Button(lampOnCommand);
		button1.pressed();
		
		Alarm alarm = new Alarm();
		Command alarmOnCommand = new AlarmOnCommand(alarm);
		
		Button button2 = new Button(alarmOnCommand);
		button2.pressed();
		
		button2.setCommand(lampOnCommand);
		button2.pressed();
	}
}

 

결과창

 이렇게 Command 인터페이스를 실제로 구현해주는 LampOnCommand, LampOffCommand, AlarmOnCommand, 객체가 Button객체에 설정되어, Button클래스에서 pressed() 메서드를 통해 Command 인터페이스의 excute() 메서드를 호출 받을 수 있게 하여, 각각 구현된 concreted command 인 LampOnCommand, LampOffCommand, AlarmOnCommand에서 excute() 메서드를 실행할 수 있게 된 것이다. 

 

 결과적으로 여기에 "알람끄기" 라는 기능을 추가하려고 할때, AlarmOffCommand를 추가하고 , Alarm객체에 끄는 기능을 가진 메서드만 추가해준다면 Button의 변경없이 수행 할 수 있을 것이다. 

 

 

참고자료 : Java 객체지향 디자인 패턴, 한빛미디어, 정인상 채흥석 저, 2014

 

728x90