3주차 미션이 종료되었다.
회고에 앞서 지난 2주차 때 세웠던 Try와 코드리뷰를 먼저 확인해보자!
지난 Try
- 구현부터 리팩토링은 나중에!
- 이번엔 객체지향적 설계에 맞춰보자!
- 코드리뷰 스터디를 통해 구현의도를 설명하자
- README.md를 통해 프로젝트 소개를 야무지게 해보자
코드리뷰 종합
- 서비스 레이어의 역할을 고려하며 설계하기 by @Heon0208
- 정적 팩토리 메서드를 무작정 도입은 말자 by @wns312
미션 초기 프리코스 커뮤니티 함께-나누기
에 올라온 게시글 하나
바로 스토리텔링식으로 이야기를 짜보고 협력/책임/역할로 나눠보는 것이었다. Thanks to 작성자님🙏🏻
이런걸 그냥 넘어갈 내가 아니지. 바로 실천해보았다.
1️⃣ 이야기 : 로또를 구매하러 옴
- 게임장엔 구매자, 로또 매니저, 진행자, 게임 매니저, 회계사가 있었음
- 진행자가 "로또를 구매하라"함
- 구매자가 “이 금액만큼 자동이요”라고 외침
- 구매자가 정렬된 로또 번호들을 소유함
- 진행자가 "당첨번호랑 보너스 번호 적어놔" 함
- 게임 매니저가 6개 당첨번호와 1개 보너스번호를 적어둠
- 진행자가 로또 매니저에게 "로또번호들 말하라" 외침
- 진행자가 "1등부터 5등별로 총 몇 개 됐는지 확인해봐" 함
- 심판이 결과를 말함
- 진행자가 "수익률을 말하라" 외침
- 회계사가 수익률을 계산함
2️⃣ 협력 : 요청과 응답
- 누군가가 진행자에게 로또 게임을 요청함으로써 게임이 시작됨
- 진행자는 구매자에게 로또번호들을 구매할 것을 요청함
- 구매자는 금액만큼의 자동으로 생성되는 로또번호들을 요청함
- 로또 매니저는 정렬된 6자리 로또 번호들을 응답함
- 진행자가 게임 매니저에게 6자리 당첨번호와 1개의 보너스 번호를 요청함
- 게임 매니저는 6자리 당첨번호와 1개의 보너스번호를 응답함
- 진행자가 서기에게 로또번호들 목록들을 적어라 요청함
- 서기가 로또 매니저에게 로또번호들 요청함
- 로또 매니저가 로또번호들을 응답함
- 서기가 로또번호들 목록을 응답함
- 진행자가 게임 매니저에게 1등부터 5등별 게임 결과를 요청함
- 게임 매니저가 로또 매니저에게 번호들을 요청함
- 로또 매니저가 로또번호들을 응답함
- 게임 매니저가 게임 결과를 응답함
- 진행자가 회계사에게 수익률을 요청함
- 회계사가 로또 구매자에게 초기 자본과 게임 매니저에게 게임 결과를 요청함
- 로또 구매자가 초기 자본을 응답함
- 게임 매니저가 게임 결과를 응답함
- 회계사가 수익률 계산 결과를 응답함
3️⃣ 책임 : 하는 것과 아는 것
- 진행자
- 하는 것
- 구매자에게 로또 구매를 요청, 구매자에게 로또 번호 목록들을 요청
- 게임 매니저에게 게임 결과를 답하도록 요청, 회계사에게 총 수익률을 요청
- 구매자
- 하는 것 : 로또 번호 목록을 요청
- 아는 것 : 초기 금액
로또 매니저
- 하는 것: 로또 번호 목록을 생성
- 아는 것:
- 로또 번호 최대 최소
- 로또 규칙
게임 매니저
- 하는 것 : 게임 결과 산출
- 아는 것 : 당첨번호와 보너스 번호
회계사
- 하는 것: 총 수익률 계산
- 아는 것: 계산 방법
마지막 역할은 고민을 해보았지만 해결되지 않았다.
완벽하진 않았지만 이 글을 토대로 설계를 시작하니 상당히 체계적으로 구현이 되었다.
.matches()
로직을 사용할때 Pattern 객체를 캐싱해야 성능 이슈를 줄일 수 있는 것을 알게 되었다. - Thanks to 현지님
역시나 나는 뭐다? 스펀지가 되어보기로 했으니 바로 적용해본다.
public class InputView {
private static final String REGEXP_ONLY_NUMBER = "^\\d*$";
private static final Pattern PATTERN_ONLY_NUMBER = Pattern.compile(REGEXP_ONLY_NUMBER);
//...//
}
실제 구현까지 해보니 왜 해야하는지가 더 머릿속에 남게 되는 기분이다.
public class LottoResult {
private final Map<LottoRanking, Integer> lottoResults;
private LottoResult(Map<LottoRanking, Integer> lottoResults) {
this.lottoResults = lottoResults;
}
public static LottoResult createLottoResult(Map<LottoRanking, Integer> lottoResults) {
return new LottoResult(lottoResults);
}
public void addLottoResult(LottoRanking lottoRanking) {
if (Objects.nonNull(lottoRanking)) {
lottoResults.put(lottoRanking, lottoResults.get(lottoRanking) + 1);
}
}
당첨된 로또를 저장하기 위해 Map을 사용했다.
로또 1장당 발생하는 당첨 결과를 추가하는데 void 메서드를 사용하게 되었다. 문제는 테스트에서 발생하였다.
반환값이 없으면 테스트를 진행할 수 없었다.
물론 mock을 사용하면 테스트는 돌아갔지만 이번 미션의 마음가짐과 위반되는 행동이었다.
이번 미션 마음가짐
- getter 사용을 지양하자.
- 테스트 작성시 mock을 지양하자.
결국 설계 오류로 남기며 미션이 종료되었다.
public class LottoGameManager {
private final LottoOwner lottoOwner;
private final WinningLotto winningLotto;
private boolean isEnd = false;
//...//
public void matchLottosWithWinningLotto() {
if (isEnd) {
throw new IllegalStateException(OWNER_ALREADY_MATCH_WITH_WINNING_LOTTO.toString());
}
isEnd = lottoOwner.matchLottosWithWinningLotto(winningLotto);
}
public WinningStatistics getWinningStatistics() {
if (!isEnd) {
throw new IllegalStateException(NOT_MATCHING_WITH_WINNING_LOTTO.toString());
}
return lottoOwner.getWinningStatistics();
}
//...//
}
로또 게임에서 발생하면 안되는 상태를 생각해보았다.
그걸 염두하여 위와 같은 로또게임관리자 클래스를 설계하였다.
그러나 나의 프로그램 설계상 isEnd는 무조건 정상 상태를 유지하였다.
이 또한 테스트를 하지 못하여 아쉬운 부분으로 남게 되었다.
EnumMap은 Enum을 키로 사용하는 Map이다.
public void run() {
LottoOwner lottoOwner = getLottoOwner();
outputView.printLottosInfo(lottoOwner.getLottosInfo());
Lotto winningNumbers = getWinningNumbersUntilValid();
outputView.printSpace();
WinningLotto winningLotto = getWinningLottoUntilValidBonusNumber(winningNumbers);
LottoGameManager lottoGameManager = LottoGameManager.createGameManager(lottoOwner, winningLotto);
lottoGameManager.matchLottosWithWinningLotto();
printWinningStatistics(lottoGameManager);
outputView.printRateOfReturn(lottoGameManager.getRateOfReturn());
}
대망의 4주차도 클린 코드를 적용해보려한다. 지난 Try 목록은 자연스레 keep으로 쌓여가게 된다.