커맨드 패턴 활용으로 조건문 없애기

Glen·2023년 3월 18일
0

배운것

목록 보기
3/34

개요

우테코 미션을 수행하거나 간단한 프로그램을 구현하여 볼 때 사용자 입력에 따라 어떠한 결과를 기대하는 로직을 작성해 본 적이 있을 것이다.

사용자 입력에 대해 조건문을 사용하여 원하는 결과에 맞게 출력하는 로직을 작성하게 된다면 모든 조건에 사용자가 입력한 커맨드에 대해 검사를 해야한다.

조건이 1~2개 일 경우 읽기에 큰 무리가 없지만, 조건이 점점 늘어날 경우 조건문이 많아지고 코드도 길어져 가독성이 나빠진다.

그렇다면 어떻게 조건문을 사용하지 않고 사용자 입력에 대한 로직을 실행할 수 있을까?

조건문을 사용한 방식

public class Controller {
    private final Game game;

    public Controller(Game game) {
        this.game = game;
    }

    public void run() {
        OutputView.printWelcomeMessage();
        while (!loop());
    }

    private boolean loop() {
        String command = InputView.readCommand();
        if (command.equals("start")) {
            OutputView.printStartMessage();
            game.start();
        }
        if (command.equals("end")) {
            OutputView.printEndMessage();
            game.end();
            return true;
        }
        return false;
    }
}

보통 이런식의 사용자 입력에 대한 조건문을 작성을 해본적이 있을 것이다.
이렇게 코드를 작성하게 된다면, 새로운 조건이 추가될 때 조건문이 하나 더 늘어나게 된다.

private boolean loop() {
    String command = InputView.readCommand();
    if (command.equals("start")) {
        OutputView.printStartMessage();
        game.start();
    }
    if (command.equals("end")) {
        OutputView.printEndMessage();
        game.end();
        return true;
    }
    // 새롭게 추가된 move 기능
    if (command.equals("move")) {
        OutputView.printMoveMessage();
        game.move();
    }
    return false;
}

만약 새로운 기능들이 계속 추가가 된다면 해당 if (command.eqauls("command"))와 같은 중복된 코드가 생기고, loop 메서드는 길이가 점점 늘어날 것이다.

어떻게 하면 중복된 조건문을 없애고, 메소드를 깔끔하게 유지할 수 있을까?

커맨드 패턴

커맨드 패턴을 사용하면 해당 부분을 깔끔하게 변경할 수 있다.

우선 위의 코드에서 중복되는 부분을 찾아보면 조건문이 어떠한 입력에 대해 game의 로직을 실행시키고, boolean 값을 반환하는 것을 볼 수 있다.

따라서, 우리는 해당 메소드를 Command라는 추상 클래스로 만들어 StartCommand, MoveCommand, EndCommand 라는 클래스로 분리를 할 수 있다.

public abstract class Command {
    protected final Game game;

    protected Command(Game game) {
        this.game = game;
    }

    public abstract boolean execute();
}
public class StartCommand extends Command {

    public StartCommand(Game game) {
        super(game);
    }

    @Override
    public boolean execute() {
        OutputView.printStartMessage();
        game.start();
        return false;
    }
}
public class MoveCommand extends Command {

    public MoveCommand(Game game) {
        super(game);
    }

    @Override
    public boolean execute() {
        OutputView.printMoveMessage();
        game.move();
        return false;
    }
}
public class EndCommand extends Command {

    public EndCommand(Game game) {
        super(game);
    }

    @Override
    public boolean execute() {
        OutputView.printEndMessage();
        game.end();
        return true;
    }
}
public class InvalidCommand extends Command {
    public static final InvalidCommand INSTANCE = new InvalidCommand(null);
    
    private InvalidCommand(Game game) {
        super(game);
    }

    @Override
    public boolean execute() {
        throw new IllegalArgumentException();
    }
}

이렇게 Command 추상 클래스를 구현하여 해당 Commnad의 로직에 관한 메소드를 분리할 수 있다.
이제 분리한 CommandController에 적용을 해보자

public class Controller {
    private final Map<String, Command> commandMap = new HashMap<>();

    public Controller(Game game) {
        commandMap.put("start", new StartCommand(game));
        commandMap.put("move", new EndCommand(game));
        commandMap.put("end", new MoveCommand(game));
    }

    public void run() {
        OutputView.printWelcomeMessage();
        while (!loop());
    }

    private boolean loop() {
        String command = InputView.readCommand();
        return commandMap.getOrDefault(command, InvalidCommand.INSTANCE)
                .execute();
    }
}

코드가 놀랍도록 깔끔해졌다.
이제 새로운 사용자 입력에 대한 기능이 추가로 생기더라도, commandMap에 새로운 Command 구현체를 넣어주면 변화에 쉽게 대응이 가능하게 된다!

하지만 여기 새로운 고민이 생겼다.
과연 Command라는 클래스가 도메인 클래스인 Game을 필드로 들고 있는게 합당할까?
어떻게 하면 Command에 도메인에 대한 의존을 없앨 수 있을까?

함수형 인터페이스 사용

Java 8부터 새로운 패러다임이 열렸다.
바로 함수형 인터페이스이다.
함수형 인터페이스를 사용하면 Command 클래스를 구체 클래스로 만들 필요가 없어진다.
또한, 도메인 로직이 Command에 노출되는 것도 막을 수 있다.
우선 바로 코드로 살펴보자.

public class Action {
    public static final Action INVALID_ACTION = new Action(() -> {
        throw new IllegalArgumentException();
    });

    private final BooleanSupplier payload;

    public Action(BooleanSupplier payload) {
        this.payload = payload;
    }

    public boolean execute() {
        return payload.getAsBoolean();
    }
}

기존 Command의 리턴 타입이 boolean이고, 파라미터가 없으므로 BooleanSupplier를 사용하여 로직을 추상화 시킬 수 있다.

이제 새로운 Action이라는 클래스를 활용하면 다음과 같다.

public class Controller {
    private final Map<String, Action> commandMap = new HashMap<>();
    private final Game game;

    public Controller(Game game) {
        this.game = game;
        commandMap.put("start", new Action(this::gameStart));
        commandMap.put("move", new Action(this::gameMove));
        commandMap.put("end", new Action(this::gameEnd));
    }

    public void run() {
        OutputView.printWelcomeMessage();
        while (!loop());
    }

    private boolean loop() {
        String command = InputView.readCommand();
        return commandMap.getOrDefault(command, Action.INVALID_ACTION)
        		.execute();
    }

    private boolean gameStart() {
        game.start();
        return false;
    }

    private boolean gameMove() {
        game.move();
        return false;
    }

    private boolean gameEnd() {
        game.end();
        return true;
    }
}

로직이 조금 많아지기는 했지만, 외부에 도메인 로직의 노출을 최소화시키고, 한 곳에서 도메인 로직을 관리할 수 있게 되었다!

결론

커맨드 패턴을 활용하여 기존의 조건문에서 사용자 입력을 모두 검사하고, 로직을 실행했던 것을 조건문을 하나도 사용하지 않고 변화에 쉽게 대응할 수 있는 코드를 작성할 수 있게 되었다.

여기서 한 발자국 더 나아간다면 상태 패턴Game 클래스에 적용하여, 메소드들의 boolean 반환 타입을 void로 변경시켜 더욱 깔끔한 코드를 작성해볼 수도 있다.

끝.

profile
꾸준히 성장하고 싶은 사람

0개의 댓글