[우테코-프리코스] 4주차 회고

dooboocookie·2022년 11월 23일
7

우테코-프리코스

목록 보기
4/4

과제

, 아래 두 칸으로 이루어진 다리를 끝까지 건너면 성공하는 게임

게임 진행 순서

  1. 다리를 생성한다.
    • 다리의 길이는 3~20의 숫자를 입력받아 생성한다.
    • 0은 아래 타일, 1은 위 타일을 생성한다.
    • 위의 예시와 같은 다리는 1,0,0,1,0,1,1로 생성한 다리다.
  2. 플레이어가 한칸씩 다리를 건넌다.
    • 건널 위치는 U 혹은 D를 입력하여 선택한다.
    • 끝까지 가는데 성공하면 게임 종료
  3. 플레이어가 이동을 하면 지금 까지 건넌 경로를 표시한다.
    • 잘 건넌 타일은 O
    • 건너는데 실패한 타일은 X
    • 선택하지 않은 타일은 공백으로 표시
  4. 다리에서 떨어지면 재시작 여부를 물음
    • R을 선택하면 처음부터 재시작
    • Q를 선택하면 실패인 상태로 게임 종료
  5. 게임이 종료되면 총 시도 횟수 와 결과를 출력

입 출력 예시

다리 건너기 게임을 시작합니다.

다리의 길이를 입력해주세요.
7
이동할 칸을 선택해주세요. (위: U, 아래: D)
U
[ O ]
[   ]

이동할 칸을 선택해주세요. (위: U, 아래: D)
D
[ O |   ]
[   | O ]

이동할 칸을 선택해주세요. (위: U, 아래: D)
U
[ O |   | X ]
[   | O |   ]

게임을 다시 시도할지 여부를 입력해주세요. (재시도: R, 종료: Q)
R
이동할 칸을 선택해주세요. (위: U, 아래: D)
U
[ O ]
[   ]

...

이동할 칸을 선택해주세요. (위: U, 아래: D)
D
[ O |   |   | O |   | O |   ]
[   | O | O |   | O |   | X ]

게임을 다시 시도할지 여부를 입력해주세요. (재시도: R, 종료: Q)
R
이동할 칸을 선택해주세요. (위: U, 아래: D)
U
[ O ]
[   ]

...

이동할 칸을 선택해주세요. (위: U, 아래: D)
U
[ O |   |   | O |   | O | O ]
[   | O | O |   | O |   |   ]
  
최종 게임 결과
[ O |   |   | O |   | O | O ]
[   | O | O |   | O |   |   ]

게임 성공 여부: 성공
총 시도한 횟수: 2

프로그래밍 요구 사항

  1. 지난 주 과제(링크)에 추가된 요구 사항이 있다.
  2. 함수의 길이 15라인 제한에서 10라인 제한으로 줄음
  3. 메서드의 파라미터 개수는 최대 3개까지만 허용
  4. InputView, OutputView, BridgeGame, BridgeMaker, BridgeRandomNumberGenerator클래스 요구사항에 맞춰 각 클래스 활용하여 프로그램 구현
    • BridgeGame에서 View사용하지 않음

MVC 패턴

├── controller
│   └── BridgeGameController.java
├── model
│   ├── Bridge.java
│   ├── BridgeGame.java
│   ├── PlayerPath.java
│   ├── PlayerStatus.java
│   ├── Tile.java
│   └── TryCount.java
├── util
│   ├── Errors.java
│   ├── Rules.java
│   └── Validator.java
├── view
│   ├── InputView.java
│   └── OutputView.java
├── Application.java
├── BridgeMaker.java
├── BridgeNumberGenerator.java
└── BridgeRandomNumberGenerator.java
  • 지난 주와 마찬가지로 MVC 패턴을 활용하여 클래스를 최대한 나누려고 노력하였다.

  • 위와 같은 구조를 갖도록 설계를 하였다.
  • 다이어그램으로 뽑아보니 그래도 저번 주보다 많이 성장한 듯한 느낌이 들었다.
  • BridgeGame에서 View를 사용하지 못한다는 요구 사항을 토대로 작성했다.

Controller

  • BridgeController는 어떤 모델이 비즈니스 로직을 실행하는지 결정하고, 그에 대한 결과를 뷰에 전달한다.
  • BridgeConroller에는 비즈니스 로직이 없고, 위의 기능만 있어야된다고 생각했지만 그렇게 쉽게 되지는 않았다.

컨트롤러의 역할을 넘어버린 것 같다..

  • 컨트롤러는 어떤 요청을 받고 그에 대한 비즈니스 로직과 View를 연결해주는 역할만 하길 원했다.

하지만...

  1. 다리에서 떨어지거나 완전히 건널 때까지 다리 선택 건너기를 반복해서 호출해야한다.
// controller에서 재귀 함수의 형식으로 구현
public void playRound() {
    selectTile(); 
    if (isSuccessAndNotVictory()) {
    	// 재귀 호출
        playRound();
    }
}

private boolean isSuccessAndNotVictory() {
    return (bridgeGame.checkPlayerStatus() != PlayerStatus.COMPLETE_CROSSING_BRIDGE)
            && (bridgeGame.possibleNextStep());
}

public void selectTile() {
	String nextStep = inputView.readMoving();
    moveBridge(nextStep);
}

public void moveBridge(String nextStep) {
    bridgeGame.move(nextStep);
    outputView.printMap(bridgeGame.possibleNextStep(), bridgeGame.getPlayerPath());
}
  1. 떨어졌으면 재시작 여부 성공했으면 결과 출력을 해야한다.
public void playGame() {
    playRound();
    if (bridgeGame.checkPlayerStatus() != PlayerStatus.COMPLETE_CROSSING_BRIDGE) {
        askRetry();
    }
}

public void askRetry() {
    try {
        String retryRorQ = inputView.readGameCommand();
        decideRetryOrQuit(retryRorQ);
    } catch (IllegalArgumentException illegalArgumentException) {
        outputView.printErrorMessage(illegalArgumentException);
        askRetry();
    }
}

// 3-6. 재시작 선택시 재시작하는 메소드 호출
private void decideRetryOrQuit(String retryRorQ) {
    if (bridgeGame.retry(retryRorQ)) {
        playGame();
    }
}
  • 이런 게임 순서에 관련된 로직이 컨트롤러에 있을 수 밖에 없었다.

이상적이라고 생각했던 컨트롤러의 역할은 이런 것이었다.

public void gameResult() {
    PlayerStatus playerStatus = bridgeGame.checkPlayerStatus();
    int tryCount = bridgeGame.getTryCount();
    List<Tile> playerPath = bridgeGame.getPlayerPath();
    outputView.printResult(playerStatus, tryCount, playerPath);
}
  • 이런식으로 단순히 비즈니스 로직에 대한 결과물을 뷰로 전달해 주는 과정만 있어야 됐다고 생각했다.
  • 저 위에 재귀호출을 하는 식의 방식으로 진행순서에 관여를 하는게 컨트롤러에서 하는 역할이 맞나? 라는 생각이 들었다.
  • 반복해야되는 호출이 입력이나 출력을 포함한 작업이여서 컨트롤러말고는 구현할 위치를 아직 찾지 못하였다..
  • 이는 일단 아쉬움으로 남기고 다른 분들의 코드를 보면서 공부해볼 예정이다.

View

  • InputView는 큰 무리 없이 입력 시 지켜야할 검증 사항과 Console.readLine()으로 입력 받은 값을 적절한 타입으로 파싱해서 넘겨주는 역할을 부여하였다.
  • 문제는 OutputView였다...

View에 어떤 값을 전달해줘야 되지?

  • 꽤 헷갈렸던 부분이었다...
  • 일단 Model 객체에서 toString을 재정의하는 것은 출력에 관련된 부분이므로 절대 하지 않았다.
    • 만약 출력하는 형식이 바뀌거나, 출력을 HTML로 렌더링 해야된다면 전혀 다른 방식으로 출력이 될태니...
  • 따라서 출력되어야되는 모양보다는 모델에서 로직이 끝난 결과를 전달 받아서 그 결과를 뷰가 어느 정도 판별하여 페이지의 출력할 수 있도록 하였다.
    • 근데 저 판별하는 부분이 맞는지 아직 잘 모르겠다...
public void printMap(boolean possibleNextStep, List<Tile> playerPath) {
	// 윗쪽 타일을 출력
    System.out.println(tracePathByTile(possibleNextStep, playerPath, Tile.UP_TILE));
    // 아랫쪽 타일을 출력
    System.out.println(tracePathByTile(possibleNextStep, playerPath, Tile.DOWN_TILE));
    System.out.println();
}

// 위나 아래의 다리를 String으로 그리는 부분
private String tracePathByTile(boolean possibleNextStep, List<Tile> playerPath, Tile bridgeTile) {
    StringBuilder tileRecord = new StringBuilder(MAP_PREFIX);
    for (int pathIndex = 0; pathIndex < playerPath.size() - 1; pathIndex++) {
        tileRecord.append(getStringTile(playerPath.get(pathIndex), bridgeTile));
        tileRecord.append(MAP_SEPARATOR);
    }
    tileRecord.append(getLastTile(possibleNextStep, playerPath.get(playerPath.size() - 1), bridgeTile));
    tileRecord.append(MAP_SUFFIX);
    return tileRecord.toString();
}

// "O","X"," "인지를 판별하는 부분 (이 부분이 헷갈린다)
private String getStringTile(Tile playerStep, Tile bridgeTile) {
    if (playerStep.equals(bridgeTile)) {
        return MAP_SUCCESS_STEP;
    }
    return MAP_NOT_STEP;
}
  • 이 과정에서 마지막에 O,X, 를 판별하는 부분이 제일 헷갈렸다.
  • 이건 결과를 처리하는 모델에서 저 O,X,공백을 담은 리스트같은걸 뷰에 전달해줘서 뷰는 그것을 출력하기만해야될까?
  • 일단 스스로는 아니라는 결론을 내렸다.
    • O나 X, 공백 같은 어떤 스트링으로 결과를 출력한다는 것은 콘솔창에 출력할 때 한정이라는 생각이 들었고, 뷰가 어떤식으로 바뀐다면, 그건 뷰의 코드가 바껴야된다고 생각했기 때문이다.

Model

  • 모델의 구조는 2~3주차에 비하면 복잡하진 않았다.
클래스 내용
BridgeGame 게임 진행 상황을 관리하는 클래스
Bridge 다리의 Up Down 여부를 List로 갖고 있는 일급 컬렉션
PlatyerPath 플레이어가 지나간 길이 Up Down인지 List로 갖고 있는 일급컬렉션
Tile 다리의 위치가 Up인지 Down인지 값을 나타내는 enum
TryCount 게임이 시작되고 시도한 횟수(int)를 관리하는 클래스
PlayerStatus 플레이어의 성공 여부를 값을 나타내는 enum

고민했던 몇가지..

  • 여기서 나온 클래스들은 이 글 제일 처음에 요구 분석을 할 때 강조 표시를 했던 몇몇 엔티티들이다.
  • 그래서, 제일 고민이었던 것은 Player 클래스를 만들지 고민했다.
  • 만약 이렇게 된다면 BridgeGame에는 Player와 Bridge가 속하게 되고 Player 안에 TryCount와 PlayerPath와 PlayerStatus가 있는 상태로 구성을 했을 것이다.
  • 그렇다 보니 PlayerPath는 Bridge와 연관이 많은 부분이라 Player를 하나 더 거친다는게 비효율적이라는 생각이 들었다.
  • 또한 Player가 한 게임에 여러 명 등장할 수 있으면, Player를 만들었겠지만 그렇지 않기 때문에 그냥 Player없이 진행하였다.

검증과 예외처리에 대한 고민

  • 이번에 바뀐 예외 요구사항이 예외가 발생이 되면 입력을 다시 받는다였다.
  • 보자마자 든 생각은 while문이나 재귀함수를 쓰면 되겠다.였다.
  • 근데 문제는 그 검증과 예외처리를 어디서 하지?

검증 처리를 하는 위치는 InputView와 각 Model 클래스로 정했다.

  • Bridge를 만드는 과정을 생각해보면
  • InputView에서만 검증 처리를 해도 되지 않을까? 라는 생각을 처음에 했다.
  • 이 프로그래밍에서는 Bridge가 만들어질려면 무조건 InputView에서 입력을 받고 만들어져야했다.
  • 하지만, 이건 너무 뷰에 의존적이라는 생각이 들었다.
  • 예를 들어 이게 웹 어플리케이션이라고 쳤을 때, 입력 값을 파라미터 정보로 받게 된다.
  • 파라미터 정보는 HTTP 요청을 충분히 조작하여 넘길 수 있는데 이것이 인풋뷰에서만 검증이 이루어 지게 되면, 잘못된 Bridge가 만들어질 가능성이 생기는 것이다.
  • 그렇기 때문에, 사이즈에 대한 검증은 모델 클래스 자체에도 포함 시켰다.
//InputView.java 의 입력받는 메소드
public int readBridgeSize() {
    System.out.println(ASK_BRIDGE_SIZE);
    String input = readString();
    // 타입 검증
    Validator.validateNumberType(input);
    int inputNumber = Integer.parseInt(input);
    // 사이즈 검증
    Validator.validateBridgeSize(inputNumber);
    System.out.println();
    return inputNumber;
}
//Bridge.java 의 생성자
public Bridge(int bridgeLength) {
	// 사이즈 검증
    Validator.validateBridgeSize(bridgeLength);
    BridgeRandomNumberGenerator bridgeRandomNumberGenerator = new BridgeRandomNumberGenerator();
    BridgeMaker bridgeMaker = new BridgeMaker(bridgeRandomNumberGenerator);
    List<String> makeBridge = bridgeMaker.makeBridge(bridgeLength);
    bridge = bridgeStringToTile(makeBridge);
}
  • 이런식으로 입력 받는 부분에서도 사이즈에 대한 검증을 하고, 실제로 객체가 만들어지는 생성자에서도 검증을 하였다.

그렇다면, 이 예외 처리를 어디서 해야될까?

  • 처음에는 InputView에서 할려했다.
  • 그렇다면, Model에서 발생한 예외는 어떻게 처리르 해야될까?
    • 모델과 뷰는 완전히 분리되어 있기 때문에 모델에서 예외가 발생된다고 해서 인풋뷰에 있는 메소드를 다시 호출할 방법은 없었다.
  • 그래서 결론은 이 두가지를 호출하는 Controller에서 예외처리를 하도록 하였다.
//BridgeGameController.java 에서 다리를 만드는 과정
public void createNewBridge() {
    try {
        int bridgeLength = inputView.readBridgeSize();
        bridgeGame.newBridge(bridgeLength);
    } catch (IllegalArgumentException illegalArgumentException) {
    	// 예외 메세지 출력
        outputView.printErrorMessage(illegalArgumentException);
        // 다시 해당 메소드 실행
        createNewBridge();
    }
}
  • 이것이 제일 맞는 방향인지 아직 확신은 없으나, 내가 느낀 바로는 최선의 방법이었다.. (다른 분들의 의견이 좀 궁금하다..)
  • 사실 이렇다 보니 사이즈에 대한 검증을 진행하고 랜덤번호를 발생시켜야했고, 그 검증에 대한 책임은 InputView와 Bridge에 있었다.
  • 그래서 어쩔 수 없이 랜덤 번호를 발생시키는 부분을 생성자 안에 포함시켰는데, 이는 지난 주 피드백 내용과는 상반되는 내용이다..(테스트 진행에 대한 피드백이였는데, 이 문제는 모킹을 통하여 해결하였다)

예상치 못한 예외

  • 요구 사항에서 예외 처리시 Exception 같은 전체적인 예외처리말고 IllegalArgumentException에 대해서 예외처리를 하도록 하였다.
  • 하지만, 내가 모든 이 프로그램의 예외 상황을 예측 했다고 생각하지 않았다.
  • 그래서 Application.java 의 main()메소드에서 전체적인 예외에 대해서 메세지를 출력할 수 있는 부분을 추가하였다.
public static void main(String[] args) {
    try {
        BridgeGameController controller = new BridgeGameController();
        controller.run();
    } catch (Exception exception) {
        // 예측 못한 에러 발생 시 메세지 출력 후 프로그램 종료
        OutputView.printUnExpectedErrorMessage(exception);
    }
}
private static final String UNEXPECTED_EXCEPTION = "예상치 못한 오류가 발생하여 프로그램 종료합니다.";

public static void printUnExpectedErrorMessage(Exception exception) {
    System.out.println(UNEXPECTED_EXCEPTION);
    System.out.println(exception.getMessage());
}
  • 요구사항에 명시되어있는 예외 상황에 대해서는 IllegalArgumentException이 발생하도록 하였고, IllegalArgumentException의 예외처리를 하도록 짜놨지만 main()메소드 만큼은 저렇게 Exception으로 뭉틍그려 처리해놨다.
  • 이것이 요구사항을 어기는 것일까? 라는 약간의 두려움이 있긴 했지만, 내가 프로그램을 짠다면 필수적으로 있어야되는 부분이라는 생각이 들어서 (나름) 용기내어 저 부분을 추가하였다..

후기

이번 주는 MVC 패턴에 대한 고민을 많이 하였다.
컨트롤러가 해야될 역할, 뷰가 해야될 역할, 검증과 예외처리를 어디서 해야될 지, ...
이 글을 쓰며, 이전의 과제들에 대해서 많이 생각하게 되었다.
2~3주 전과 비교해 지금 실력이 훨씬 향상되었다는 생각이 든다. 이전 과제들의 코드를 보면 정말 내멋대로 코드를 짯다...
피어리뷰에 올리신 코드들을 보면 너무나 잘하는 분들이 많아 위축이 된다.
하지만 이 프리코스라는 과정을 통해서 어떻게 생각해야 되는지, 어떻게 성장해야 되는지... 성장 방향성에 대해서 감을 잡을 수 있는 시간이어서 너무 감사한 마음이 크다.
최종 코딩 테스트에 가게될 지는 모르겠지만, 지금까지 푼 과제들을 다시 수정해보고 다른 분들의 코딩을 보며 배우면서 다시 복기하는 과정을 보낼 예정이다.
그러다 보면 결과와는 상관없이 성장할 수 있을 것이다.

다들 너무 고생많으셨고, 많이 가르침 주셔서 감사하다는 말씀 전해드리고 싶습니다.

profile
1일 1산책 1커밋

5개의 댓글

comment-user-thumbnail
2022년 11월 23일

잘보고갑니다!
한달간 고생많으셨어요!!

1개의 답글
comment-user-thumbnail
2022년 11월 24일

고생하셨습니다! 좋은 결과 있으실 겁니다.

1개의 답글
comment-user-thumbnail
2022년 11월 29일

Thanks for sharing that It is a great. FaceTime App

답글 달기