우테코 미션
을 수행하거나 간단한 프로그램을 구현하여 볼 때 사용자 입력
에 따라 어떠한 결과를 기대하는 로직을 작성해 본 적이 있을 것이다.
사용자 입력
에 대해 조건문
을 사용하여 원하는 결과에 맞게 출력하는 로직을 작성하게 된다면 모든 조건에 사용자가 입력한 커맨드에 대해 검사를 해야한다.
조건이 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
의 로직에 관한 메소드를 분리할 수 있다.
이제 분리한 Command
를 Controller
에 적용을 해보자
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
로 변경시켜 더욱 깔끔한 코드를 작성해볼 수도 있다.
끝.