Skip to content

우아한테크코스 프리코스 3주차 회고

후기

우아한테크코스 3주차 프리코스가 끝났다. 3주차 미션은 로또 미션이었다. 로또 프로그램을 제작하는 미션이었는데, 기능 흐름은 아래와 같았다.

1. 구입할 로또 금액을 입력받는다.
2. 로또 금액에 따라 로또가 생성된다. (로또 가격이 1000원이고, 구입 금액이 15000원이면 15개 생성)
3. 로또 당첨번호를 입력한다.
4. 보너스 번호를 입력한다.
5. 각 등수별 당첨 개수와 수익율이 출력되고 종료된다.

5단계의 흐름속에서 고려해야할 대상이 많다는 것을 깨달았고, 생각보다 쉽지 않았다.

고민했던 내용

enum 도대체 왜 사용하는건가?

2주차 미션을 수행하면서 많은 참여자들이 enum을 이용하여 에러에 대한 상수나, 출력할 상수를 한번에 관리하는 것을 아래의 코드처럼 사용 가능하다는 것을 알게 되었다. 필자도 오류메시지와 출력할 메시지를 각각의 enum을 정의하여 사용하였다.

LottoCommonString
package lotto.common;

public enum LottoCommonString {
    INPUT_LOTTO_BUY_NUMBER("구입금액을 입력해 주세요."),
    FORMAT_LOTTO_BUY_SUCCESS("%d개를 구매했습니다."),
    INPUT_WINNER_LOTTO_NUMBER("당첨 번호를 입력해 주세요."),
    INPUT_BONUS_NUMBER("보너스 번호를 입력해 주세요."),
    LOTTO_WINNER_STATUS("당첨 통계"),
    LOTTO_RESULT_LINE("---"),
    FORMAT_WINNER_TABLE_RESULT("%s (%s원) - %d개"),
    FORMAT_PROFIT_RATE("총 수익률은 %.1f%%입니다."),
    LOTTO_INPUT_DELIMITER(",");

    private final String message;

    private LottoCommonString(String message) {
        this.message = message;

    }

    public String getMessage() {
        return message;
    }
}

이렇게 쓰다보니 다음과 같은 장점이 생겨서 코드를 짜기 편했다.

  1. 한 번에 상수값을 관리하여 사용하기 편리하였다.

  2. 파라미터를 Enum으로 받게 되면 입력값이 제한되어 검증하는 코드를 작성하지 않아서 좋았다.

  3. 같은 성격의 자료를 한 번에 묶어서 관리하기가 편했다.

하지만 Enum의 성격을 모른체 사용하여 나쁜 경우도 있었다. 이런식으로 코드를 작성하면 컴파일오류가 아니라 ExceptionInInitializerError 가 나타나여 주의해야 한다.

LottoErrorMessage.java
public enum LottoErrorMessage {
    LOTTO_NUMBER_OUT_OF_RANGE(String.format("로또 번호는 1부터 45 사이의 숫자여야 합니다.")), // 사용하려 할 때 예외처리가 나타난다.
    // 중략
    private String message;

    private LottoErrorMessage(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

3주차가 끝나고 회고하였을 때 위와 같이 사용하는 것 보다 아래와 같이 같은 포맷끼리 묶어서 관리하는 것이 좋지 않을까 싶다.

막상 미션이 끝나고서야 이런생각이 떠오르는지 모르겠다 😓 4주차에는 한 번 적용해 볼 예정이다.

LottoInformation.java
public class LottoNumberRange { 
    public static final int LOTTO_MIN_RANGE = 1;
    public static final int LOTTO_MAX_RANGE = 45;
}

public enum LottoNumberRangeError {
    LOTTO_NUMBER_OUT_OF_RANGE("로또 번호는 %d부터 %d 사이의 숫자여야 합니다."),
    WRONG_LOTTO_NUMBER("잘못된 로또 번호를 입력하셨습니다. %d 부터 %d 숫자를 입력해주세요.");
    private String message;

    private LottoNumberRangeError(String message) {
        this.message = message;
    }

    public String getMessage() {
        return String.format(message,LottoNumberRange.LOTTO_MIN_RANGE
                                    ,LottoNumberRange.LOTTO_MAX_RANGE);
    }
}

// 사용
jshell> LottoNumberRangeError.WRONG_LOTTO_NUMBER.getMessage();
$3 ==> "잘못된 로또 번호를 입력하셨습니다. 1 부터 45 숫자를 입력해주세요."

테스트 케이스 메소드명은 어떻게 작성해야 할까.

2주차와 다르게 3주차 부터는 성공 실패 대신에 Given_When_Then 을 적용해보았다. 이렇게 코드를 짜보니, 테스트 코드가 어떤 것을 테스트하는지 눈에 띄게 되었고, 요구사항에 맞춰서 자연스럽게 테스트되어 한눈에 요구사항을 충족하는지 확인할 수 있었다.

함수 이름으로 코드를 짜다보니, 생성하기가 힘들었다. 다음엔 DisplayName 어노테이션을 활용하여 무엇을 테스트하는지 설명을 추가하는게 좋을 것 같다.

테스트 코드들
@Test
void 입력받은_당첨번호로_로또번호를_생성가능해야_함() {
    // given
    List<Integer> givenNumbers = List.of(1, 2, 3, 4, 5, 6);
    String givenStringNumber = "1,2,3,4,5,6";
    // when
    Lotto lotto = Lotto.fromString(givenStringNumber);
    List<Integer> numbers = lotto.getNumbers();

    // then
    assertThat(numbers).isEqualTo(givenNumbers);
}

@Test
void 입력값이_숫자가_아니면_예외발생_후_오류_식별이_가능한_메시지_발생() {
    // given
    String givenMoney = "1234thereisnotNumeric";

    // when
    assertThatThrownBy(() -> ValidatorLottoBuy.validateString(givenMoney))
            // then
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessageContaining(ERROR_STRING)
            .hasMessageContaining(LottoErrorMessage.WRONG_LOTTO_PRICE.getMessage());
}

예외가 발생할 경우 발생한 지점부터 다시 처리하기

예외가 발생한 부분 즉, 입력값 검증이 실패한 지점부터 어떻게 재반복할지 고민하였다. 처음엔 while 문으로 아래와 같이 검토하면 되겠다고 생각했다.

ExecuteUntilSuccess.java
while(untilSuccess){
    result = null;
    try{
        result = 입력값_검증();
        untilSuccess = false; // 코드 빠저나가기
    }catch(예외발생 e){
        예외_출력
    }
    return result;
}

함수의 줄이 많이 차지하게 되어 함수를 매개변수로 받을 수 있는 방법에 대해 연구해보았는데 interface가 그 역할을 수행한다고 하여 아래와 같이 코드를 작성하여 함수를 줄일 수 있게 되었다.

ExceuteUntilSuccess.java
public class ExecuteUntilSuccess {
    private static <R> R executeWithRetry(OutputFunction output,
            Supplier<R> executionLogic) {
        R result = null;
        boolean executeUntilSuccess = true;
        while (executeUntilSuccess) {
            try {
                output.execute();
                result = executionLogic.get();
                executeUntilSuccess = false;
            } catch (IllegalArgumentException e) {
                OutputView.printErrorMessage(e);
            }
        }
        OutputView.printNewLine();
        return result;
    }

    public static <T, R> R execute(OutputFunction output, InputFunction<T> input, FactoryFunction<R, T> validator) {
        return executeWithRetry(output, () -> validator.execute(input.execute()));
    }
}

//호출
private Lotto getLottoWinnerFromPrompt() {
    return ExecuteUntilSuccess.execute(
            OutputView::printInputWinnerLottoNumber,
            InputView::getBuyingLottoNumber,
            Lotto::fromString);
}

달라진 점

코드를 돌이켜보면서 if문이 검증하는 곳 빼고 현저히 줄어들었음을 눈에 띄게 보였다. 그리고 함수를 나누다 보니 (필자가 다시 코드를 봤을때는) 가독성이 좋아졌다.

잘하고 있고, 앞으로도 잘할 것이다. 화이팅!


Comments