Design Patter4: Command Pattern

업데이트:
3 분 소요

비트캠프 서초본원 엄진영 강사님의 수업을 듣고 정리했습니다.


Command Patter

  • 커맨드 디자인 패턴은 기존 소스에 영향을 끼치지 않고 새 기능을 추가하는 방식이다.

1. Command 패턴이란?

  • 메서드의 객체화 설계 기법이다.
  • 한 개의 명령어를 처리하는 메서드를 별개의 클래스로 분리하는 기법이다.
  • 이렇게 하면 명령어가 추가될 때마다 새 클래스를 만들면 되기 때문에 기존 코드를 손대지 않아서 유지보수에 좋다.
  • 즉 기존 소스에 영향을 끼치지 않고 새 기능을 추가하는 방식이다.
  • 명령처리를 별도의 객체로 분리하기 때문에 실행 내역을 관리하기 좋고, 각 명령이 수행했던 작업을 다루기가 편하다.
  • 인터페이스를 이용하면 메서드 호출 규칙을 단일화 할 수 있기 때문에 코딩의 일관성을 높여줄 수 있다.
  • 단 기능 추가할 때마다 해당 기능을 처리하는 새 클래스가 추가되기 때문에 클래스 개수는 늘어난다.
  • 그러나 유지보수 측면에서는 기존 코드를 변경하는 것 보다는 클래스 개수가 늘어나는 것이 좋다.
  • 유지보수 관점에서는 소스 코드를 일관성 있게 유지보수 할 수 있는게 더 중요하다.

2. Command 패턴 적용 전

  • 커맨드 패턴을 적용하기 전에는 새 기능을 추가할 때 마다 기존 코드를 변경해야 한다.
  • 기존 코드에 새 코드를 추가하는 것은 기존 코드를 손댈 수 있는 위험성이 있다.
  • 즉 잘되던 기능을 망치는 경우가 발생할 수 있다.

3. Command 패턴 적용

  • 명령어를 처리하는 각 메서드를 클래스로 정의한 후 사용한다.
  • 일관된 사용을 위해 인터페이스로 호출 규칙을 정의한다.
  • 나중에 명령어가 추가되면 그 명령어를 처리할 클래스를 추가하면 된다.

0단계 - 기존 코드를 확인한다.

  • 기존에는 XXXHandler에 여러 메서드를 두고 각 명령어에 따라 다른 메서드를 호출하는 방식으로 명령어를 처리한다.

BoardHandler.java

public class BoardHandler {
  List<Board> boardList;

  public BoardHandler(List<Board> list) {
    this.boardList = list;
  }

  public void add() {
    System.out.println("[게시물 등록]");
  }

  public void list() {
    System.out.println("[게시물 목록]");
  }

  public void detail() {
    System.out.println("[게시물 상세보기]");
  }

  public void update() {
    System.out.println("[게시물 변경]");
  }

  public void delete() {
    System.out.println("[게시물 삭제]");
  }
}
  • 새로운 기능을 추가하기 위해서는 기존 코드에 새 코드를 추가해야 한다.
  • 또한 명령어를 추가한다면, 그 명령을 처리할 메서드도 추가해야 한다.

App.java

loop:
  while (true) {
    String command = Prompt.inputString("명령> ");

    switch (command) {
      case "/board/add": boardHandler.add(); break;
      case "/board/list": boardHandler.list(); break;
      case "/board/detail": boardHandler.detail(); break;
      case "/board/update": boardHandler.update(); break;
      case "/board/delete": boardHandler.delete(); break;

      case "quit":
      case "exit":
        System.out.println("안녕!");
        break loop;
      default:
        System.out.println("실행할 수 없는 명령입니다.");
    }
  }

1단계 - Command 인터페이스를 적용한다.

  • 명령을 처리하는 메서드의 호출 규칙인 Command 인터페이스를 정의한다.
public interface Command {
  void execute();
}

2단계 - 명령을 처리하는 Command 구현체를 생성한다.

  • 각 명령어를 처리하는 메서드를 별도의 XXXCommand 클래스를 만들어 분리한다.
public class BoardAddCommand implements Command {
  List<Board> boardList;

  public BoardAddCommand(List<Board> list) {
    this.boardList = list;
  }

  @Override
  public void execute() {
    System.out.println("[게시물 등록]");
  }
}
public class BoardListCommand implements Command {
  List<Board> boardList;

  public BoardListCommand(List<Board> list) {
    this.boardList = list;
  }

  @Override
  public void execute() {
    System.out.println("[게시물 목록]");
  }
}
public class BoardDetailCommand implements Command {
  List<Board> boardList;

  public BoardDetailCommand(List<Board> list) {
    this.boardList = list;
  }

  @Override
  public void execute() {
    System.out.println("[게시물 조회]");
  }
}
public class BoardUpdateCommand implements Command {
  List<Board> boardList;

  public BoardUpdateCommand(List<Board> list) {
    this.boardList = list;
  }

  @Override
  public void execute() {
    System.out.println("[게시물 변경]");
  }
}
public class BoardDeleteCommand implements Command {
  List<Board> boardList;

  public BoardDeleteCommand(List<Board> list) {
    this.boardList = list;
  }

  @Override
  public void execute() {
    System.out.println("[게시물 삭제]");
  }
}

3단계 - 사용자가 명령어를 입력했을 때 Command 구현체를 실행하도록 변경한다.

  • App 클래스가 XXXCommand 객체를 통해 처리하도록 변경한다.

App.java

loop:
  while (true) {
    String command = Prompt.inputString("명령> ");

    switch (command) {
      case "/board/add": boardAddCommand.execute(); break;
      case "/board/list": boardListCommand.execute(); break;
      case "/board/detail": boardDetailCommand.execute(); break;
      case "/board/update": boardUpdateCommand.execute(); break;
      case "/board/delete": boardDeleteCommand.execute(); break;

      case "quit":
      case "exit":
        System.out.println("안녕!");
        break loop;
      default:
        System.out.println("실행할 수 없는 명령입니다.");
    }
  }

4단계 - /hello 명령을 추가한다.

  • 커맨드 패턴을 적용하여 애플리케이션 아키텍처를 변경되었다.
  • 커맨드 패턴을 적용할 경우 새 기능 추가가 쉽다는 것을 확인할 수 있다.
명령> /hello
안녕하세요!

명령>

App.java

loop:
  while (true) {
    String command = Prompt.inputString("명령> ");

    switch (command) {
      	case "/board/add": boardAddCommand.execute(); break;
        case "/board/list": boardListCommand.execute(); break;
        case "/board/detail": boardDetailCommand.execute(); break;
        case "/board/update": boardUpdateCommand.execute(); break;
        case "/board/delete": boardDeleteCommand.execute(); break;
        case "/hello": helloCommand.execute(); break;

        case "quit":
        case "exit":
          System.out.println("안녕!");
          break loop;
        default:
          System.out.println("실행할 수 없는 명령입니다.");
    }
  }

5단계 - HashMap을 이용하여 커맨드 객체를 관리한다.

  • 낱개의 레퍼런스로 커맨드 객체를 관리하던 방식을 Map을 이용하여 관리한다.
  • 명령어를 처리할 때는 Map에서 커맨드 객체를 찾아 실행한다.

App.java

  // 커맨드 객체를 저장할 맵 객체를 준비한다.
  Map<String, Command> commandMap = new HashMap<>();

  // 맵 객체에 커맨드 객체를 보관한다.
  List<Board> boardList = new ArrayList<>();
  commandMap.put("/board/add", new BoardAddCommand(boardList));
  commandMap.put("/board/list", new BoardListCommand(boardList));
  commandMap.put("/board/detail", new BoardDetailCommand(boardList));
  commandMap.put("/board/update", new BoardUpdateCommand(boardList));
  commandMap.put("/board/Delete", new BoardDeleteCommand(boardList));

loop:
  while (true) {
    String inputStr = Prompt.inputString("명령> ");

    switch (inputStr) {
      case "quit":
      case "exit":
        System.out.println("안녕!");
        break loop;
      default:
        Command command = commandMap.get(inputStr);
          if (command != null) {
            command.execute();
          } else {
            System.out.println("실행할 수 없는 명령입니다.");
          }
    }
  }