Github – 로또 프로젝트
학습 목표
- 관련 함수를 묶어 클래스를 만들고, 객체들이 협력하여 하나의 큰 기능을 수행하도록 한다.
- 클래스와 함수에 대한 단위 테스트를 통해 의도한 대로 정확하게 작동하는 영역을 확보한다.
- [[우테코 7기 프리코스] 2주차 피드백](https://velog.io/@grayson1999/%EC%9A%B0%ED%85%8C%EC%BD%94-7%EA%B8%B0-%ED%94%84%EB%A6%AC%EC%BD%94%EC%8A%A4-2%EC%A3%BC%EC%B0%A8-%ED%94%BC%EB%93%9C%EB%B0%B1)을 최대한 반영한다.
미션 설명
해당 미션은 로또 발매기를 코드로 구현하는 것이다. 사용자는 구입할 로또의 금액을 입력하고, 이에 따라 로또 번호가 발행된다. 로또 번호는 1부터 45까지의 범위에서 중복되지 않는 6개의 숫자로 구성된다. 또한, 당첨 번호와 보너스 번호를 입력받아 로또의 당첨 내역을 확인할 수 있다. 사용자가 입력한 구입 금액이 1,000원 단위로 나누어 떨어지지 않을 경우, 프로그램은 예외를 발생시켜 재입력을 요구한다.
게임이 끝난 후, 로또의 당첨 통계를 출력하며, 각 등수에 따른 당첨 내역을 확인할 수 있다. 1등부터 5등까지의 당첨 기준과 금액이 정해져 있으며, 사용자가 구매한 로또 번호와 당첨 번호를 비교하여 수익률을 계산하여 출력한다. 만약 잘못된 값이 입력될 경우에는 IllegalArgumentException이 발생하여 애플리케이션이 종료된다.
입출력 예
구입금액을 입력해 주세요.
8000
8개를 구매했습니다.
[8, 21, 23, 41, 42, 43]
[3, 5, 11, 16, 32, 38]
[7, 11, 16, 35, 36, 44]
[1, 8, 11, 31, 41, 42]
[13, 14, 16, 38, 42, 45]
[7, 11, 30, 40, 42, 43]
[2, 13, 22, 32, 38, 45]
[1, 3, 5, 14, 22, 45]
당첨 번호를 입력해 주세요.
1,2,3,4,5,6
보너스 번호를 입력해 주세요.
7
당첨 통계
---
3개 일치 (5,000원) - 1개
4개 일치 (50,000원) - 0개
5개 일치 (1,500,000원) - 0개
5개 일치, 보너스 볼 일치 (30,000,000원) - 0개
6개 일치 (2,000,000,000원) - 0개
총 수익률은 62.5%입니다.
아키텍처
├─main
│ └─java
│ └─lotto
│ Application.java
│ ErrorMessages.java
│ Lotto.java
│ LottoController.java
│ LottoPurchase.java
│ WinningLotto.java
│
└─test
└─java
└─lotto
ApplicationTest.java
LottoControllerTest.java
LottoPurchaseTest.java
LottoTest.java
WinningLottoTest.java
요구 사항 분석
과제 진행 요구 사항
- 기능을 구현하기 전 README.md에 구현할 기능 목록을 정리한다.
- Git의 커밋 단위는 앞 단계에서 README.md에 정리한 기능 목록 단위로 추가한다.
프로그래밍 요구 사항
- JDK 21 버전을 사용한다.
build.gradle파일 변경은 불가하다.- 제공된 라이브러리 이외의 외부 라이브러리 사용은 금지된다.
기능 요구 사항
- 로또 번호의 숫자 범위는 1~45이다.
- 1개의 로또를 발행할 때 중복되지 않는 6개의 숫자를 뽑는다.
- 당첨 번호 추첨 시 중복되지 않는 숫자 6개와 보너스 번호 1개를 뽑는다.
- 당첨 기준과 금액은 아래와 같다:
시퀀스
- 로또 구매:
- 사용자가 구입할 금액을 입력한다.
- 구입 금액에 대한 검증 로직이 포함된다.
- 로또 발행:
- 로또 1장의 가격은 1,000원이며, 입력한 금액을 1,000원으로 나눈 만큼의 로또를 발행한다.
- 당첨 번호 추첨:
- 중복되지 않는 6개의 당첨 번호와 1개의 보너스 번호를 입력받는다.
- 당첨 내역 확인:
- 사용자가 구매한 로또 번호와 당첨 번호를 비교하여 각 등수에 따른 당첨 여부를 확인한다.
- 당첨 통계 및 수익률을 계산하여 출력한다.
기능 목록
로또
- 로또 번호
- 정상 로또 확인
당첨 로또
- 로또 번호
- 보너스 번호
- 정상 로또 확인
로또 구입
- 구입 금액 입력
- 당첨 로또 번호 입력
- 보너스 번호 입력
- 구입 금액 입력 검증
로또 컨트롤러
- 로또 발행
- 로또 결과 확인
로또 클래스 구현
Lotto 클래스는 로또 번호를 관리하고 유효성을 검증하는 역할을 담당한다.
package lotto;
import java.util.HashSet;
import java.util.List;
import static lotto.ErrorMessages.*;
public class Lotto {
private final List<Integer> numbers;
public Lotto(List<Integer> numbers) {
if (numbers == null) {
throw new IllegalArgumentException(ERROR_LOTTO
_NUMBER_NULL);
}
validate(numbers);
numbers = sortNumber(numbers);
this.numbers = numbers;
}
private void validate(List<Integer> numbers) {
if (numbers.isEmpty()) {
throw new IllegalArgumentException(ERROR_LOTTO_NUMBER_EMPTY);
}
if (numbers.size() != 6) {
throw new IllegalArgumentException(ERROR_LOTTO_NUMBER_SIZE);
}
if (!validateRange(numbers)) {
throw new IllegalArgumentException(ERROR_LOTTO_NUMBER_RANGE);
}
if (!validateDuplicate(numbers)) {
throw new IllegalArgumentException(ERROR_LOTTO_NUMBER_DUPLICATE);
}
}
public List<Integer> getNumbers() {
return numbers;
}
boolean validateRange(List<Integer> numbers) {
for (int number : numbers) {
if (number < 1 || number > 45) {
return false;
}
}
return true;
}
boolean validateDuplicate(List<Integer> numbers) {
return numbers.size() == new HashSet<>(numbers).size();
}
List<Integer> sortNumber(List<Integer> numbers) {
return numbers.stream().sorted().toList();
}
public void printLotto() {
System.out.println(numbers.toString());
}
}
당첨 로또 클래스
WinningLotto 클래스는 기본 Lotto 클래스를 상속받아 당첨 번호와 보너스 번호를 관리하는 역할을 한다.
package lotto;
import java.util.List;
import static lotto.ErrorMessages.ERROR_BONUS_NUMBER_DUPLICATE;
import static lotto.ErrorMessages.ERROR_LOTTO_NUMBER_RANGE;
public class WinningLotto extends Lotto {
private final int bonusNumber;
public WinningLotto(List<Integer> numbers, int bonusNumber) {
super(numbers);
validateBonusNumber(numbers, bonusNumber);
this.bonusNumber = bonusNumber;
}
private void validateBonusNumber(List<Integer> numbers, int bonusNumber) {
if (numbers.contains(bonusNumber)) {
throw new IllegalArgumentException(ERROR_BONUS_NUMBER_DUPLICATE);
}
if (bonusNumber < 1 || bonusNumber > 45) {
throw new IllegalArgumentException(ERROR_LOTTO_NUMBER_RANGE);
}
}
public int getBonusNumber() {
return bonusNumber;
}
}
로또 구입 클래스
package lotto;
import camp.nextstep.edu.missionutils.Console;
import java.util.Arrays;
import java.util.List;
import static lotto.ErrorMessages.*;
public class LottoPurchase {
private int purchaseAmount;
private int purchaseCount;
private WinningLotto winningLotto;
public void inputAmount() {
System.out.println("구입금액을 입력해주세요");
String userInputAmount = Console.readLine();
inputAmount(userInputAmount);
}
public void inputAmount(String userInputAmount) {
if (!userInputAmount.matches("\\d+")) {
throw new IllegalArgumentException(ERROR_NOT_NUMBER);
}
int inputAmount = Integer.parseInt(userInputAmount);
validateInputAmount(inputAmount);
purchaseAmount = inputAmount;
purchaseCount = inputAmount / 1000;
}
private void validateInputAmount(int inputAmount) {
if (inputAmount % 1000 != 0) {
throw new IllegalArgumentException(ERROR_NOT_1000_MULTIPLE);
}
if (inputAmount < 1000) {
throw new IllegalArgumentException(ERROR_MIN_PURCHASE_AMOUNT);
}
}
public void inputLottoNumber() {
System.out.println("당첨 번호를 입력해 주세요.");
String userInputLottoNumber = Console.readLine();
System.out.println("보너스 번호를 입력해 주세요.");
String userInputBonusNumber = Console.readLine();
inputLottoNumber(userInputLottoNumber, userInputBonusNumber);
}
public void inputLottoNumber(String userInputLottoNumber, String userInputBonusNumber) {
if (!userInputLottoNumber.matches("^\\d+(?:,\\d+)*quot;)) {
throw new IllegalArgumentException(ERROR_INVALID_LOTTO_FORMAT);
}
if (!userInputBonusNumber.matches("\\d+")) {
throw new IllegalArgumentException(ERROR_INVALID_BONUS_NUMBER);
}
winningLotto = new WinningLotto(splitLottoNumber(userInputLottoNumber), Integer.parseInt(userInputBonusNumber));
}
private List<Integer> splitLottoNumber(String userInputLottoNumber) {
return Arrays.stream(userInputLottoNumber.split(",")).map(Integer::parseInt).toList();
}
public int getPurchaseAmount() {
return purchaseAmount;
}
public WinningLotto getWinningLotto() {
return winningLotto;
}
public int getPurchaseCount() {
return purchaseCount;
}
}
로또 컨트롤러
LottoController 클래스는 로또 게임의 주요 로직을 처리하는 클래스이다.
package lotto;
import camp.nextstep.edu.missionutils.Randoms;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import static java.lang.Math.round;
public class LottoController {
private List<Lotto> lottos;
private int purchaseCount;
private WinningLotto winningLotto;
private LottoPurchase lottoPurchase;
private Map<Integer, Integer> winningList;
private double winningRate;
public LottoController() {
lottos = new ArrayList<>();
lottoPurchase = new LottoPurchase();
winningList = new HashMap<>();
initialWinningList();
}
public LottoController(String Test) {
lottos = new ArrayList<>();
initialWinningList();
}
public void getStart() {
while (true) {
try {
lottoPurchase.inputAmount();
issueLotto(lottoPurchase.getPurchaseCount());
printIssuedLottos();
break;
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
}
while (true) {
try {
lottoPurchase.inputLottoNumber();
updateWinningList(lottoPurchase.getWinningLotto());
printWinningReport();
break;
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
}
calculateWinningRate();
printWinningRage();
}
public void issueLotto(int purchaseCount) {
this.purchaseCount = purchaseCount;
for (int i = 0; i < purchaseCount; i++) {
List<Integer> numbers = Randoms.pickUniqueNumbersInRange(1, 45, 6);
lottos.add(new Lotto(numbers));
}
}
public void printIssuedLottos() {
System.out.printf("%d개를 구매했습니다.\n", purchaseCount);
for (Lotto lotto : lottos) {
lotto.printLotto();
}
}
private void initialWinningList() {
for (int i = 0; i < 6; i++) {
winningList.put(i, 0);
}
}
public void updateWinningList(WinningLotto winningLotto) {
this.winningLotto = winningLotto;
for (Lotto lotto : lottos) {
int lottoRank = getRanking(lotto);
winningList.put(lottoRank, winningList.getOrDefault(lottoRank, 0) + 1);
}
}
public int getMatchCount(Lotto lotto) {
int matchCount = 0;
for (Integer number : lotto.getNumbers()) {
if (winningLotto.getNumbers().contains(number)) {
matchCount++;
}
}
return matchCount;
}
public int getRanking(Lotto lotto) {
int matchCount = getMatchCount(lotto);
return findRanking(lotto, matchCount);
}
public int findRanking(Lotto lotto, int matchCount) {
if (matchCount == 6) {
return 1; // 1등
} else if (matchCount == 5) {
if (lotto.getNumbers().contains(winningLotto.getBonusNumber())) {
return 2; // 2등
}
return 3; // 3등
} else if (matchCount == 4) {
return 4; // 4등
} else if (matchCount == 3) {
return 5; // 5등
}
return 0; // 미당첨
}
public void printWinningReport() {
System.out.println("당첨 통계");
System.out.println("---");
List<Integer> winningListKeys = winningList.keySet().stream().sorted(Comparator.reverseOrder()).toList();
for (int key : winningListKeys) {
if (key == 5) {
System.out.printf("3개 일치 (5,000원) - %d개\n", winningList.get(key));
} else if (key == 4) {
System.out.printf("4개 일치 (50,000원) - %d개\n", winningList.get(key));
} else if (key == 3) {
System.out.printf("5개 일치 (1,500,000원) - %d개\n", winningList.get(key));
} else if (key == 2) {
System.out.printf("5개 일치, 보너스 볼 일치 (30,000,000원) - %d개\n", winningList.get(key));
} else if (key == 1) {
System.out.printf("6개 일치 (
2,000,000,000원) - %d개\n", winningList.get(key));
}
}
}
public double calculateWinningRate() {
double totalWinningPrice = 0;
for (int key : winningList.keySet()) {
if (key == 1) {
totalWinningPrice += 2000000000 * winningList.get(key);
} else if (key == 2) {
totalWinningPrice += 30000000 * winningList.get(key);
} else if (key == 3) {
totalWinningPrice += 1500000 * winningList.get(key);
} else if (key == 4) {
totalWinningPrice += 50000 * winningList.get(key);
} else if (key == 5) {
totalWinningPrice += 5000 * winningList.get(key);
}
}
int totalPurchaseAmount = purchaseCount * 1000;
if (totalPurchaseAmount == 0) {
return 0;
}
double rate = Math.round(totalWinningPrice / totalPurchaseAmount * 1000) / 10.0;
this.winningRate = rate;
return this.winningRate;
}
public void printWinningRage() {
System.out.println("총 수익률은 " + String.format("%.1f", winningRate) + "%입니다."); // 소수점 한 자리까지 포맷팅
}
public List<Lotto> getLottos() {
return lottos;
}
public Map<Integer, Integer> getWinningList() {
return winningList;
}
}
회고
이번 프로젝트는 졸업 작품 전과 발표 준비가 겹쳐 시간이 많이 부족했다. 구현할 시간이 하루밖에 없었고, 학습 목표에 따라 작은 단위에 대한 테스트를 먼저 작성한 후 개발하는 방법을 사용했다. 이 과정에서 작은 단위에 대한 테스트를 우선 작성하여 의도한 대로 정확하게 작동하는 영역을 확보하는 것이 중요하다는 점과 기능 목록을 먼저 작성하는 것이 왜 필요한지를 깨달았다.
프로젝트 초기에 사용자가 로또를 구매한 금액에 따라 입력을 받고, 당첨 번호를 랜덤 함수에서 추출하여 결과를 비교하는 방식으로 이해했다. 그러나 이는 잘못된 이해였다. 실제로는 사용자가 입력한 금액에 맞춰 로또를 랜덤으로 발행하고, 사용자가 당첨 번호를 입력해야 했다.
이런 혼란 속에서 몇 분간 뇌정지가 왔다. 그 후, 내가 구현한 클래스를 점검하면서 로또 클래스와 보너스 번호를 포함한 발행 로또 클래스를 구현했음을 확인하고, 로또 구입 클래스를 추가했다. 만약 클래스를 분리하지 않았다면 처음부터 다시 작성해야 했을 것이다. 그러나 클래스를 분리해두었기 때문에 한눈에 파악할 수 있었고, 클래스와 메서드 이름을 변경하는 정도로 실수를 해결할 수 있었다. 이번 경험을 통해 클래스화를 해야 하는 이유와 메서드를 작은 단위로 설정해야 하는 이유, 그리고 기능 목록을 먼저 작성해야 하는 이유를 알게 되었다.
기존에 작성한 테스트에서 변경된 기능에 대한 테스트를 수정한 후, 테스트가 문제 없이 통과하는 것을 바로 확인할 수 있었다. 의도하지 않았지만, 오늘의 주제를 한 번에 파악할 수 있는 시간이 되었다.