과제
아래 네 단계를 수행하는 과제였다.
- 구매 금액에 따라 로또를 구매
- 로또 장수만큼 로또 번호를 생성
- 당첨번호 및 보너스 번호를 지정
- 수익이 얼마나 되는지 구현
로또는 왠지 익숙해서 (낙첨도 익숙^^...) 간단할거라고 생각했지만 오산이었다. 생각보다 구현 조건은 고려할 게 많았다. 로또 살때는 쉬웠는데 당첨 규칙이 이리 복잡할줄이야... 맨날 등수랑 돈만 봐서 몰랐다 ㅎ;
후기
구조
고려했던 것들
지난번 과제를 통해 배운 점들을 적용했다. 구조를 보다 빠르고 디테일하게 설계하려고 했다.
그리고 다른 분들과 코드리뷰를 하며 배운 점들을 적용했다. 테스트 코드나 Enum 사용, 적절한 변수명, controller 와 service 의 의존성 등 많은 것을 배울 수 있었다.
구조에 대한 고민
2주차 과제를 하며 구조에 대해 많이 고민하고 배웠다. 그래서 요번 3주차 과제에서는 적용해보는 연습을 했다.
- 먼저 일이 어떤 순서로 일어나는지에 대한 flow 를 생각한다.
- 무엇이 필요할지 정확하게는 모르지만, 일단은 두루뭉술하게 역할을 나눈다.
- 역할들이 해야 할 일과, 그를 위해 어떤 것을 주고받아야 하는지 생각한다. 이 과정에서 각 역할은 하는 일이 뚜렷해져 커플링이 적게 분리가 된다. 그리고 역할 간의 주고받을 값들을 구체적으로 정할 수 있다.
각 역할의 분리는 내공만큼 잘 나오는 것 같다. 2주차 과제보다 3주차 과제의 구조 설계가 더 빠르게 끝났고, 분리도 더 잘 되었다.
4주차 과제에서는 과연..! *_*
여담이지만 이런 설계 흐름이 어떤 책에 써있다는 글을 보게 되었다... *_* 기분이 좋아졌다~~~끼요홋
테스트 코드 작성
지난번 코드리뷰를 하며 테스트 코드를 상세히 짠 코드들을 봤었다. 그리고 테스트 코드 실행 시 변수를 넣어주는 등 다양한 방법도 알게 되었다. 그래서 이번엔 그 방법을 적용해보았다.
// CsvSource 로 변수를 넣어주는 방법
@DisplayName("적절한 금액이 아닌 경우")
@ParameterizedTest
@CsvSource({
"1500",
"2100",
"310j",
"-1000"
})
void testPriceValidate(String price) {
assertThatThrownBy(() -> buySystem.validatePrice(price))
.isInstanceOf(IllegalArgumentException.class);
}
// Stream 을 만들어서 넣어주는 방법
@DisplayName("갯수 및 컴마 구분 테스트")
@ParameterizedTest
@MethodSource("providedNumbersCount")
void testCheckWinningNumbersCount(String numbers) {
assertThatThrownBy(() -> Validations.checkWinningNumbersCount(numbers))
.isInstanceOf(IllegalArgumentException.class);
}
static Stream<Arguments> providedNumbersCount() {
return Stream.of(
Arguments.of("1,2,3,4,5,6,7,8"),
Arguments.of("123,4,5,6,7,8"),
Arguments.of("123_4,5,6,7,8")
);
}
몰랐을 때는 하나의 테스트 안에서 반복문으로 처리하거나, 값을 출력해서 비교하거나, 아니면 매번 테스트를 만들었었다.
앞으론 더 간결하게 할 수 있을 것 같다.
Controller 와 Model 간의 의존
이전에는 Controller 안에서 Model, View 를 선언해서 사용했다. 그러나 이렇게 하면 단위 테스트가 어려웠다. 왜냐하면 생성자에 특정 값을 넣어줘야 하기 때문이었다.
그래서 Controller 를 실행하는 Application 에서 Model, View 를 만들었다. 그리고 Controller 에서는 이들간에 setter 를 통해 값을 넘겨주도록 했다.
public class Application {
public static void main(String[] args) {
LottoBuySystem buySystem = new LottoBuySystem();
LottoDrawSystem drawSystem = new LottoDrawSystem();
LottoView view = new LottoView();
LottoController controller = new LottoController(buySystem, drawSystem, view);
controller.run();
}
}
Enum 사용
Enum 은 변하지 않을 값들을 관리하기에 적절하다고 한다. 그래서 텍스트 값만 있고, 변하지 않을 값인 에러 메시지를 Enum 으로 관리했다.
package lotto.utils;
import static lotto.utils.Constants.ERROR_PREFIX;
public enum LottoMessages {
WRONG_WINNING_NUMBERS(ERROR_PREFIX + "당첨번호 6개와 보너스 번호 1개를 입력해주세요. 번호는 숫자,로 이루어져있어야 합니다."),
WRONG_NUMBER(ERROR_PREFIX + "숫자를 입력해주세요."),
PRICE_ZERO(ERROR_PREFIX + "금액은 0이 될 수 없습니다."),
PRICE_DIVIDE_BY_LOTTO_PRICE(ERROR_PREFIX + "로또 금액으로 나누어져야 합니다."),
OUT_OF_LOTTO_NUMBER_BOUND(ERROR_PREFIX + "1 이상 45 이하의 숫자를 입력해야 합니다."),
DUPLICATE_NUMBER(ERROR_PREFIX + "중복된 숫자는 입력할 수 없습니다."),
DIVIDE_BY_ZERO(ERROR_PREFIX + "0으로 나눌 수 없습니다.")
;
private final String kr;
LottoMessages(String givenKr) {
this.kr = givenKr;
}
public String getKr() {
return this.kr;
}
}
간결한 코드에 대한 고민
이번에도 마찬가지로 함수는 각 함수는 15줄 이하, depth 는 최대 2까지 였다. 2주차 과제까지는 여차저차 되었는데 요번 과제는 간결하게 하려고 머리를 꽤나 싸맸었다.
왜냐하면 복잡한 당첨 로직과 입력 시 에러가 발생해도, 계속 입력을 해야하는 조건이 추가되었기 때문이다.
간결한 코드에 대한 고민 - 당첨 로직
당첨 로직은 얼핏 봤을 때 패턴화를 하기가 어려웠다. 2등 당첨조건과 보너스볼의 존재 때문이었다.
- 1등: 6개 번호 일치 / 2,000,000,000원
- 2등: 5개 번호 + 보너스 번호 일치 / 30,000,000원 # 이녀석!!!!
- 3등: 5개 번호 일치 / 1,500,000원
- 4등: 4개 번호 일치 / 50,000원
- 5등: 3개 번호 일치 / 5,000원
알고있어야 하는 정보도 많이 있었다. 맞춰야 하는 공의 갯수, 보너스 공 당첨 여부, 당첨금, 그에 따른 메시지가 그것들이다. 무엇보다 2등의 당첨 조건과 보너스볼의 존재로 인해 간결한 분리가 쉽지 않아보였다.
단순 분기문으로는 로직을 구현할 수가 없었다. 왜냐하면 문제에 명시된 간결한 코드 규칙 이 있기 때문이었다. 분기문을 쓰자니 else 가 들어가고, 그렇다고 else 를 쓰지 않자고 return 을 쓰니 15줄을 넘어버린다.
이에 꽤 오랜 고민을 했고, 알아야 하는 값들을 분리했다. 등수, 당첨 번호의 갯수, 상금, 메시지 로 분리하였다.
분리하고 나니 어떤 값들이 이들 값들을 결정하는 인자인지 알게 되었다. 그렇게 배열 로 관리하기로 했다.
- 등수(k) 는 당첨번호의 갯수(x), 보너스번호 당첨 여부(y) 로 결정이 된다. 그래서 2차원 배열로 만들었다.
- 당첨 번호의 갯수(k) 는 (일반 번호 기준) 등수(x) 로 결정이 된다. 1차원 배열로 만들었다.
- 상금(k) 는 등수(x) 로 결정이 된다. 1차원 배열로 만들었다.
- 메시지(k) 도 등수(x) 로 결정이 된다. 1차원 배열로 만들었다.
package lotto.model;
import lotto.utils.Utils;
import static lotto.utils.Constants.LOTTO_DRAW_NUMBER_COUNT;
public class Prize {
public int[][] GRADE_BOARD;
public int[] MONEY_BOARD;
public int[] MATCH_COUNT_BOARD;
public String[] MESSAGE_BOARD;
public int GRADE_COUNT = 5;
public int FIRST_SAME_COUNT = 6;
public int SECOND_SAME_COUNT = 5;
public int THIRD_SAME_COUNT = 5;
public int FOURTH_SAME_COUNT = 4;
public int FIFTH_SAME_COUNT = 3;
public int BONUS_BALL_SAME_COUNT = 1;
public int FIRST_MONEY = 2_000_000_000;
public int SECOND_MONEY = 30_000_000;
public int THIRD_MONEY = 1_500_000;
public int FOURTH_MONEY = 50_000;
public int FIFTH_MONEY = 5_000;
public Prize() {
this.setGradeBoard();
this.setMoneyBoard();
this.setMatchCountBoard();
this.setMessageBoard();
}
private int getBonusSameCount(boolean bonusSame) {
if(bonusSame) {
return 1;
}
return 0;
}
public int getGrade(int sameNumbersCount, boolean bonusSame) {
int bonusSameCount = getBonusSameCount(bonusSame);
return this.GRADE_BOARD[sameNumbersCount][bonusSameCount];
}
public int getMoney(int grade) {
return this.MONEY_BOARD[grade];
}
public String getMessage(int grade) {
return MESSAGE_BOARD[grade];
}
public void setGradeBoard() {
int rows = FIRST_SAME_COUNT + 1;
int cols = BONUS_BALL_SAME_COUNT + 1;
GRADE_BOARD = new int[rows][cols];
GRADE_BOARD[FIRST_SAME_COUNT][0]
= GRADE_BOARD[FIRST_SAME_COUNT-1][BONUS_BALL_SAME_COUNT] = 1;
GRADE_BOARD[SECOND_SAME_COUNT][BONUS_BALL_SAME_COUNT] = 2;
GRADE_BOARD[THIRD_SAME_COUNT][0]
= GRADE_BOARD[THIRD_SAME_COUNT-1][BONUS_BALL_SAME_COUNT] = 3;
GRADE_BOARD[FOURTH_SAME_COUNT][0]
= GRADE_BOARD[FOURTH_SAME_COUNT-1][BONUS_BALL_SAME_COUNT] = 4;
GRADE_BOARD[FIFTH_SAME_COUNT][0]
= GRADE_BOARD[FIFTH_SAME_COUNT-1][BONUS_BALL_SAME_COUNT] = 5;
}
public void setMoneyBoard() {
MONEY_BOARD = new int[GRADE_COUNT + 1];
MONEY_BOARD[1] = FIRST_MONEY;
MONEY_BOARD[2] = SECOND_MONEY;
MONEY_BOARD[3] = THIRD_MONEY;
MONEY_BOARD[4] = FOURTH_MONEY;
MONEY_BOARD[5] = FIFTH_MONEY;
}
public void setMatchCountBoard() {
MATCH_COUNT_BOARD = new int[LOTTO_DRAW_NUMBER_COUNT+1];
MATCH_COUNT_BOARD[1] = FIRST_SAME_COUNT;
MATCH_COUNT_BOARD[2] = SECOND_SAME_COUNT;
MATCH_COUNT_BOARD[3] = THIRD_SAME_COUNT;
MATCH_COUNT_BOARD[4] = FOURTH_SAME_COUNT;
MATCH_COUNT_BOARD[5] = FIFTH_SAME_COUNT;
}
public void setMessageBoard() {
MESSAGE_BOARD = new String[GRADE_COUNT + 1];
MESSAGE_BOARD[1] =
MATCH_COUNT_BOARD[1] + "개 일치 (" + Utils.getFormattedMoney(MONEY_BOARD[1]) + "원)";
MESSAGE_BOARD[2] =
MATCH_COUNT_BOARD[2] + "개 일치, 보너스 볼 일치 (" + Utils.getFormattedMoney(MONEY_BOARD[2]) + "원)";
MESSAGE_BOARD[3] =
MATCH_COUNT_BOARD[3] + "개 일치 (" + Utils.getFormattedMoney(MONEY_BOARD[3]) + "원)";
MESSAGE_BOARD[4] =
MATCH_COUNT_BOARD[4] + "개 일치 (" + Utils.getFormattedMoney(MONEY_BOARD[4]) + "원)";
MESSAGE_BOARD[5] =
MATCH_COUNT_BOARD[5] + "개 일치 (" + Utils.getFormattedMoney(MONEY_BOARD[5]) + "원)";
}
}
작성할 땐 최선을 다했다~ 였는데 지금보니 빈 곳이 많다 ㅎ... 상수화 할 곳도 보이고 조금더 줄일 곳도 보이고 ^^...... 함수 호출 중복도 보이고 ^^........................ 하 다음엔 더 좋은 코드로 찾아뵙겠습니다......
간결한 코드에 대한 고민 - 입력 로직
이번 과제에선 새로운 입력 조건이 추가되었다. 바로 입력 시 에러가 발생해도, 해당하는 에러 문구를 출력하고 계속 입력을 받는 것이었다.
- 입력을 받는다.
- IllegalArgumentException 에러가 발생한다.
- 다시 1번으로 돌아가서 입력을 받는다.
- 잘 입력을 받았으면 입력 값을 반환한다.
즉 에러 발생 여부 와 입력 값 두 가지만 분리하면 공통 로직인 것이다. 그래서 이 값을 DTO 로 만들고, controller 에서 입력 시에 DTO 를 보고 재입력 여부를 결정했다.
package lotto.dto;
public class PriceEnter {
String price;
boolean inputValid;
public PriceEnter(String price, boolean inputValid) {
this.price = price;
this.inputValid = inputValid;
}
public String getPrice() {
return price;
}
public boolean isInputValid() {
return inputValid;
}
public void setPrice(String price) {
this.price = price;
}
public void setInputValid(boolean inputValid) {
this.inputValid = inputValid;
}
}
package lotto.controller;
public class LottoController {
// ...
private String enterPrice() {
String price = "";
boolean validInput = false;
PriceEnter dto = new PriceEnter(price, validInput);
while (!dto.isInputValid()) {
_enterPrice(dto);
}
return dto.getPrice();
}
private void _enterPrice(PriceEnter dto) {
this.view.printEnterLottosCount();
try {
String price = readLine();
dto.setPrice(price);
lottoBuySystem.validatePrice(dto.getPrice());
dto.setInputValid(true);
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
}
}
'우아한테크코스 > 프리코스' 카테고리의 다른 글
2023 우아한테크코스 6기 프리코스 후기 (0) | 2023.11.15 |
---|---|
우아한테크코스 프리코스 2주차 과제 후기 - 자동차 경주 (2) | 2023.11.01 |