티스토리 뷰

 

4주 차를 보내며

 

처음 우테코 지원서를 작성할 때가 엊그제 같은데, 벌써 한 달이라는 시간이 흘러 프리코스의 마침표를 찍게 되었습니다. 한 달이라는 시간 동안 하루가 어떻게 지나가는지조차 모를 정도로 빠르게 느껴졌던 적이 없었던 것 같습니다. 마지막 4주 차 소감문에서는 좀 더 제한된 요구사항을 기반으로 객체지향적인 설계를 하며 겪었던 과정과 느낀 점을 담아보았습니다.

 

 

다리 건너기 객체 설계

 

다리 건너기 게임도 다른 과제들과 마찬가지로 그림을 그려가며 객체를 분리했습니다.

 

제가 설계한 구조에서 게임을 진행하는 핵심 객체들의 역할입니다.

  • User: 다리 건너기 게임에 참가하는 사람입니다. 초기 시작 위치는 0입니다.
  • BridgeMaker: 사용자가 원하는 크기의 다리를 생성합니다.
  • Bridge: 다리 하나는 3이상 20이하의 길이를 가질 수 있으며, 하나의 단계에서 가질 수 있는 방향은 U, D 중 하나 입니다.
  • BridgeState: 참가자가 한 번 움직일 때 마다 진행 상태를 기록합니다.
  • StepResult: 참가자가 한 번 움직일 때 마다, 해당 움직임의 결과를 생성합니다.

 

이번 주 과제는 다른 주 과제들과는 다르게 템플릿 클래스가 주어졌고 새로운 요구사항 '함수 하나에 10줄 이내로 작성'이 주어졌습니다. 처음 과제를 마주했을 때는 10줄 이내로 작성하는 것이 어렵지 않을 것이라고 생각했습니다. 막상 코드를 작성해 보니 10줄을 넘지 않도록 구현하기 쉽지 않았습니다. 특히 게임을 시작한 후 종료할 때까지 게임 과정을 판단하며 진행하는 메서드의 길이를 줄이는 것이 정말 쉽지 않았습니다.

public void start(BridgeGame bridgeGame) {
        do {
            boolean moveFlag = bridgeGame.move(inputView.readMoving());
            outputView.printMap(bridgeGame.getBridgeState());
            if (!moveFlag && !checkRetry(bridgeGame)) {
                break;
            }
        } while (bridgeGame.canContinue());
        outputView.printResult(bridgeGame);
    }

위 코드는 최종으로 작성한 게임을 진행하는 함수입니다. 한 가지 아쉬운 점이 건너는 것을 실패했을 때, 게임을 재시작하는 로직을 분리하지 못했습니다. If문에 AND로 조건이 NOT flag와 함께 묶여있는게 마음에 들지않아, 분리를 해보았는데 가독성이 떨어지는 것 같아 지금 상태로 유지했습니다. 추가적인 방법으로 do - while 구조로 한번 더 분리하는 방법도 있었지만, BridgeGame에 클래스 인스턴스를 추가로 작성해야되는 오버헤드가 있어서 선택하지 않았습니다.

 

또한 사용자가 잘못된 값을 입력할 경우 입력을 다시 받아야 하는 요구사항도 적용하기 쉽지 않았습니다. 이전 로또 게임을 진행하며 MVC 패턴에 적합하도록 View와 Model에서의 역할과 책임에 따른 예외 처리를 구분하여 설계하였는데, 이번 과제에서는 View 계층에서 모든 예외를 처리해야 한다는 생각이 들었습니다. 왜냐하면 입력을 받는 단계에서 예외가 발생하면 다시 입력을 받아야 했기 때문에, 객체의 인스턴스 타입 유형의 검증까지 처리를 하여 Controller에 넘겨줘야 한다고 생각했습니다. 그러므로 이번 과제에서는 View 계층에서 예외를 검증하는 방향으로 설계를 마쳤습니다.

public class InputValidator {

    ...


    public static void checkBridgeSize(String bridgeSize) throws IllegalArgumentException {
        if (isNotDigit(bridgeSize)) {
            throw new IllegalArgumentException(BRIDGE_LENGTH_FORMAT_ERROR_MESSAGE);
        }

        if (isWrongBridgeSize(Integer.valueOf(bridgeSize))) {
            throw new IllegalArgumentException(BRIDGE_LENGTH_RANGE_ERROR_MESSAGE);
        }
    }

    public static void checkMovingChoice(String movingChoice) throws IllegalArgumentException {
        if (isNotAlpha(movingChoice) || isWrongMoving(movingChoice)) {
            throw new IllegalArgumentException(DIRECTION_CHOICE_ERROR_MESSAGE);
        }
    }

    public static void checkRetryIntention(String retryIntention) throws IllegalArgumentException {
        if (isNotAlpha(retryIntention) || isWrongRetry(retryIntention)) {
            throw new IllegalArgumentException(GAME_RETRY_CHOICE_ERROR_MESSAGE);
        }
    }

    private static boolean isNotDigit(String inputDigit) {
        return !inputDigit.matches(DIGIT_REGEX);
    }

    private static boolean isWrongBridgeSize(int bridgeSize) {
        return (bridgeSize < Constant.BRIDGE_MIN_SIZE || bridgeSize > Constant.BRIDGE_MAX_SIZE);
    }

    private static boolean isNotAlpha(String inputAlpha) {
        return !inputAlpha.matches(ALPHA_REGEX);
    }

    ...

}

 

아래 코드는 로또 게임을 진행할 때 View 계층에서 작성한 검증 로직입니다.

로또 게임에서 설계한 View 계층에서는 숫자, 문자와 같은 타입만 검증했고, Model에서 실질적으로 인스턴스에 관련해 검증했습니다. 지난 로또 게임에 적용했던 방법이 잘못된 방법인지 의문이 들어 로또 게임과 다리 건너기 게임에서 구현한 검증 방법을 비교해 보니, 주어진 요구사항에 따라 적절한 방법으로 예외 처리를 진행하면 될 것 같다는 판단을 내리게 되었습니다. 

public class InputValidator {

    ...
    
    public static void checkMoney(String money) throws IllegalArgumentException {
        if (isNotDigit(money)) {
            throw new IllegalArgumentException("숫자만 입력할 수 있습니다.");
        }

        if (isOverMax(money)) {
            throw new IllegalArgumentException("입력할 수 있는 금액의 최대값을 넘었습니다.");
        }
    }

    public static void checkBonusNumber(String number) {
        if (isNotDigit(number)) {
            throw new IllegalArgumentException("숫자만 입력할 수 있습니다.");
        }
    }

    private static boolean isNotDigit(String number) {
        final String regex = "^[0-9]+$";
        return !number.matches(regex);
    }

    private static boolean isOverMax(String money) {
        return Long.parseLong(money) > MAX_INPUT_MONEY;
    }
}

 

마지막으로 지난 과제에 학습했던 enum을 U, D 방향에 따른 움직임 성공 유무로 케이스를 나눠 다리 상태를 출력하는 과정에 적용했습니다. 움직임의 성공 유무에 따른 결과 값과 출력문을 하나의 enum 타입의 객체로 정의해 사용했습니다. 그러므로 결과를 출력하는 단계에서 결과에 따른 출력을 구분하는 불필요한 if문 없이, 출력에만 집중할 수 있었습니다.

public enum StepResult {

    UPPER_SUCCESS("U", Constant.MOVE_SUCCESS, "O"),
    UPPER_FAIL("U", Constant.MOVE_FAIL, "X"),
    LOWER_SUCCESS("D", Constant.MOVE_SUCCESS, "O"),
    LOWER_FAIL("D", Constant.MOVE_FAIL, "X"),
    NO_RESULT("null", Constant.MOVE_FAIL, "null");

    private final String direction;
    private final boolean passed;
    private final String result;

    StepResult(String direction, boolean passed, String result) {
        this.direction = direction;
        this.passed = passed;
        this.result = result;
    }

    public static StepResult getStepResult(String moving, boolean moveFlag) {
        return Arrays.stream(values())
                .filter(stepResult -> stepResult.direction.equals(moving))
                .filter(stepResult -> stepResult.passed == moveFlag)
                .findAny()
                .orElse(NO_RESULT);
    }

    public String getDirection() {
        return direction;
    }

    public String getResult() {
        return this.result;
    }
}

우테코를 마치며

며칠 전, 자주 보는 헬스 유튜버 ‘권혁’이라는 분께서 이런 말씀을 해주셨습니다.

지금 뭔가 힘들 수도 있지만, 힘들면 잘 하고 있다는 반증의 가능성이 높다.
쉬운 길은 남들이 다 가려고 한다, 어려운 길을 가려고 하는 사람이 결국 성공한다.

프리코스를 진행하면서 잘 하고 있는 것인지 고민을 하며 힘들었던 시점에 이 두 가지 말이 정말 깊게 와닿았습니다. 아침에 눈을 뜨고 나서 밤에 잠에 들기 전까지 “지금 하고 있는 방법이 옳은 방법일까?” “다른 방법으로 개선할 수 있을까?”라는 생각이 머릿속에서 떠나지 않았습니다. 계속해서 같은 코드를 바라보고 분석하는 것이 지겹고 싫증이 날 때도 있었지만, 고통 없이 성장할 수 없다고 생각하기에 그럴 때마다 더 즐겨보자고 마음을 다 잡았습니다.

 

프리코스를 시작하기 전과 마친 후의 저를 돌아보면 스스로도 정말 많이 성장한 것 같습니다. 단순히 클래스를 이용해 개발하는 것이 객체지향 프로그래밍이라고 생각하고 있던 제가, 객체의 역할과 책임을 고려하게 되고, 객체에 메시지를 던지며 ‘진짜 객체지향 프로그래밍’을 바라보는 시야를 가지게 되었습니다. 짧은 기간 동안 성과라면 성과라고 할 수 있는 ‘성장’을 이끌어냈기 때문에, 이렇게 열정을 가지고 몰입한다면 못할 것이 없다는 스스로의 신념을 세울 수 있게 되었습니다.

댓글