배경
소프트웨어 개발에서 자주 마주치는 문제는 새로운 기능을 추가할 때 기존 코드를 수정해야 하는 상황입니다. 이는 개방-폐쇄 원칙(Open-Closed Principle, OCP)을 위반하며, 코드의 유지보수성을 크게 떨어뜨립니다.
문제 상황
간단한 예로, 버튼 클래스를 생각해보겠습니다. 초기에 버튼은 불을 켜는 기능만 제공했습니다. 그런데 이제 알람 기능도 추가하고 싶다면 어떻게 해야 할까요?
나쁜 접근 방식
public class Button {
private Light light;
private Alarm alarm;
public void press() {
// 불 켜기 또는 알람 울리기
if (현재모드 == 불모드) {
light.turnOn();
} else if (현재모드 == 알람모드) {
alarm.ringAlarm();
}
}
}
이 접근 방식의 문제점:
- 버튼 클래스를 직접 수정해야 합니다.
- 새로운 기능을 추가할 때마다 클래스 내부를 변경해야 합니다.
- 단일 책임 원칙(SRP)을 위반합니다.
- 코드의 복잡성이 증가합니다.
커맨드 패턴으로의 해결
핵심 아이디어
"행동(커맨드)을 객체로 캡슐화하여 요청을 매개변수화한다."
구현 방법
- 공통 Command 인터페이스 정의
- 각 기능별 구체적인 Command 클래스 구현
- 버튼은 현재 커맨드만 실행
// Command 인터페이스
interface Command {
void execute();
}
// 구체적인 커맨드 클래스들
class LightOnCommand implements Command {
private Light light;
public void execute() {
light.turnOn();
}
}
class AlarmCommand implements Command {
private Alarm alarm;
public void execute() {
alarm.ringAlarm();
}
}
// 버튼 클래스
class Button {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void press() {
command.execute();
}
}
장점
- 새로운 기능 추가 시 기존 코드 수정 불필요
- 각 기능이 독립적으로 분리됨
- 런타임에 동적으로 기능 변경 가능
- 확장에는 열려있고, 수정에는 닫혀있는 OCP 원칙 준수
활용 예시
public class Main {
public static void main(String[] args) {
Button button = new Button();
// 불 켜기 기능 설정
button.setCommand(new LightOnCommand());
button.press(); // 불 켜짐
// 알람 울리기 기능으로 변경
button.setCommand(new AlarmCommand());
button.press(); // 알람 울림
}
}
커맨드 패턴의 고급 활용: 유연성과 확장성 극대화
커맨드 패턴의 심화 기능
1. 매크로 커맨드 (복합 커맨드)
매크로 커맨드는 여러 개의 개별 커맨드를 하나의 큰 커맨드로 묶어 실행할 수 있게 해줍니다.
// 매크로 커맨드 구현 예시
class MacroCommand implements Command {
private List<Command> commands;
public MacroCommand(List<Command> commands) {
this.commands = commands;
}
@Override
public void execute() {
commands.forEach(Command::execute);
}
@Override
public void undo() {
// 역순으로 undo 수행
Collections.reverse(commands);
commands.forEach(Command::undo);
Collections.reverse(commands);
}
}
2. 실행 취소(Undo) 기능
모든 커맨드에 undo()
메서드를 추가하여 작업 취소 기능을 구현할 수 있습니다.
interface Command {
void execute();
void undo(); // 실행 취소 메서드 추가
}
3. 커맨드 히스토리 및 로깅
실행된 커맨드들을 추적하고 저장할 수 있습니다.
class CommandInvoker {
private List<Command> executedCommands = new ArrayList<>();
public void executeCommand(Command command) {
command.execute();
executedCommands.add(command);
}
public void undoLastCommand() {
if (!executedCommands.isEmpty()) {
Command lastCommand = executedCommands.remove(executedCommands.size() - 1);
lastCommand.undo();
}
}
public void printCommandHistory() {
executedCommands.forEach(cmd ->
System.out.println(cmd.getClass().getSimpleName())
);
}
}
4. 커맨드 큐잉
커맨드를 큐에 저장하여 순차적으로 실행하거나 지연 실행할 수 있습니다.
class CommandQueue {
private Queue<Command> commandQueue = new LinkedList<>();
public void addCommand(Command command) {
commandQueue.offer(command);
}
public void executeAll() {
while (!commandQueue.isEmpty()) {
Command command = commandQueue.poll();
command.execute();
}
}
public void scheduleExecution(Command command, long delay) {
new Timer().schedule(new TimerTask() {
@Override
public void run() {
command.execute();
}
}, delay);
}
}
5. 매개변수화된 커맨드
커맨드에 추가 매개변수를 전달하여 더욱 유연한 동작 구현.
class ParameterizedCommand implements Command {
private Receiver receiver;
private String parameter;
public ParameterizedCommand(Receiver receiver, String parameter) {
this.receiver = receiver;
this.parameter = parameter;
}
@Override
public void execute() {
receiver.action(parameter);
}
}
6. 트랜잭션 및 롤백 구현
여러 커맨드를 하나의 트랜잭션으로 묶어 전체 성공 또는 전체 롤백 처리.
class TransactionalCommand implements Command {
private List<Command> commands;
public TransactionalCommand(List<Command> commands) {
this.commands = commands;
}
@Override
public void execute() {
List<Command> executedCommands = new ArrayList<>();
try {
for (Command cmd : commands) {
cmd.execute();
executedCommands.add(cmd);
}
} catch (Exception e) {
// 실패 시 이미 실행된 커맨드 롤백
for (Command cmd : Lists.reverse(executedCommands)) {
cmd.undo();
}
throw e;
}
}
@Override
public void undo() {
for (Command cmd : Lists.reverse(commands)) {
cmd.undo();
}
}
}
커맨드 패턴의 핵심 이점
- 유연성: 새로운 기능 추가가 쉬움
- 확장성: 기존 코드 수정 없이 새 기능 통합 가능
- 디커플링: 요청자와 수행자 사이의 종속성 제거
- 재사용성: 커맨드 객체의 조합과 재활용 용이
활용 팁
- 간단한 커맨드부터 시작
- 점진적으로 기능 확장
- 단일 책임 원칙 준수
- 과도한 추상화 주의
커맨드 패턴은 복잡한 시스템에서 행동을 객체로 캡슐화하고 관리하는 강력한 디자인 패턴입니다. 적절히 활용하면 유연하고 확장 가능한 소프트웨어 아키텍처를 만들 수 있습니다.
결론
커맨드 패턴은 요청을 객체로 캡슐화하여 시스템의 유연성과 확장성을 높입니다. 새로운 기능을 추가할 때 기존 코드를 수정하지 않고도 쉽게 통합할 수 있어, 소프트웨어 설계의 중요한 패턴 중 하나입니다.
'디자인패턴' 카테고리의 다른 글
디자인 패턴 - 옵저버 패턴 (1) | 2024.12.02 |
---|---|
디자인 패턴 - 상태 패턴 (0) | 2024.11.28 |
디자인 패턴 - 전략 패턴 (0) | 2024.11.27 |
디자인 패턴 - SOLID 패턴 SRP (0) | 2024.11.26 |
댓글