[우아한테크 도전기] 이전 기수 프리코스 4주차_다리 건너기 연습

Dev_ch·2023년 9월 17일
0

우아한테크 도전기

목록 보기
35/51
post-thumbnail

이번 포스팅은 이전기수 프리코스 4주차 문제였던 다리 건너기를 구현하는 과정을 작성하고 회고해보는 시간을 가져보도록 하자! 마지막 문제였던 만큼 확실히 가장 구현하기 까다롭다고 생각했다.

🚀 문제

마지막 문제인 만큼 기능 요구 사항과 프로그래밍 요구 사항, 과제 진행 요구 사항이 많았다. 해당 요구 사항을 맞춰가면서 코드를 작성하다보니 까다로워 지는 부분이 꽤 많았던 것 같다.

📝 풀이 및 회고

확실히 이전 문제보다 구현해야할 메서드가 많아졌다. 위 사진에 들어가있는 메서드 외에도 요구사항에 더 적었고 개발하면서도 분리되는 메서드들이 많았다. 실제 프리코스에 진입하게 되면 노트를 이용해 그려가면서 설계를 해야할 것 같다.

예전부터 느낀거지만 펜이랑 노트로 흐름도나 그림을 그려가면서 기능을 설계하면 훨씬 수월하게 작업된다. 프리코스에서는 해당 방법을 이용해 기능을 설계하고 개발해야 할 것 같다.

이번에도 MVC 패턴을 기준으로 잡고 개발을 명세서도 작성하고 기능을 구현하였는데, 이렇게 작성하는게 확실히 편한 것 같다.

이번에는 Service로 따로 분리하지않고 Model에서 프로덕트 코드를 구현하였다. 마찬가지로 Bridge는 Main 함수가 존재한다.

Model

public class BridgeGame {

    private final List<String> gameBridge;
    private Integer totalPlay;
    private Integer nextMove;

    public Integer getTotalPlay() {
        return totalPlay;
    }

    public List<String> getGameBridge() {
        return gameBridge;
    }

    public Integer getNextMove() {
        return nextMove;
    }

    public BridgeGame(List<String> gameBridge) {
        this.gameBridge = gameBridge;
        this.totalPlay = 1;
        this.nextMove = 0;
    }

    public Boolean move(String selectMove) {
        String selectBridge = gameBridge.get(this.nextMove);
        this.nextMove++;
        return selectBridge.equals(selectMove);
    }

    public Boolean retry(String selectRetry) {
        if (selectRetry.equals("R")) {
            this.totalPlay++;
            this.nextMove--;
            return true;
        } else {
            return false;
        }
    }
}

내 생각에는 해당 게임을 구현하는 방법은 되게 여러가지라고 생각한다. 필자의 경우 객체 내 필드에서 nextMove를 내가 선택한 움직임과 확인하여 성공 / 실패 여부를 판단하고 한칸의 다리를 건너게 구현해주었다.

게임 재시작도 마찬가지로 사용자가 게임을 재시작할경우 게임 총 횟수가 증가하면서 움직였던 nextMove를 다시 뒤로 보내주어 게임을 진행하도록 구현하였다.

여기서 아쉬운점은 move와 retry의 반환값이 다른 로직과 여러번을 거쳐 게임의 결과나 진행이 도출된다는 것인데 분명 리팩터링이 가능해보인다 🥲 이 부분은 더욱 고민해봐야 할 것 같다.

public class BridgeMaker {
    private final BridgeNumberGenerator bridgeNumberGenerator;

    public BridgeMaker(BridgeNumberGenerator bridgeNumberGenerator) {
        this.bridgeNumberGenerator = bridgeNumberGenerator;
    }

    public List<String> makeBridge(int size) {
        List<String> bridge = new ArrayList<>();

        for (int i = 0; i < size; i++) {
            addSuccessBridge(bridge, bridgeNumberGenerator.generate());
        }

        return bridge;
    }

    private static void addSuccessBridge(List<String> bridge, int random) {
        if (random == 0) {
            bridge.add("U");
        } else {
            bridge.add("D");
        }
    }
}

해당 클래스는 사용자가 다리의 길이를 입력했을때 게임의 다리를 랜덤하게 만드는 클래스이다. 프리코스에서 제공된 클래스를 사용하여 리스트 안에 랜덤한 "U" 방향과 "D" 방향이 들어가도록 구현하였다.

제공된 BridgeNumberGenerator와 BridgeRandomNumberGenerator를 이용하여 랜덤한 수가 나오고 그 수에 맞춰 U / D 를 넣어주었다.

View

이번 문제는 View 특히 OutputView 에서 고민이 정말 많았다. InputView와 OutputView 클래스가 나누어져 있었는데 아래는 각각의 구현이다.

public int readBridgeSize() {
        System.out.println("다리의 길이를 입력해주세요.");
        String strBridgeSize = readLine();
        System.out.println();

        validateInputIsNull(strBridgeSize);

        return parseIntBridgeSize(strBridgeSize);
    }

    public String readMoving() {
        System.out.println("이동할 칸을 선택해주세요. (위: U, 아래: D)");
        String move = readLine();
        validateInputIsNull(move);
        validateReadMoving(move);

        return move;
    }

    public String readGameCommand() {
        System.out.println("게임을 다시 시도할지 여부를 입력해주세요. (재시도: R, 종료: Q)");
        String command = readLine();
        validateInputIsNull(command);
        validateReadGameCommand(command);

        return command;
    }

InputView의 경우 크게 어려운건 없었다. 사용자에게 안내 메세지를 출력하고 readLine을 통해 값을 읽어왔다. 읽어온 값을 validate를 통해 검증을 시켜주고 값을 반환해주었다.

public class OutputView {

    private final static String SUCCESS_BRIDGE = " O ";
    private final static String FAIL_BRIDGE = " X ";
    private final static String SPACE_BRIDGE = "   ";
    public static final String UP = "U";

    public void printStart() {
        System.out.println("다리 건너기 게임을 시작합니다.\n");
    }

    public String printMap(BridgeGame bridgeGame, Boolean isSuccess) {
        StringBuilder upBridge = new StringBuilder().append("[");
        StringBuilder downBridge = new StringBuilder().append("[");

        appendMapStatus(bridgeGame, isSuccess, upBridge, downBridge);

        String map = upBridge.append("]\n").append(downBridge).append("]\n").toString();
        System.out.println(map);

        return map;
    }

    private static void appendMapStatus(BridgeGame bridgeGame, Boolean isSuccess, StringBuilder upBridge, StringBuilder downBridge) {
        for (int i = 0; i < bridgeGame.getNextMove(); i++) {
            String control = bridgeGame.getGameBridge().get(i);

            if (i == bridgeGame.getNextMove() - 1) {  // 마지막 인덱스인 경우 성공여부에 따른 결과 등록
                appendBridgeBasedOnStatus(isSuccess, upBridge, downBridge, control);
            } else { // 아닌 경우 전부 O로 등록
                appendSuccessBridge(upBridge, downBridge, control);
                upBridge.append("|");
                downBridge.append("|");
            }
        }
    }

    private static void appendBridgeBasedOnStatus(
            Boolean isSuccess, StringBuilder upBridge, StringBuilder downBridge, String control
    ) {
        if (isSuccess) {
            appendSuccessBridge(upBridge, downBridge, control);
        } else {
            appendFailBridge(upBridge, downBridge, control);
        }
    }

    private static void appendFailBridge(StringBuilder upBridge, StringBuilder downBridge, String control) {
        if (control.equals(UP)) {
            upBridge.append(SPACE_BRIDGE);
            downBridge.append(FAIL_BRIDGE);
        } else {
            upBridge.append(FAIL_BRIDGE);
            downBridge.append(SPACE_BRIDGE);
        }
    }

    private static void appendSuccessBridge(StringBuilder upBridge, StringBuilder downBridge, String control) {
        if (control.equals(UP)) {
            upBridge.append(SUCCESS_BRIDGE);
            downBridge.append(SPACE_BRIDGE);
        } else {
            upBridge.append(SPACE_BRIDGE);
            downBridge.append(SUCCESS_BRIDGE);
        }
    }

    public void printResult(String map) {
        System.out.println("최종 게임 결과");
        System.out.println(map);
    }

    public void printResultMessage(Boolean clear, Integer totalPlay) {
        String message = getResultMessage(clear);

        System.out.printf("게임 성공 여부: %s\n", message);
        System.out.printf("총 시도한 횟수: %s\n", totalPlay);
    }

    private static String getResultMessage(Boolean clear) {
        String message;
        if (clear) {
            message = "성공";
        } else {
            message = "실패";
        }
        return message;
    }

}

결과에 맞추어 사용자가 건너온 Map을 만드는 printMap 메서드를 구현하는 것이 고민이 정말 많았다. 애초에 View에서 이러한 책임을 지게하는게 맞는지도 잘 모르겠다. 특히 요구 사항에 하나의 메서드가 10줄이 넘어가면 되지 않아야 하기에 함수로 어떻게 분리해야 할까 고민도 많았다.

아무튼 View 에서 많은 책임을 지게 한 것 같아 좋은 코드라고는 볼 수 없을 것 같다. 리팩터링을 어떻게 해야 할지 감이 조금 안 잡히는데, 만약 하게 된다면 수행되는 로직 자체가 좀 변경되어야 하지 않을까 싶다.

Controller

public class BridgeController {

    private final OutputView outputView = new OutputView();
    private final InputView inputView = new InputView();
    private final BridgeMaker bridgeMarker;

    public BridgeController(BridgeRandomNumberGenerator bridgeRandomNumberGenerator) {
        this.bridgeMarker = new BridgeMaker(bridgeRandomNumberGenerator);
    }

    public void startMessagePrint() {
        outputView.printStart();
    }

    public BridgeGame createBridgeGame() {
        int inputSize = inputView.readBridgeSize();
        List<String> gameBridge = bridgeMarker.makeBridge(inputSize);
        return new BridgeGame(gameBridge);
    }

    public void playBridgeGame(BridgeGame bridgeGame) {
        while (true) {
            Boolean isSuccess = bridgeGame.move(inputView.readMoving()); // 이동 후 알맞은 다리인지 여부 확인
            String map = outputView.printMap(bridgeGame, isSuccess); // 현재 자신이 건너온 다리 위치 출력

            if (isRestartRequested(bridgeGame, isSuccess, map)) { // 알맞은 다리를 건너지 않았을때 커맨드가 Q 라면 게임 종료
                break;
            }
            if (isGameClear(bridgeGame, map)) { // 다리를 다 건넜다면 게임종료
                break;
            }
        }
    }

    private boolean isGameClear(BridgeGame bridgeGame, String map) {
        if (bridgeGame.getGameBridge().size() == bridgeGame.getNextMove()) {
            return isGameStop(map, true, bridgeGame);
        }
        return false;
    }

    private boolean isRestartRequested(BridgeGame bridgeGame, Boolean isSuccess, String map) {
        if (!isSuccess) {
            String command = inputView.readGameCommand();
            Boolean retry = bridgeGame.retry(command);
            if (!retry) {
                return isGameStop(map, false, bridgeGame);
            }
        }
        return false;
    }

    private boolean isGameStop(String map, boolean clear, BridgeGame bridgeGame) {
        outputView.printResult(map);
        outputView.printResultMessage(clear, bridgeGame.getTotalPlay());
        return true;
    }
}

이번엔 Controller의 경우에도 기능을 조금 분리해주게 되었다. 문제는 Controller에서 해당 책임을 지는게 맞나? 라는 생각이 든다. Controller는 오로지 model과 view를 연결해주는 매개체 역할을 수행해야 한다고 생각하는데 Controller에서 책임을 지게된 부분이 많아졌다는 것 이다.

세가지 기능을 위주로 메서드를 분리해 그 안에서 한번 더 하나의 책임만 질 수 있도록 분리 해주긴 하였다.

Main

public class Bridge {
    private static final BridgeController bridgeController = new BridgeController(new BridgeRandomNumberGenerator());

    public static void main(String[] args) {
        bridgeController.startMessagePrint();
        bridgeController.playBridgeGame(bridgeController.createBridgeGame());
    }
}

아무튼 Main에서는 분리된 컨트롤러의 역할에 맞춰 수행되도록 코드를 작성해주었다.


아쉬운점

마지막 문제이다보니 구현도 나름 까다로웠고 특히 아쉬운점이 많았다. 특히 책임에 관련해서 많이 부족하다고 느꼈고 기능을 분리함에 있어도 많이 까다로웠던 것 같다. 애초에 로직이 약간 엉성한 느낌이 있는 것 같긴하다.

해당 코드를 리팩터링하게 된다면 위에서 말했듯 기능들이 수행되는 로직 자체가 변경되어야 할 것 같다. 이렇게 구현하고보니 느낀 것은 처음부터 설계를 잘해야한다고 느꼈다. 기능의 설계들이 자연스럽게 연결되지 않고 단순한 로직으로 설계되어있다보니 구현하면서 이게 맞나? 라거나 맞지 않는 부분이 발생하면 그때 수정을 하게 된다.

어떠한 기능을 구현하려면 역시 설계가 제일 중요한 것 같다. 물론 해당 문제들이 시간싸움이라면 설계를 하는 단계가 길어질수록 힘들겠지만, 여유가 있는 상황이라면 애초에 처음부터 설계를 잘 잡고 들어가야 한다는 것 이다.

필자의 경우도 Spring으로 서버 개발을 맡을 때 ERD 부터 구현하려는 API의 설계를 그려보곤한다. 특히나 어려운 로직이나 쿼리가 복잡해진다면 펜을 들고 노트를 꺼내는 것은 정말 필수적이다.

이전 기수문제들을 풀어보면서 느꼈듯이 설계의 중요성과 기능의 분리, 책임에 대한 원칙을 자세하게 느낄 수 있었다. 실제 프리코스가 시작하기 한달 정도 남은 상황에서 이렇게 문제를 다 풀어보아 느낀 점이 많아 다행이라고 생각한다.

자바는 더 연습하자...😅

profile
내가 몰입하는 과정을 담은 곳

0개의 댓글