체스 게임 - 커맨드 구현 일지

@hongo · March 26, 2023 · 4 min read

체스 게임

이번주는 체스 게임 미션을 하는데 보냈다. 체스 게임에는 세 가지의 명령어가 존재한다.

> 게임 시작 : start
> 게임 종료 : end
> 게임 이동 : move source위치 target위치 - 예. move b2 b3

명령어는 콘솔에서 List<String>으로 입력받는다.

start의 경우 : List.of("string")
move의 경우 : List.of("move", "b2", "b3")

입력받은 명령어에 대응하는 동작을 실행시키기 위해선 어떻게 해야할까?

📌 if문을 사용해 커맨드에 따른 로직 실행

가장 먼저 떠오르는 방법은 if문을 통해 입력받은 명령어가 어떤 명령어인지를 판단하고, 그에 맞는 로직을 실행시키는 방법이었다.

// ChessGameController
private ChessGame executeCommand(ChessGame chessGame, List<String> inputCommand) {
        Command command = inputCommand.get(COMMAND_HEAD_INDEX);
    
        if (command == Command.START) {
            // start 로직
        }
    
        if (command == Command.MOVE) {
            // move 로직
        }
    
        if (command == Command.END) {
            // end 로직
        }
    
        return chessGame;
}

그러나 if문을 사용하면, Command의 종류가 늘어나는 만큼, if문의 개수도 증가하게 된다. Command를 일일이 조건문으로 확인하지 않고 로직을 실행할 수 있는 방법은 없을까?

📌 Enum객체에 함수형 인터페이스 사용

CommandEnum타입이다. Enum의 각 상수는 자신만의 변수를 가질 수 있다. 아래 String타입의 command가 그 예시이다.

public enum Command {
    START("start"),
    MOVE("move"),
    END("end");

    private final String command;

    Command(String command) {
        this.command = command;
    }
}

그렇다면 Command의 각 로직도 함수형 인터페이스를 사용해 변수로 가지게 하면 어떨까?


// 커맨드의 로직을 실행시키는 CommandAction 인터페이스를 생성했다!
interface CommandAction {
        ChessGame execute(final ChessGame chessGame, List<String> commands);
}

// 커맨드가 Start일 때 실행되는 로직 StartAction을 생성했다!
public class StartAction implements CommandAction {
    @Override
    ChessGame execute(final ChessGame chessGame, List<String> commands){
        // validateCommands(commands);
        return chessGame.start();
    }
}

// 커맨드가 Move일 때 실행되는 로직 MoveAction을 생성했다!
public class MoveAction implements CommandAction {
    @Override
    ChessGame execute(final ChessGame chessGame, List<String> commands){
        // validateCommands(commands);
        String currentPosition = commands.get(1);
        String nextPosition = commands.get(2);
        return chessGame.move(currentPosition, nextPosition);
    }
}

// 커맨드가 End일 때 실행되는 로직 EndAction을 생성했다!
public class EndAction implements CommandAction {
    @Override
    ChessGame execute(final ChessGame chessGame, List<String> commands){
        // validateCommands(commands);
        return chessGame.end();
    }
}

CommandAction 인터페이스를 사용해 Enum객체에서 execute() 메소드를 호출하면, 바로 대응하는 로직을 실행시킬 수 있게 변경했다.

public enum Command {
    START("start", new StartAction()),
    MOVE("move", new MoveAction()),
    END("end", new EndAction());

    private final String command;
    private final CommandAction commandAction;
    
    public ChessGame execute(ChessGame chessGame, List<String> commands){
        return this.commandAction.execute(chessGame, commands);
    }
}

  • 기존 Command실행 로직
// ChessGameController
private ChessGame executeCommand(ChessGame chessGame, List<String> inputCommand) {
        Command command = inputCommand.get(COMMAND_HEAD_INDEX);
    
        if (command == Command.START) {
            // start 로직
        }
    
        if (command == Command.MOVE) {
            // move 로직
        }
    
        if (command == Command.END) {
            // end 로직
        }
    
        return chessGame;
}

  • 바뀐 Command 실행 로직
// ChessGameController
private ChessGame executeCommand(ChessGame chessGame, List<String> inputCommand) {
    Command command = inputCommand.get(COMMAND_HEAD_INDEX);
    command.execute(chessGame, inputCommand);
}

Command를 저장해야 하는 경우는?

수행한 Command를 전부 저장해야한다면 어떻게 될까?

  • 실행한 명령어는 취소가 가능하다.
  • 명령어 실행 히스토리를 저장한다.

위와 같은 요구사항이 추가된다면, Command 내용을 전부 저장해야한다. Action에서 커맨드 내용을 저장하면 어떨까?

public class MoveAction implements Action{
    private List<String> commands; // 커맨드 내용 저장
    
    @Override
    public void execute(ChessGame chessGame, List<String> commands) {
        // validateCommandsLength(commands);
        this.commands = commands;
        String currentPosition = commands.get(1);
        String nextPosition = commands.get(2);
        return chessGame.move(currentPosition, nextPosition);
    }
}

커맨드 내용이 잘 저장되었는지 출력해보자.

public class Application {
    public static void main(String[] args) {
        Command command1 = Command.MOVE;
        command1.execute(chessGame, Arrays.asList("move", "a2", "a3"));

        Command command2 = Command.MOVE;
        command2.execute(chessGame, Arrays.asList("move", "b7", "b5"));

        command1.printCommands();
        command2.printCommands();
    }
}
// 실행 결과
move b7 b5
move b7 b5

move a2 a3을 수행한 command1Commandsmove b7 b5로 저장되는 것을 볼 수 있다.

Enum은 상수의 집합으로, JVM이 자동으로 public static final 필드로 만든다. 각각의 상수들은 클래스 변수처럼 사용되며 같은 주소값을 참조한다.

때문에 각각의 MOVE커맨드를 저장할 수 없다.

📌 커맨드 패턴

각각의 커맨드를 저장하려면 어떻게 해야할까? Command객체를 Enum이 아닌 Class객체로 만들면 될 것 같다!

커맨드 패턴을 사용해서 구현해보았다.

Command 인터페이스 & ConcreteCommand 클래스 생성

// Command 인터페이스 생성
public interface Command {
    ChessGame execute(ChessGame chessGame, List<String> commands);
}

// Command를 구현한 명령어 로직 클래스 생성
public class MoveCommand implements Command {
    private final List<String> commands;

    public MoveCommand(List<String> commands) {
        //validateCommandsLength(commands);
        this.commands = commands;
    }
    
    @Override
    public ChessGame execute(ChessGame chessGame, List<String> commands) {
        // move 로직
    }
}

명령어를 입력받아 적절한 Command객체를 돌려줄 로직이 필요

이제 List<String> commands를 입력받아 적절한 Command객체로 돌려줄 로직이 필요하다.

이걸 어떻게 예쁘게 구현할지 고민을 많이 했는데... CommandType이라는 Enum객체를 한 번 거쳐 Command를 반환해주게 했다. 더 좋은 방법이 있을 것 같은데, 나중에 찾으면 바꿀 예정이다.

// ChessGameController
private void executeCommand(ChessGame chessGame, List<String> commands) {
        CommandType commandType = CommandType.getCommandType(commands);
        Command command = commandType.getCommand(commands);
        command.execute(chessGame, command);
}

public enum CommandType {
    START("start", StartCommand::new),
    MOVE("move", MoveCommand::new),
    END("end", EndCommand::new);
    
    private final String command;
    private final Function<List<String>, Command> commandGenerator;
    
    public static CommandType getCommandType(List<String> commands) {
    	// commands를 받아 적절한 CommandType을 반환
    }
    
    public Command getCommand(List<String> commands) {
        return commandGenerator.apply(commands);
    }
}
@hongo
홍고 블로그