<우아한 테크코스 5기 프리코스 진행사항 - 3주차>
3주차 과제도 2주차 과제와 마찬가지로 하나의 주제를 가지고 기능별로 구현을 하는 알고리즘 문제였다.
요구사항은 크게 3가지로 나뉘어져있었다.
1) 기능 요구 사항
2) 프로그래밍 요구 사항
3) 과제 진행 요구 사항
<백엔드 - Java> 기준으로 과제를 수행하였다.
<로또>
- 기능 요구 사항
- 로또 게임 기능을 구현해야 한다. 로또 게임은 아래와 같은 규칙으로 진행된다.
- 로또 번호의 숫자 범위는 1~45까지이다.
- 1개의 로또를 발행할 때 중복되지 않는 6개의 숫자를 뽑는다.
- 당첨 번호 추첨 시 중복되지 않는 숫자 6개와 보너스 번호 1개를 뽑는다.
- 당첨은 1등부터 5등까지 있다. 당첨 기준과 금액은 아래와 같다.
- 1등: 6개 번호 일치 / 2,000,000,000원
- 2등: 5개 번호 + 보너스 번호 일치 / 30,000,000원
- 3등: 5개 번호 일치 / 1,500,000원
- 4등: 4개 번호 일치 / 50,000원
- 5등: 3개 번호 일치 / 5,000원
- 입출력 요구 사항
- 프로그래밍 요구 사항
- 구현과정
요구사항을 살펴보니 "indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현 (2까지만 허용)" 이 있었다.
함수를 분리하여 구현하는 것이 중요해보였고,
또 다른 요구사항인 "함수가 한가지 일만 하도록 최대한 작게 만들기" 또한 함수 분리 구현이 필수적인 것 같았다.
2주차와 마찬가지로 JUnit 5와 AssertJ 를 이용해 각 기능별로 단위 테스트를 진행해야했다.
또 어려웠던 점은 함수의 길이가 15라인이상 넘어가지 않도록 구현해야했는데
클래스의 분리를 더 세분화시켜야할 것 같았다.
요구사항에서 제공된 lotto 클래스의 기본구조는 2주차에서의 Init_game 클래스와 비슷해보였다.
IllegalArgumentException을 발생시키는 부분이나, number.size가 넘치지 않도록 조건문을 달아주는 것 또한
숫자 야구 게임과 유사한 부분이 있었다.
이번 프로젝트에서는 클래스를 더 세분화해야하기때문에 lotto 패키지 안에
다른 세부 패키지로 나누어서 관리해야할 것 같았다.
lotto 패키지 안에 기능을 구현할 domain과 예외처리를 할 exception, 화면에 보여지는 view 패키지로 나눌 수 있었다.
Application과 Gameplay는 lotto 패키지 내에서 다른 클래스들을 참조하여 실행할 수 있도록 설계하였다.
<Application.java>
package lotto;
public class Application {
public static void main(String[] args) {
// TODO: 프로그램 구현
final Gameplay gameplay = Gameplay.create();
gameplay.run();
}
}
- Application.java에서 Gameplay를 진행하고 gameplay 클래스 안에서 게임이 실제 시작되는 로직을 구현하였다.
<Gameplay.java>
package lotto;
import lotto.domain.lotto.LottoFactory;
import lotto.domain.lotto.LottoGroup;
import lotto.domain.lotto.LottoMoney;
import lotto.domain.lotto.WinningLotto;
import lotto.domain.result.Result;
import lotto.domain.result.TicketCount;
import lotto.view.InputView;
import lotto.view.ResultView;
public class Gameplay {
private Gameplay(){
}
public static Gameplay create() {
return new Gameplay();
}
private static float getProfit(final float nowMoney, final float pastMoney) {
return nowMoney / pastMoney;
}
public void run() {
final LottoMoney lottoMoney = makeLottoMoney();
final TicketCount count = makeLottoTicketCount(lottoMoney);
final LottoGroup lottoTickets = makeLottos(count);
ResultView.printLottoTickets(count, lottoTickets);
final WinningLotto winningLotto = makeWinNums();
final Result result = Result.of(lottoTickets, winningLotto);
end(result, lottoMoney);
}
private LottoMoney makeLottoMoney() {
return LottoMoney.from(
InputView.inputMoney());
}
private TicketCount makeLottoTicketCount(final LottoMoney money) {
return TicketCount.of(money.toLottoCount(),
InputView.inputManualTicketCount());
}
private LottoGroup makeLottos(final TicketCount count) {
return LottoFactory.createLottos(count,
InputView.inputManualNums(count.ofManual()));
}
private WinningLotto makeWinNums() {
return LottoFactory.createWinNums(InputView.inputWinLottoNums(), InputView.inputBonusNumber());
}
private void end(final Result result, final LottoMoney lottoMoney) {
ResultView.printLottosResult(result);
ResultView.printProfit(getProfit((float) result.getPrize(), (float) lottoMoney.get()));
}
}
생성자를 만들고, create()를 통해 Gameplay를 재호출하도록 기본 골격을 만든다.
run()부분이 중요했는데, 로또금액인 lottoMoney와 TicketCount, LottoGroup 등을 참조하였다.
로또에 당첨되는 부분 또한 result를 통해 구현하였다.
makeLottoMoney()에서는 View 패키지의 InputView에서 참조할 수 있었다.
run부분에서는 전체적으로 생성과 게임을 진행시키는 Initialize 부분을 만들어냈다.
<domain> - <generator>
<NumsGenerator.java>
package lotto.domain.generator;
import lotto.domain.lotto.LottoNumber;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class NumsGenerator {
private static final int START_INDEX = 0;
private static final int END_INDEX = 6;
public static List<LottoNumber> RandomNumber(){
final List<LottoNumber> lottoNumbers = new ArrayList<>(LottoNumber.getCache().values());
Collections.shuffle(lottoNumbers);
return lottoNumbers.subList(START_INDEX, END_INDEX);
}
public static List<LottoNumber> generate(final List<Integer> lottoNums){
return lottoNums.stream()
.map(LottoNumber::getInstance)
.sorted()
.collect(Collectors.toList());
}
}
- 시작 인덱스와 끝 인덱스를 final로 설정하고, static final 처럼 변하지 않는 전역변수를 설정할때는
대문자로 변수를 만드는 것이 좋다는 것을 새롭게 배울 수 있었다. (우테코 프리코스 깃허브 커뮤니티)
RandomNumber메서드로 로또 번호를 생성하고 Collections 클래스의 shuffle 메서드로 이 번호들을 무작위로 섞는다.
generator 메서드에서는 stream()을 이용해 기능을 세부적으로 접근하여 리스트로 만든다.
<domain> - <lotto>
<Lotto.java>
package lotto.domain.lotto;
import lotto.exception.lotto.LottoNumDuplicatedException;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
public class Lotto {
private static final int SIZE = 6;
final List<LottoNumber> numbers;
public Lotto(List<LottoNumber> numbers) {
validate(numbers);
Collections.sort(numbers);
this.numbers = List.copyOf(numbers);
}
private static void validate(List<LottoNumber> numbers) {
Set<LottoNumber> setNumbers = new HashSet<>(numbers);
if (numbers.size() != SIZE) {
throw new IllegalArgumentException();
}
if (setNumbers.size() != numbers.size()){
throw new LottoNumDuplicatedException(numbers);
}
}
// TODO: 추가 기능 구현
public static Lotto from(final List<LottoNumber> numbers){
return new Lotto(numbers);
}
public int countSameNum(final WinningLotto winningLotto){
return (int) numbers.stream()
.filter(winningLotto::contains)
.count();
}
public boolean contains(final LottoNumber lottoNumber) {
return numbers.contains(lottoNumber);
}
public List<LottoNumber> get() {
return List.copyOf(numbers);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Lotto lotto1 = (Lotto) o;
return Objects.equals(numbers, lotto1.numbers);
}
@Override
public int hashCode() {
return Objects.hash(numbers);
}
@Override
public String toString() {
return "Lotto{" +
"lotto=" + numbers +
'}';
}
}
- 로또의 속성을 관리하는 부분이며 6개의 번호를 받는 부분과 정렬하는 부분을 구현하였다.
로또 클래스에서는 6개의 번호 규격을 넘어갈 경우 IllegalArgumentException을 발생시키도록 하였다.
setNumbers를 HashSet으로 만들어 해시의 중복제거 특징을 활용하여 LottoNumDuplicatedException 예외처리 시
중복을 검사할 수 있도록 하였다.
countSameNum메서드로 winningLotto (맞춘개수)를 증가시킨다.
<lotto> - <lotto> - <LottoFactory.java> 에서는
보너스를 처리하고, 자동적으로 로또번호를 생성하도록 하였다.
<lotto> - <lotto> - <LottoGroup.java> 에서는
NullException을 발생시키는 부분, Lotto.java와 같이 equals를 처리하는 부분을 작성하였다.
<lotto> - <lotto> - <LottoMoney.java> 에서는
LottoMoneyLessException : 금액 예외처리와
LottoMoneyDivideException : Lotto 번호 나누는 예외처리를 구현하였다.
<lotto> - <lotto> - <LottoNumber.java> 에서는
로또 번호의 범위를 지정하였다.
private static final int MIN = 1;
private static final int MAX = 45;
compareTo 를 통해 각 클래스에서 지정된 object와 기준 값을 비교할 수 있도록 하였다.
@Override
public int compareTo(LottoNumber o) {
return java.lang.Integer.compare(value, o.get());
}
<lotto> - <lotto>
<WinningLotto.java>
package lotto.domain.lotto;
import lotto.exception.lotto.BonusNumDuplicatedException;
public class WinningLotto {
private final Lotto lotto;
private final LottoNumber bonusNumber;
private WinningLotto(final Lotto lotto, final LottoNumber bonusNumber) {
validate(lotto, bonusNumber);
this.lotto = lotto;
this.bonusNumber = bonusNumber;
}
public static WinningLotto of(final Lotto lotto, final LottoNumber bonusNumber) {
return new WinningLotto(lotto, bonusNumber);
}
private static void validate(final Lotto lotto, final LottoNumber number) {
if (lotto.contains(number)) {
throw new BonusNumDuplicatedException(number.get());
}
}
public boolean contains(final LottoNumber number) {
return lotto.contains(number);
}
public LottoNumber getBonusNumber() {
return bonusNumber;
}
}
- 보너스 숫자를 관리하는 부분과 번호를 맞췄을때 다른 클래스를 참조할 수 있도록 다리 기능의 코드를 구현하였다.
<lotto> - <result>
<Rank.java>
package lotto.domain.result;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiPredicate;
import java.util.stream.Collectors;
public enum Rank {
NONE(0, 0,
(matchCount, isBonus) -> matchCount < 3),
FIFTH(5000, 3,
(matchCount, isBonus) -> matchCount == 3),
FOURTH(50000, 4,
(matchCount, isBonus) -> matchCount == 4),
THIRD(1500000, 5,
(matchCount, isBonus) -> matchCount == 5 && !isBonus),
SECOND(30000000, 5,
(matchCount, isBonus) -> matchCount == 5 && isBonus),
FIRST(2000000000, 6,
(matchCount, isBonus) -> matchCount == 6);
private final int prize;
private final int matchCount;
private final BiPredicate<Integer, Boolean> condition;
Rank(final int prize, final int matchCount, BiPredicate<Integer, Boolean> condition) {
this.prize = prize;
this.matchCount = matchCount;
this.condition = condition;
}
public static Rank of(final int matchCount, final boolean isBonus) {
return Arrays.stream(Rank.values())
.filter(rank -> rank.condition.test(matchCount, isBonus))
.findAny()
.orElse(NONE);
}
public static List<Rank> getWithoutDefault() {
return Arrays.stream(Rank.values())
.filter(rank -> !rank.equals(NONE))
.collect(Collectors.toList());
}
public int getPrize() {
return prize;
}
public int getMatchCount() {
return matchCount;
}
}
- 아무것도 못맞췄을때와 어느정도 맞추었을때의 상금의 크기를 지정하였다.
5위부터 금액이 매겨져 1위에서는 20억을 prize로 지정하였다.
<lotto> - <result>
<Result.java>
package lotto.domain.result;
import lotto.domain.lotto.Lotto;
import lotto.domain.lotto.LottoGroup;
import lotto.domain.lotto.WinningLotto;
import java.util.LinkedHashMap;
public class Result {
private final LinkedHashMap<Rank, Integer> value = new LinkedHashMap<>();
private Result(final LottoGroup lottos, final WinningLotto winningLotto) {
for (Lotto lotto : lottos.get()) {
Rank rank = Rank.of(lotto.countSameNum(winningLotto), lotto.contains(winningLotto.getBonusNumber()));
add(rank);
}
}
public static Result of(final LottoGroup lottos, final WinningLotto winningLotto) {
return new Result(lottos, winningLotto);
}
private void add(final Rank rank) {
value.merge(rank, 1, Integer::sum);
}
public long getPrize() {
long prize = 0;
for (Rank rank : value.keySet()) {
prize += (long) rank.getPrize() * value.get(rank);
}
return prize;
}
public int getRankCount(final Rank rank) {
return value.getOrDefault(rank, 0);
}
}
Result에서는 전체적인 결과를 만들어내기 위해 rank를 merge하는 부분과
getPrize()로 상금을 계산하는 부분을 구현하였다.
<lotto> - <result> - <TicektCount.java> 에서는
로또를 몇장 사는지 받아와서 자동적으로 로또번호를 생성하는 다리 역할을 하도록 구현하였다.
<lotto> - <exception> 에서 위에 각 구현하였던 클래스 별로 발생할 수 있는 예외들을 작성해서 처리하도록 하였다.
크게 중복, 잘못된 패턴, 규격에 맞지않는 사이즈 관련 예외처리 등이 있으며,
TicketCount를 처리하는 부분에서는 로또 장 수가 입력되지 않거나 너무 많게 입력되었을 경우의 처리를 담당하였다.
<lotto> - <view>
<InputView.java>
package lotto.view;
import lotto.exception.lotto.LottoNumWrongPatternException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Scanner;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class InputView {
private static final String MONEY_INPUT_MESSAGE = "구입금액을 입력해 주세요.";
private static final String LOTTO_NUMBER_INPUT_MESSAGE = System.lineSeparator() + "지난 주 당첨 번호를 입력해 주세요.";
private static final String BONUS_INPUT_MESSAGE = "보너스 볼을 입력해 주세요.";
private static final String MANUAL_TICKET_COUNT_MESSAGE = System.lineSeparator() + "수동으로 구매할 로또 수를 입력해 주세요.";
private static final String MANUAL_TICKET_NUMBS_GROUP_MESSAGE = System.lineSeparator() + "수동으로 구매할 번호를 입력해 주세요.";
private static final String REGEX = ",";
private static final Pattern PATTERN = Pattern.compile("^[\\d]+,[\\d]+,[\\d]+,[\\d]+,[\\d]+,[\\d]+$");
private static final Scanner SCANNER = new Scanner(System.in);
public static int inputMoney() {
System.out.println(MONEY_INPUT_MESSAGE);
return inputNum();
}
public static int inputManualTicketCount() {
System.out.println(MANUAL_TICKET_COUNT_MESSAGE);
return inputNum();
}
public static int inputBonusNumber() {
System.out.println(BONUS_INPUT_MESSAGE);
return inputNum();
}
public static List<Integer> inputWinLottoNums() {
System.out.println(LOTTO_NUMBER_INPUT_MESSAGE);
return inputLottoNums();
}
public static List<List<Integer>> inputManualNums(final int repeatCount) {
System.out.println(MANUAL_TICKET_NUMBS_GROUP_MESSAGE);
List<List<Integer>> numsGroup = new ArrayList<>();
for (int i = 0; i < repeatCount; i++) {
numsGroup.add(inputLottoNums());
}
return numsGroup;
}
private static int inputNum() {
int number = SCANNER.nextInt();
SCANNER.nextLine();
return number;
}
private static List<Integer> inputLottoNums() {
final String lottoNumbers = SCANNER.nextLine();
validateLottoNums(lottoNumbers);
return separateNumbers(lottoNumbers);
}
private static List<Integer> separateNumbers(final String numbersText) {
return Arrays.stream(numbersText.split(REGEX))
.map(Integer::parseInt)
.collect(Collectors.toList());
}
private static void validateLottoNums(final String rawLottoNumbers) {
if (!PATTERN.matcher(rawLottoNumbers).matches()) {
throw new LottoNumWrongPatternException(rawLottoNumbers);
}
}
}
- 전체적으로 게임 UI를 출력할 수 있도록 하였고, 입력을 받는 부분을 InputView.java에서 처리할 수 있도록 하였다.
새롭게 공부한 사실은
System.out.println( 문자열변수 );
>>
private static final String MONEY_INPUT_MESSAGE = "구입금액을 입력해 주세요.";
처럼 위에서 전역변수로 지정해주고, 그 문자열 변수에 따라 각 클래스에 넣어 처리하도록 하는 부분이
클래스 세분화에 있어서 중요한 부분이라고 생각하였다.
<lotto> - <view>
<ResultView.java>
package lotto.view;
import lotto.domain.lotto.Lotto;
import lotto.domain.lotto.LottoGroup;
import lotto.domain.lotto.LottoNumber;
import lotto.domain.result.Rank;
import lotto.domain.result.Result;
import lotto.domain.result.TicketCount;
public class ResultView {
private static final String BUY_MESSAGE =
System.lineSeparator() + "%d개를 구매했습니다." + System.lineSeparator();
private static final String LOTTO_PREFIX = "[";
private static final String LOTTO_ENDFIX = "]";
private static final String SEPARATOR = ", ";
private static final int DELETE_IDX = 2;
private static final String RESULT_START_MESSAGE =
System.lineSeparator() + "당첨 통계" + System.lineSeparator() + "---";
private static final String RESULT_RANK_MESSAGE = "%d개 일치%s(%d원)- %d개" + System.lineSeparator();
private static final String SAME_BONUS_MESSAGE = ", 보너스 볼 일치";
private static final String PROFIT_MESSAGE =
"총 수익률은 %.2f입니다." + System.lineSeparator();
private static final String NO_MESSAGE = " 아니";
public static void printLottoTickets(final TicketCount count, final LottoGroup lottoTickets) {
System.out.printf(BUY_MESSAGE, count.ofManual(), count.ofAuto());
for (Lotto lotto : lottoTickets.get()) {
printLottoNumbers(lotto);
}
}
private static void printLottoNumbers(final Lotto lotto) {
StringBuilder result = new StringBuilder(LOTTO_PREFIX);
for (LottoNumber lottoNumber : lotto.get()) {
result.append(lottoNumber.get()).append(SEPARATOR);
}
result.delete(result.length() - DELETE_IDX, result.length()).append(LOTTO_ENDFIX);
System.out.println(result);
}
public static void printLottosResult(final Result result) {
System.out.println(RESULT_START_MESSAGE);
for (Rank rank : Rank.getWithoutDefault()) {
System.out.printf(RESULT_RANK_MESSAGE,
rank.getMatchCount(), printIfSecond(rank),
rank.getPrize(), result.getRankCount(rank));
}
}
private static String printIfSecond(final Rank rank) {
if (rank.equals(Rank.SECOND)) {
return SAME_BONUS_MESSAGE;
}
return " ";
}
public static void printProfit(final float profit) {
System.out.printf(PROFIT_MESSAGE,
profit, printIfLoss(profit));
}
private static Object printIfLoss(final float profit) {
if (profit >= 1) {
return NO_MESSAGE;
}
return "";
}
}
- 입력된 부분을 처리된 후 출력을 규격에 맞게 할 수 있는 부분과
전체적인 프로그램 진행상황을 화면에 보여질 수 있도록 하였다.
또한 최종적인 결과 출력과 입력했을 때마다 나올 수 있는 부분들을 구현해낼 수 있었다.
제출 결과로 모든 예제를 통과할 수 없었다. 확실히 주마다 난이도도 올라가고, 추가되는 요구사항도 많아
클래스를 한번 구현한 후에도 코드 리팩토링을 통해 계속 수정하는 부분이 많았다.
이번에도 예제 테스트를 통과할 수 있도록 많은 리팩토링을 거쳤지만 과제 제출기간 내에 완벽히 구현해내지 못해서
아쉬운 결과였다.
마지막 주차를 남겨두고 있는 가운데 다음 과제를 위해 더 공부하며 코드적으로 보강해야겠다고 생각했다.
'Recording > 우아한테크코스 5기 Pre-course' 카테고리의 다른 글
[마무리🎆] [우테코 5기] <4주차> 'java-bridge' 회고 (0) | 2023.03.11 |
---|---|
[우테코 5기] <2주차> 'java-baseball' 회고 (1) | 2022.12.13 |
[우테코 5기] <1주차> 'java-onboarding' 회고 (2) | 2022.11.29 |
[우테코 5기] <0주차> OT 후기 (0) | 2022.11.06 |