Seren dev's blog
article thumbnail

프리코스 4주차 미션을 진행한 과정, 느낀 점, 개선할 점을 작성한 회고록이다.

 

프리코스 4주차 다리 건너기 미션 Github 링크

🎯 프로그래밍 요구 사항

추가된 요구 사항

  • 함수(또는 메서드)의 길이가 10라인을 넘어가지 않도록 구현한다.
    • 함수(또는 메서드)가 한 가지 일만 잘하도록 구현한다.
  • 메서드의 파라미터 개수는 최대 3개까지만 허용한다.
  • 아래 있는 InputView, OutputView, BridgeGame, BridgeMaker, BridgeRandomNumberGenerator 클래스의 요구사항을 참고하여 구현한다.
    • 각 클래스의 제약 사항은 아래 클래스별 세부 설명을 참고한다.
    • 이외 필요한 클래스(또는 객체)와 메서드는 자유롭게 구현할 수 있다.
    • InputView 클래스에서만 camp.nextstep.edu.missionutils.Console 의 readLine() 메서드를 이용해 사용자의 입력을 받을 수 있다.
    • BridgeGame 클래스에서 InputView, OutputView 를 사용하지 않는다.

InputView 클래스

  • 제공된 InputView 클래스를 활용해 구현해야 한다.
  • InputView의 패키지는 변경할 수 있다.
  • InputView의 메서드의 시그니처(인자, 이름)와 반환 타입은 변경할 수 있다.
  • 사용자 값 입력을 위해 필요한 메서드를 추가할 수 있다.
public class InputView {

    public int readBridgeSize() {
        return 0;
    }

    public String readMoving() {
        return null;
    }

    public String readGameCommand() {
        return null;
    }
}

OutputView 클래스

  • 제공된 OutputView 클래스를 활용해 구현해야 한다.
  • OutputView의 패키지는 변경할 수 있다.
  • OutputView의 메서드의 이름은 변경할 수 없고, 인자와 반환 타입은 필요에 따라 추가하거나 변경할 수 있다.
  • 값 출력을 위해 필요한 메서드를 추가할 수 있다.
public class OutputView {

    public void printMap() {
    }

    public void printResult() {
    }
}

BridgeGame 클래스

  • 제공된 BridgeGame 클래스를 활용해 구현해야 한다.
  • BridgeGame에 필드(인스턴스 변수)를 추가할 수 있다.
  • BridgeGame의 패키지는 변경할 수 있다.
  • BridgeGame의 메서드의 이름은 변경할 수 없고, 인자와 반환 타입은 필요에 따라 추가하거나 변경할 수 있다.
  • 게임 진행을 위해 필요한 메서드를 추가 하거나 변경할 수 있다.
public class BridgeGame {

    public void move() {
    }

    public void retry() {
    }
}

BridgeMaker 클래스

  • 제공된 BridgeMaker 클래스를 활용해 구현해야 한다.
  • BridgeMaker의 필드(인스턴스 변수)를 변경할 수 없다.
  • BridgeMaker의 메서드의 시그니처(인자, 이름)와 반환 타입은 변경할 수 없다.
public class BridgeMaker {

    public List<String> makeBridge(int size) {
        return null;
    }
}

BridgeRandomNumberGenerator 클래스

  • Random 값 추출은 제공된 bridge.BridgeRandomNumberGenerator의 generate()를 활용한다.
  • BridgeRandomNumberGenerator, BridgeNumberGenerator 클래스의 코드는 변경할 수 없다.

사용 예시

  • 다리 칸을 생성하기 위한 Random 값은 아래와 같이 추출한다.
int number = bridgeNumberGenerator.generate();

 

4주차 미션 구현 내용


4주차 미션 목표 : 3주차 + 클래스(객체)를 분리하는 연습 & 리팩토링

⭐어려웠던 점

함수(또는 메서드)의 길이가 10라인을 넘어가지 않도록 구현하며, 메서드의 파라미터 개수는 최대 3개까지만 허용한다.

이 요구사항을 지키기가 가장 힘들었는데, 함수의 길이를 줄이면 파라미터의 개수가 늘어나고, 파라미터의 개수를 줄이면 함수의 길이가 늘어났다;;

특히 현재까지 이동한 다리의 상태를 출력하는 OutputView의 printMap 메서드를 처음 작성할 때는 20줄이 넘어갔다.

package bridge.view;

import bridge.domain.Bridge;
import bridge.domain.BridgeGame;
import bridge.domain.UserPath;

/**
 * 사용자에게 게임 진행 상황과 결과를 출력하는 역할을 한다.
 */
public class OutputView {

    private static final String BRIDGE_PREFIX = "[ ";
    private static final String DELIMITER = " | ";
    private static final String BRIDGE_SUFFIX = " ]";
    private static final String PASS = "O";
    private static final String NO_PASS = "X";
    private static final String EMPTY = " ";

    /**
     * 현재까지 이동한 다리의 상태를 정해진 형식에 맞춰 출력한다.
     * <p>
     * 출력을 위해 필요한 메서드의 인자(parameter)는 자유롭게 추가하거나 변경할 수 있다.
     */
    public void printMap(BridgeGame bridgeGame) {
        Bridge bridge = bridgeGame.getBridge();
        UserPath userPath = bridgeGame.getUserPath();

        StringBuilder output = new StringBuilder(BRIDGE_PREFIX);

        int userPosition = bridgeGame.getUserPosition();
        for (int i = 0; i < userPosition; i++) {
            if (bridge.compareWithPosition(i, "U")) {
                if (userPath.find(i).equals("U")) {
                    output.append(PASS);
                }
                else output.append(NO_PASS);
            }
            else output.append(EMPTY);

            if (i != userPosition - 1)
                output.append(DELIMITER);
        }
        output.append(BRIDGE_SUFFIX);

        System.out.println(output);
    }
    
    public void printResult() {
    }
}

위 코드는 "else 예약어를 쓰지 않는다."는 요구사항도 지키지 못했다.

 

함수(메서드)가 10줄이 넘어가지 않도록 하고, 파라미터는 최대 3개만 사용하기 위해 하나의 메서드는 한 가지의 일만 하도록 메서드를 최대한 작게 분리했다. 또한 else 예약어를 쓰지 않기 위해 메서드를 분리하여 if 조건절에서 문자열을 바로 return 하도록 구현했다.

package bridge.view;

import bridge.domain.Bridge;
import bridge.domain.BridgeGame;
import bridge.domain.UserPath;

public class OutputView {

    private static final String BRIDGE_PREFIX = "[ ";
    private static final String DELIMITER = " | ";
    private static final String BRIDGE_SUFFIX = " ]";
    private static final String PASS = "O";
    private static final String NO_PASS = "X";
    private static final String EMPTY = " ";

    private static final String TOTAL_RESULT = "최종 게임 결과";
    private static final String IS_SUCCESS = "게임 성공 여부: ";
    private static final String SUCCESS = "성공";
    private static final String FAIL = "실패";
    private static final String TRY_NUMBER = "총 시도한 횟수: ";

    public void printMap(BridgeGame bridgeGame) {
        StringBuilder output = new StringBuilder();

        output.append(printMapOneLine(bridgeGame, "U"));
        output.append(printMapOneLine(bridgeGame, "D"));

        System.out.println(output);
    }

    private StringBuilder printMapOneLine(BridgeGame bridgeGame, String upOrDown) {
        StringBuilder output = new StringBuilder();

        output.append(BRIDGE_PREFIX);
        output.append(printGameState(bridgeGame, upOrDown));
        output.append(BRIDGE_SUFFIX);
        output.append("\n");

        return output;
    }

    private StringBuilder printGameState(BridgeGame bridgeGame, String upOrDown) {
        UserPath userPath = bridgeGame.getUserPath();

        StringBuilder output = new StringBuilder();
        for (int idx = 0; idx < userPath.size(); idx++) {
            output.append(printOneSquare(bridgeGame, upOrDown, idx));

            if (idx != userPath.size() - 1)
                output.append(DELIMITER);
        }
        return output;
    }

    private String printOneSquare(BridgeGame bridgeGame, String upOrDown, int position) {
        Bridge bridge = bridgeGame.getBridge();
        UserPath userPath = bridgeGame.getUserPath();

        if (userPath.find(position).equals(upOrDown)) {
            return passOrNo(upOrDown, bridge, position);
        }

        return EMPTY;
    }

    private String passOrNo(String upOrDown, Bridge bridge, int position) {
        if (bridge.compareWithPosition(position, upOrDown)) {
            return PASS;
        }
        return NO_PASS;
    }

    ...
}

클래스 분리에 따른 예외 처리 로직, 상수,  에러 메시지 관리 위치

이번 미션에서도 사용자 입력 시 잘못된 값이 들어오면 예외 처리 로직을 실행해야 한다.

저번 주차 미션에서는 유효성 검사 로직과 상수는 각 도메인에서, 에러 메시지는 하나의 클래스에서 관리하였다. 3주차 미션 코드리뷰를 하면서, 여러가지 다양한 방법으로 유효성 검사 로직을 작성한 분들이 많았다. 나와 같은 방법을 쓰는 분도 있었고, validator 패키지를 생성하여 Validator 클래스를 여러 개 생성하여 유효성 검사 로직을 작성하는 분도 있었고, 상수나 에러 메시지도 validator 패키지나 util 패키지에서 관리하는 분도 있었다.

 

이번 미션에서는 저번 미션과 다른 방법을 사용해봤다. validator 패키지를 생성하여 각 입력에 대한 Validator 클래스를 생성하였고, 각 Validator 클래스에서 상수와 에러 메시지를 관리하였다.

 

다리(Bridge)를 생성할 때 유효성을 검증하는 Validator, 사용자의 입력(다리 길이, 이동할 칸, 재시작/종료 여부)을 검증하는 Validator

입력값에 대한 검증 로직은 다음과 같이 작성했다.

package bridge.validator;

public class InputBridgeSizeValidator {

    public static final int MINIMUM_BRIDGE_SIZE = 3;
    public static final int MAXIMUM_BRIDGE_SIZE = 20;

    private static final String BRIDGE_SIZE_ERROR_MESSAGE = "[ERROR] 다리 길이는 " + MINIMUM_BRIDGE_SIZE+ "부터 "+ MAXIMUM_BRIDGE_SIZE + " 사이의 숫자여야 합니다.";

    public static void validateBridgeSize(int size) {
        if (size < MINIMUM_BRIDGE_SIZE || size > MAXIMUM_BRIDGE_SIZE) {
            throw new IllegalArgumentException(BRIDGE_SIZE_ERROR_MESSAGE);
        }
    }
}
public class BridgeGameController {

    private final InputView inputView;
    private final OutputView outputView;
    private final BridgeMaker bridgeMaker;

    public BridgeGameController() {
        this.inputView = new InputView();
        this.outputView = new OutputView();
        this.bridgeMaker = new BridgeMaker(new BridgeRandomNumberGenerator());
    }

    public void startGame() {
        try {
            int bridgeSize = inputView.readBridgeSize();
            InputBridgeSizeValidator.validateBridgeSize(bridgeSize);

            BridgeGame bridgeGame = new BridgeGame(bridgeSize, bridgeMaker);
            playGame(bridgeGame);
        } catch (IllegalArgumentException e) {
            outputView.printErrorMessage(e.getMessage());
        }
    }
    
    ...
 }

BridgeGameController에서 InputView의 메서드를 호출하여 입력값을 받고, Validator 클래스의 메서드를 호출하고 입력값을 인자로 넘겨 입력에 대한 유효성 검사 로직을 실행했다.

 

도메인을 생성할 때 도메인에 대한 유효성 검증 로직은 다음과 같이 작성했다.

package bridge.validator;

import java.util.List;

public class BridgeFormatValidator {

    public static final String UP = "U";
    public static final String DOWN = "D";

    private static final String BRIDGE_FORMAT_ERROR_MESSAGE = "[ERROR] 다리는 위, 아래 두 칸만으로 이루어져 있습니다.";

    public static void validate(List<String> bridge) {
        if (bridge.stream()
                .anyMatch(element -> !element.equals(UP) && !element.equals(DOWN))) {
            throw new IllegalArgumentException(BRIDGE_FORMAT_ERROR_MESSAGE);
        }
    }
}
package bridge.domain;

import bridge.validator.BridgeFormatValidator;
import java.util.List;

public class Bridge {

    private final List<String> bridge;

    public Bridge(List<String> bridge) {
        BridgeFormatValidator.validate(bridge);
        this.bridge = bridge;
    }
    ...
}

Bridge의 생성자를 호출하여 다리를 생성할 때 BridgeFormatValidator 를 호출하여 다리가 위, 아래 두칸만으로 이루어져 있는지 검사한다,

 

validator 패키지를 생성하여 Validator 클래스를 사용하는 방법은 유효성 검사 로직이 하나의 패키지에 담겨져 있어 각 입력 또는 도메인에 대한 유효성 검사 로직을 찾는 것이 매우 쉽지만, 하나의 Validator 클래스에서 관리되는 상수가 다른 Validator 클래스나 도메인 클래스에서도 사용될 경우 Validator 클래스를 import를 해야하기 때문에 객체의 복잡도가 높아지고 코드가 지저분해지는 것 같다.

유효성 검사 로직과 상수, 에러 메시지 관리를 어떻게 할지는 앞으로도 고민이 많이 필요할 것 같다.

 

4주차 소감


이번 4주차는 저번 3주차 미션보다 구현을 더 잘 못했던 것 같다... 다리 건너기 게임도 처음에는 이해를 하지 못했는데, 오징어 게임에서 나온 다리 건너기 게임이라고 슬랙에서 얘기해서 그 때 게임을 이해했다. 아마 오징어 게임을 보지 못했다면 게임을 이해하는 데도 시간이 더 걸렸을 것 같다. 또 이번 주차는 여러가지 활동과 겹쳐서 시간이 부족해 요구사항을 구현하는 데에만 집중했고, 그래서 코드 리팩토링도 제대로 못하고 제출한 것 같다ㅠㅠ

 

그리고 추가된 프로그래밍 요구사항을 보면 이미 InputViewOutputViewBridgeGameBridgeMakerBridgeRandomNumberGenerator 클래스가 생성되어 있고, 구현할 메서드에 대해서도 각 클래스마다 있었는데, 이런 점이 오히려 더 헷갈리거나 구현하기 힘들게 했던 것 같다. 어떤 클래스는 패키지 변경이 가능하다고 나와있고, 어떤 클래스는 패키지 변경이 가능하다는 말이 아예 없어서 변경하지 않고 구현했는데, 이후 슬랙에서 패키지 변경을 한 다른 분들은 ApplicationTest에서 에러가 났다고 말씀하셨다.

기능 요구사항도 프로그래밍 요구사항도 내용이 정말 많아서 읽기 힘들 수도 있지만, 요구사항을 정말 꼼꼼이 읽어야한다. 아는 내용이거나 저번 주차 미션과 내용이 비슷하다고 요구사항을 대충 읽어서 잘못 구현할 수도 있는데, 이러한 실수를 방지하기 위해서도 요구사항을 정말 꼼꼼이 잘 읽어야 한다...ㅎㅎ

 

 

프리코스를 마무리하며


4주 동안 힘들었던 때도 있었지만 배운 것도 많고 스스로 성장했다는 것도 확실히 느낄 수 있는 한 달이었다. 객체 분리, 메서드 분리, 리팩토링, Git 커밋 메시지 컨벤션 등 프리코스뿐만 아니라 이후 개발 할 때에도 도움이 많이 되는 여러가지 것들을 배울 수 있었고, 프리코스를 진행하면서 내가 만약 우아한 테크코스에 합격한다면 좋은 개발자로서 성장을 할 수 있겠다는 확신을 가지게 되었다. 꼭 합격해서 다른 분들과 같이 열심히 배우고 좋은 개발자로 성장해나가고 싶다.

 

 

728x90
profile

Seren dev's blog

@Seren dev

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!