커맨드와 액티브 오브젝트 패턴

커맨드 패턴

1
2
3
public interface Command {
void do();
}


여러 행동을 호출해야 하는 경우 여러 행동을 커맨드로 추상화하여 사용하는 쪽에서는 커맨드가 어떻게 행동하는 지 관심없이 사용하기만 하도록 구현하는 패턴이다.
이 패턴은 커맨드를 사용하는 쪽에서 커맨드를 실행하기 위해서 어떤 객체가 사용되는지 알아지 않아도 되는 장점이 있다. 커맨드 패턴은 명령의 개념을 캡슐화해서 시스템과 연결된 장치의 논리적인 상호 연결을 분리해낼 수 있다.

물리적 분리


커맨드 패턴을 사용하면 사용자에게서 데이터를 받는 코드와 그 데이터를 검증하고 작업을 하는 코드, 도메인 객체를 분리할 수 있다. 데이터를 받는 코드에서 커맨드의 실행 코드와 검증 코드와 물리적으로 분리 된다.

시간적 분리

사용자에게서 데이터를 입력받는 역할과 그 데이터를 검증하고 작업을 하는 코드를 물리적으로 분리할 수 있다고 했다. 이를 다르게 생각해보면 데이터를 입력받았다고 바로 검증과 실행을 하지 않아도 된다는 의미다. 즉 명령을 일단 다 생성해서 모아놓고 특정 시점에 몰아서 명령을 검증하고 실행할 수 있다는 의미이다.

되돌리기

1
2
3
4
public class Command {
void do();
void undo();
}

커맨드에 실행하는 메서드말고 되돌리기 메서드를 선언해놓으면 롤백 기능도 구현할 수 있다. 위처럼 인터페이스로 실행과 실행 취소 메서드를 선언해놓고 각 명령마다 실행과 실행 취소 로직을 구현해놓으면 된다.
직원 임금 관리 서비스에서 직원을 추가하는 명령이 있다고 할 때 실행시키는 메서드는 직원 객체를 생성하고 저장소에 저장하는 방식으로 구현된다. 실행 취소 메서드는 저장소에 해당 직원의 데이터를 제거하는 방식으로 구현할 수 있을 것이다.

액티브 오브젝트 패턴

액티브 오브젝트 패턴은 커맨드 패턴의 응용된 버전으로 다중 제어 스레드 구현을 위해 등장했다.
액티브 오브젝트 패턴은 하나의 스레드로 멀티 스레드가 이벤트 방식으로 작동하는 것처럼 구현할 수 있어서 런타임 스택을 여러개 만들지 않아도 된다. 즉 멀티 스레드를 활용하고 메모리가 부족한 상황에서 사용할 만한 대안이다.

액티브 오브젝트 패턴은 커맨드를 모아두고 꺼내서 실행하는 엔진과 실행 시킬 커맨드, 마지막으로 실행 시킬 조건을 다루는 커맨드 이렇게 세가지로 구성된다.

엔진

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ActiveObjectEngine {

private final LinkedList<Command> commands = new LinkedList<>();

public void add(Command c) {
commands.add(c);
}

public void run() throws Exception {
while(!commands.isEmpty()) {
commands.pop()
.execute();
}
}
}

엔진을 실행시키면 자신이 가진 모든 커맨드를 순차적으로 실행한다.

실행 조건을 다루는 커맨드

특정 시간만큼 sleep하고 커맨드를 실행 이라는 조건을 달성하기 위해 커맨드를 만들어보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class SleepCommand implements Command {

private final long sleepMilliSeconds;
private final ActiveObjectEngine engine;
private final Command wakeUpCommand;
private long startedTime = 0;
private boolean started = false;

public SleepCommand(final long sleepMilliSeconds, final ActiveObjectEngine engine, final Command wakeUpCommand) {
this.sleepMilliSeconds = sleepMilliSeconds;
this.engine = engine;
this.wakeUpCommand = wakeUpCommand;
}

@Override
public void execute() throws Exception {
final long currentTime = System.currentTimeMillis();
if (!started) {
started = true;
startedTime = currentTime;
engine.add(this);
return;
}
if (currentTime - startedTime < sleepMilliSeconds) {
engine.add(this);
return;
}
engine.add(wakeUpCommand);
}
}

자신이 엔진에 의해 실행된 경우가 없다면 현재 시간을 시작 시각으로 기록하고 다시 엔진에 자신을 집어넣는다.
자신이 엔진에 의해 실행된 경우가 있으면 현재 시각과 시작 시각을 비교해서 커맨드 실행 조건에 달성했는 지 체크한다.
조건에 달성했다면 wakeUpCommand 즉 우리가 원래 실행하려고 했던 커맨드를 실핸한다.

실행하려는 커맨드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class DelayedTyper implements Command {
private final ActiveObjectEngine engine;
private final long itsDelay;
private final char itsChar;

public DelayedTyper(final ActiveObjectEngine engine, final long itsDelay, final char itsChar) {
this.engine = engine;
this.itsDelay = itsDelay;
this.itsChar = itsChar;
}

@Override
public void execute() throws Exception {
System.out.print(itsChar);
delayAndRepeat();
}

private void delayAndRepeat() throws Exception {
engine.add(new SleepCommand(itsDelay, engine, this));
}
}

주어진 글자를 출력하고 특정 시간만큼 sleep 이후 다시 출력하는 커맨드이다.

마지막으로 이를 실행하는 코드를 통해 실제 결과물을 확인할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
public class Application {
private static final ActiveObjectEngine ENGINE = new ActiveObjectEngine();

public static void main(String[] args) throws Exception {
ENGINE.add(new DelayedTyper(ENGINE, 100, '1'));
ENGINE.add(new DelayedTyper(ENGINE, 300, '3'));
ENGINE.add(new DelayedTyper(ENGINE, 500, '5'));
ENGINE.add(new DelayedTyper(ENGINE, 700, '7'));
ENGINE.add(new SleepCommand(20000, ENGINE, () -> System.exit(0)));
ENGINE.run();
}
}
Share