커맨드 패턴 (Command Pattern)
- 요청을 호출하는 쪽과 요청을 수행하는 쪽을 분리하는 (Decoupling) 패턴이다.
- 요청을 요청에 대한 모든 정보를 포함하는 독립형 객체 (stand-alone object) 로 변환하는 동작 디자인 패턴이다.
- 요청을 메서드 인수로 제공하거나 요청 실행을 지연하거나 대기열에 추가하거나 실행 취소 가능한 작업을 지원할 수 있다.
다이어그램으로 살펴보기
- 호출자, 명령, 수신자가 각각 나누어져 있다.
- 이 때문에 요청을 처리하는 방법이 바뀌더라도 호출자의 코드는 바뀌지 않는다.
- 커맨드 자체는 재사용도 가능하다.
- 로깅 혹은 실행취소 등
문제: 텍스트 에디터 앱의 예시
- 텍스트 에디터 상단 툴바에는 다양한 버튼들이 존재한다.
- 버튼들은 모두 다른 기능을 하지만 비슷하게 생겼다.
- 처음에는 모두 서브클래스를 이용해 구현을 했다
- '복사', '붙여넣기', '잘라내기', '저장' 등의 버튼을 만들었다.
- 막상 버튼의 서브클래스로 만들어놓고 보니, 똑같은 기능을 여러군데에서 제공할 필요 있다는 것을 깨닫게 됐다.
- 이를테면
Ctrl + C
,Ctrl + V
단축키를 눌렀을 때도 동일하게 복사 붙여넣기를 제공해야 한다. - 마우스 우클릭을 해서 나오는 툴팁에서 복사 붙여넣기를 해도 동일한 기능을 제공해야 한다.
결국 문제는 동일한 기능을 다양한 곳에서 호출할 니즈가 있어서 하나의 UI 에 종속되면 안된다는 것이다.
문제 해결
- 좋은 소프트웨어는 보통 관심사 분리 원칙 (principle of separation of concerns) 에 기반해 만들어진다.
- 이 원칙을 따르면, 앱을 여러가지 레이어로 쪼개게 된다.
- 다음과 같이 레이어를 나눠볼 수 있다.
- 그래픽 UI 레이어
- 비즈니스 로직 레이어
- 그래픽 UI 레이어는
- 화면에 UI 를 그린다.
- 사용자가 화면을 이용해 상호작용 했을 때 행위를 캡쳐한다.
- 행위에 따른 결과 화면을 다시 그려준다.
- 비즈니스 로직 레이어는
- 화면에 보이지 않는 복잡한 논리를 처리한다.
- 복사할 때 OS 에 따른 클립보드를 불러와 텍스트를 넣는다거나 클립보드에서 텍스트를 꺼내온다.
- 꺼내온 텍스트를 그래픽 UI 레이어로 넘기고 화면에 대한 책임을 위임한다.
그래픽 UI 와 비즈니스 로직을 분리하기
- 비즈니스 로직을 별도의 레이어에 구성한다.
- 그래픽 UI 레이어인 각 버튼들에서는 사용자와의 상호작용에서 발생한 이벤트를 캡처하여 이를 인자로 비즈니스 레이어의 함수를 호출한다.
- 비즈니스 로직 레이어는 내부적으로 복잡한 비즈니스 레이어를 처리하고 이 결과를 다시 그래픽 UI 레이어로 넘긴다.
- 그래픽 레이어 UI 는 넘겨받은 결과를 토대로 결과 UI 를 그려준다.
계산기 코드로 살펴보기
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Command Pattern Calculator</title>
</head>
<body>
<input type="number" id="number1" placeholder="Enter first number" />
<input type="number" id="number2" placeholder="Enter second number" />
<button id="addButton">Add</button>
<button id="subtractButton">Subtract</button>
<div id="result"></div>
<script src="app.js"></script>
</body>
</html>
// Command interface
class Command {
execute() {}
}
// Concrete command classes
class AddCommand extends Command {
constructor(receiver) {
super();
this.receiver = receiver;
}
execute(a, b) {
return this.receiver.add(a, b);
}
}
class SubtractCommand extends Command {
constructor(receiver) {
super();
this.receiver = receiver;
}
execute(a, b) {
return this.receiver.subtract(a, b);
}
}
// Receiver class
class Calculator {
add(a, b) {
return a + b;
}
subtract(a, b) {
return a - b;
}
}
// Invoker
class ButtonHandler {
setCommand(command) {
this.command = command;
}
executeCommand(a, b) {
return this.command.execute(a, b);
}
}
// UI event bindings
const calculator = new Calculator();
const addButtonHandler = new ButtonHandler();
const subtractButtonHandler = new ButtonHandler();
addButtonHandler.setCommand(new AddCommand(calculator));
subtractButtonHandler.setCommand(new SubtractCommand(calculator));
document.getElementById("addButton").addEventListener("click", () => {
const a = parseInt(document.getElementById("number1").value, 10);
const b = parseInt(document.getElementById("number2").value, 10);
const result = addButtonHandler.executeCommand(a, b);
displayResult(result);
});
document.getElementById("subtractButton").addEventListener("click", () => {
const a = parseInt(document.getElementById("number1").value, 10);
const b = parseInt(document.getElementById("number2").value, 10);
const result = subtractButtonHandler.executeCommand(a, b);
displayResult(result);
});
// Display result in the UI
function displayResult(value) {
document.getElementById("result").innerHTML = `Result: ${value}`;
}
Command
: 인터페이스로서 정의되었다.AddCommand
,SubstractCommand
:Command
클래스를 구현한 구현체다. 구체적인 수행 내용이 있다기보다는receiver
에 들어갈 수신자를 받아receiver
의add()
메서드substract()
메서드를 실행해주는 리모콘 같은 존재이다.Calculator
: 실제로add()
메서드와substract()
메서드에 해당하는 비즈니스 로직을 구현한다."click" 이벤트 리스너
: 그래픽 UI 를 통해 사용자와 상호작용한 내용을 비즈니스 로직 커멘드로 보낸다. 비즈니스 로직에서 얻은 결과를 다시 그래픽 UI 에 표현하게 된다.
장점과 단점
장점
- 클라이언트 코드를 변경하지 않고도 새로운 비즈니스 로직 코드를 만들 수 있다.
- 다양한 수신자 (
receiver
) 를 만들어 클라이언트 코드의 변경 없이도 여러가지 구현체를 바꿔끼워볼 수 있다. undo()
와 같은 기능을 쉽게 구현할 수 있다.
class Multiply3 {
calc(a) {
return a * 3;
}
undo(a) {
return a / 3;
}
}
class divide2 {
calc(a, b) {
return a / 2;
}
undo(a, b) {
return a * 2;
}
}
단점
- 커멘드 객체가 많이 늘어난다.
- 코드가 조금 더 복잡해보인다.
그러나 이 단점을 상쇄할만큼의 큰 장점을 가져다 줘서 사실 단점이 의미가 있나 싶다.
실생활 예에서 생각해보기
- 실생활 예에서는 리모콘이 좋은 예시가 될 수 있다.
- 많은 TV 에 통합으로 적용되는 리모콘이 있는데 이 리모콘은 TV 가 무엇을 할지에 대한 내용을 전부 가지고 있는 것이 아니다.
- 다만, 보통의 TV 가 어떤 신호를 어떤 주파수에 넣어놓는지만 알고 있을 뿐일 것이다.
- 실제
receiver
는 TV 가 되는 것이고, TV 내부에 있는 구현 로직이 TV 에 대한 명령을 수행할 것이다.
자바와 스프링에서 찾아볼 수 있는 패턴
ExecutorService
in 자바
public class CommandInJava {
public static void main(String[] args) {
Light light = new Light();
Game game = new Game();
ExecutorService executorService = Executors.newFixedThreadPool(4);
executorService.submit(light::on);
executorService.submit(game::start);
executorService.submit(game::end);
executorService.submit(light::off);
executorService.shutdown();
}
}
ExcutorService
는 자바 동시성 라이브러리에 있는 서비스이다.- 전달받은
Runnable
객체를 스레드에 올려준다. ExecutorService
는 세부사항을 모르고, 그냥receiver
인thread
에게Runnable
혹은Callable
타입의 객체를 주고,thread
는 이를 실제로 호출하여 명령을 수행한다.
레퍼런스
반응형
'Java > 자바 디자인 패턴' 카테고리의 다른 글
이터레이터 패턴 (Iterator Pattern) 이란? (0) | 2023.04.30 |
---|---|
인터프리터 패턴 (Interpreter Pattern) 이란? (0) | 2023.04.30 |
책임 연쇄 패턴 (Chain Of Responsibility) 이란? (2) | 2023.04.26 |
프록시 패턴 (Proxy Pattern) 이란? (0) | 2023.04.25 |
플라이웨이트 패턴 (Flyweight Pattern) 이란? (0) | 2023.04.22 |