자바/초록스터디

(6)초록스터디 로또 1,2 단계 구현

문상휘파람 2024. 5. 23. 20:18

로또 미션의 전체적인 틀은 다음과 같다.

 

로또 미션

 

로또 미션은 클린 코드를 목표로 진행되는 미션인 것 같다.

 

 원래는 레이싱카 미션이 앞에 있는데 초록스터디 하기 전에 멘토님과 다른 스터디를 진행하면서 레이싱카를 구현했기에, 레이싱카 미션은 협의 하에 뛰어넘기로 하였다. 


로또 1,2 단계 기능 구현 목록이다

로또 1,2 단계 기능 구현 목록

 

 이번 로또 미션에서는 일급컬렉션과 원시값 포장이 핵심으로 느껴졌다. 일급컬렉션에 대해서는 들어본 적 있지만, 원시값 포장은 나에게 너무 생소했다. 그래서 여러 블로그와 문서를 참고하여 일급컬렉션과 원시값 포장에 관한 개념을 익히고 기능 구현을 시작했다.


로또 1,2 단계 기능 구현

 

도메인 객체는 다음과 같이 나누었다.

 

 

도메인 객체 하나 하나 뜯어보며 내가 썼던 코드를 복기해봐야겠다.


* LottoController

package controller;

import domain.*;
import view.InputView;
import view.OutputView;

import java.util.List;
import java.util.Random;

public class LottoController {

    private final InputView inputView;
    private final OutputView outputView;

    public LottoController(InputView inputView, OutputView outputView) {
        this.inputView = inputView;
        this.outputView = outputView;
    }

    public void startLotto() {
        int lottoMoney = getLottoMoney();
        Lottos lottos = buyLotto(lottoMoney);
        List<Integer> lastWeekLottoNumber = getLastWeekLottoNumber();
        rankLotto(lottos, lastWeekLottoNumber, lottoMoney);
    }

    private int getLottoMoney() {
        outputView.printGetLottoMoney();
        return inputView.getLottoMoney();
    }

    private Lottos buyLotto(int getLottoMoney) {
        Random randomNumberGenerator = new Random();
        outputView.printLottoCount(getLottoMoney);
        CreateLottoNumber createLottoNumber = new LottoNumberGenerator(randomNumberGenerator);
        Lottos lottos = new Lottos(getLottoMoney, createLottoNumber);
        for (Lotto lotto : lottos.getLottos()) {
            outputView.printLotto(lotto.getLottoNumber());
        }
        return lottos;
    }

    private List<Integer> getLastWeekLottoNumber() {
        outputView.LastWeekLottoNumber();
        String inputLastWeekLottoNumber = inputView.inputLastWeekLottoNumber();
        LastWeekLottoNumber lottoNumber = new LastWeekLottoNumber(inputLastWeekLottoNumber);
        return lottoNumber.getLastWeekLottoNumber();
    }

    private void rankLotto(Lottos lottos, List<Integer> lastWeekLottoNumber, int getLottoMoney) {
        LottosRank lottosRank = new LottosRank(lottos, lastWeekLottoNumber);
        List<Integer> lottoRank = lottosRank.getRankLottos();
        LottoReturnRate lottoReturnRate = new LottoReturnRate(lottoRank, getLottoMoney);
        outputView.printLottoStatistics();
        double returnRate = lottoReturnRate.calculateLottoReturnRate();
        outputView.printLottoRanker(lottoRank, lottoReturnRate.makeLottoPrice());
        outputView.printRateOfReturn(returnRate);
    }
}

 

 컨트롤러는 위 코드와 같이 구현하였다.  크게 4가지 기능으로 나누었다.

로또 머니 입력 -> 로또 생성 -> 지난 주 로또 번호 입력 -> 로또 순위 계산(수익률 계산 포함)


*Lotto

package domain;

import java.util.List;

public class Lotto {
    private final List<Integer> lottoNumber;

    public Lotto(CreateLottoNumber createLottoNumber) {
        this.lottoNumber = createLottoNumber.getRandomLottoNumber();
    }

    public List<Integer> getLottoNumber() {
        return lottoNumber;
    }
}

 

 도메인 Lotto 클래스 코드는 다음과 같다. 원래는 로또 랜덤 번호를 가지는 클래스를 따로 만들어서 값을 로또 객체에 넘겨주어 로또를 생성하는 형식으로 만들었는데, 이 경우 제대로 된 테스트 코드를 짤 수 없었다. 

 정확히는 랜덤 번호를 가지는 클래스를 테스트 할 수 없었다.(랜덤 번호가 잘 생성되는지 테스트가 불가했음. 진짜 말 그대로 랜덤 번호라 어떤 숫자가 생성되는지 테스트 불가 ㅠ)

 

 그래서 멘토님에게 조언을 구해 인터페이스를 생성하였고, 인터페이스를 통해 메서드를 오버라이딩 하여 랜덤 넘버를 관리하였다. 인터페이스로 메서드 오버라이딩을 하게 되면 테스트 코드 짤 때, 또 다른 클래스(FakeCreateLottoNumber)에 인터페이스 상속 받아서 테스트 코드를 쉽게 구현할 수 있음 ㅋ.

 

따라서 위 코드에서 CreateLottoNumber 인터페이스를 생성자로 받아서 바로 랜덤 번호 생성받을 수 있게 해줬음.


*CreateLottoNumber(인터페이스) 

package domain;

import java.util.List;

public interface CreateLottoNumber {

    List<Integer> getRandomLottoNumber();
}

 

위 코드가 바로 인터페이스 코드이다. 다른 클래스에서 List<Integer> getRandomLottoNumber 메서드를 오버라이딩 함.


*LottoNumberGenerator

package domain;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class LottoNumberGenerator implements CreateLottoNumber {

    private static final int INITIAL_NUMBER = 0;
    private static final int FIRST_LOTTO_NUMBER = 1;
    private static final int LAST_LOTTO_NUMBER = 45;
    private static final int LOTTO_NUMBER_LENGTH_BOUNDARY = 6;

    private final Random randomNumberGenerator;

    public LottoNumberGenerator(Random randomNumberGenerator) {
        this.randomNumberGenerator = randomNumberGenerator;
    }

    @Override
    public List<Integer> getRandomLottoNumber() {
        List<Integer> getLotto = new ArrayList<>();
        for (int i = INITIAL_NUMBER; i < LOTTO_NUMBER_LENGTH_BOUNDARY; i++) {
            int randomLottoNumber = checkDuplicateNumber(getLotto, randomNumberGenerator.nextInt(FIRST_LOTTO_NUMBER,LAST_LOTTO_NUMBER));
            getLotto.add(randomLottoNumber);
        }
        return getLotto;
    }

    private int generateRandomNumber(){
        return randomNumberGenerator.nextInt(FIRST_LOTTO_NUMBER,LAST_LOTTO_NUMBER);
    }

    private int checkDuplicateNumber(List<Integer> buyLotto, int randomLottoNumber) {
        if (buyLotto.contains(randomLottoNumber)) {
            return checkDuplicateNumber(buyLotto, generateRandomNumber());
        }
        return randomLottoNumber;
    }
}

 

 이 클래스가 위에서 정의한 인터페이스를 상속받아 메서드를 재정의 한 클래스이다. 로또 숫자이기에 중복체크 기능 넣었고, 크게 어려운 메서드는 없음.


*Lottos

package domain;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Lottos {

    private static final int INITIAL_NUMBER = 0;
    private static final int LOTTO_COUNT_NUMBER = 1000;

    private final int lottoMoney;
    private final List<Lotto> lottos;

    public Lottos(int lottoMoney, CreateLottoNumber createLottoNumber) {
        this.lottoMoney = lottoMoney;
        this.lottos = makeLottos(createLottoNumber);
    }

    public List<Lotto> getLottos() {
        return lottos;
    }

    private List<Lotto> makeLottos(CreateLottoNumber createLottoNumber) {
        List<Lotto> lottosBundle = new ArrayList<>();
        for (int i = INITIAL_NUMBER; i < lottoMoney / LOTTO_COUNT_NUMBER; i++) {
            Lotto lotto = new Lotto(createLottoNumber);
            sortLottoNumber(lotto.getLottoNumber());
            lottosBundle.add(lotto);
        }
        return lottosBundle;
    }

    private void sortLottoNumber(List<Integer> lottoNumber) {
        Collections.sort(lottoNumber);
    }
}

 

생성된 로또들을 로또 묶음으로 만들어 주는 클래스이다. 여기도 별로 어려운 부분은 없고, 로또 묶음을 만들어 줄 때 오름차순으로 정렬시켜서 넣어줬다.


*LastWeekLottoNumber

package domain;

import java.util.ArrayList;
import java.util.List;

public class LastWeekLottoNumber {

    private static final String SPLIT_STRING_DELIMITER = ",";

    private final List<Integer> lastWeekLottoNumber;
    private final String inputLastWeekLottoNumber;

    public LastWeekLottoNumber(String LastWeekLottoNumber) {
        this.inputLastWeekLottoNumber = LastWeekLottoNumber;
        this.lastWeekLottoNumber = makeLastWeekLottoNumberList();
    }

    public List<Integer> getLastWeekLottoNumber() {
        return lastWeekLottoNumber;
    }

    private List<Integer> makeLastWeekLottoNumberList() {
        List<String> inputLottoNumber = splitStringLottoNumber();
        List<Integer> lastWeekLottoNumber = new ArrayList<>();
        for (String lottoNumber : inputLottoNumber) {
            lastWeekLottoNumber.add(Integer.parseInt(lottoNumber));
        }
        return lastWeekLottoNumber;
    }

    private List<String> splitStringLottoNumber() {
        String[] lottoNumber = inputLastWeekLottoNumber.split(SPLIT_STRING_DELIMITER);
        return List.of(lottoNumber);
    }
}

 

이 클래스는 지난 주 로또 번호를 문자열로 입력 받으면, 로또 숫자와 맞출 수 있도록 Integer 형으로 반환해주는 클래스이다.

저번에 구현하였던 문자열 계산기 메서드랑 흡사하다.


*LottoRank

package domain;

import java.util.List;

public class LottoRank {

    private static final int RESET_NUMBER = 0;
    private static final int INITIAL_NUMBER = 1;

    private final int rankLottoNumber;
    private final Lotto lotto;
    private final List<Integer> lastWeekLottoNumber;

    public LottoRank(Lotto lotto, List<Integer> lastWeekLottoNumber) {
        this.lotto = lotto;
        this.lastWeekLottoNumber = lastWeekLottoNumber;
        this.rankLottoNumber = rankLotto();
    }

    public int getSameLottoNumber() {
        return rankLottoNumber;
    }

    public int rankLotto() {
        int correspondingLottoNumber = RESET_NUMBER;
        for (int elementOfLastWeekLottoNumber : lastWeekLottoNumber) {
            correspondingLottoNumber += checkSameLottoNumber(lotto.getLottoNumber(), elementOfLastWeekLottoNumber);
        }
        return correspondingLottoNumber;
    }

    private int checkSameLottoNumber(List<Integer> lottoNumber, int elementOfLastWeekLottoNumber) {
        if (lottoNumber.contains(elementOfLastWeekLottoNumber)) {
            return INITIAL_NUMBER;
        }
        return RESET_NUMBER;
    }
}

 

이 클래스는 로또 번호와 지난 주 당첨 번호를 비교하여 몇 개 맞았는지 확인하여 반환해주는 코드이다. 특별히 어려운 메서드는 없다.


*LottosRank

package domain;

import java.util.ArrayList;
import java.util.List;

public class LottosRank {

    private static final int INITIAL_NUMBER = 1;
    private static final int START_RANK_NUMBER = 3;
    private static final int LAST_RANK_NUMBER = 6;

    private final List<Integer> lastWeekLottoNumber;
    private final List<Integer> rankLottos;
    private final Lottos lottos;

    public LottosRank(Lottos lottos, List<Integer> lastWeekLottoNumber) {
        this.lottos = lottos;
        this.lastWeekLottoNumber = lastWeekLottoNumber;
        this.rankLottos = makeLottosRank();
    }

    public List<Integer> getRankLottos() {
        return rankLottos;
    }

    private List<Integer> makeLottosRank() {
        List<Integer> correspondingLottoNumber = checkCorrespondingLottoNumber();
        List<Integer> rankLottos = new ArrayList<>();
        for (int i = START_RANK_NUMBER; i < LAST_RANK_NUMBER + INITIAL_NUMBER; i++) {
            int rankNumber = i;
            long countRankNumber = correspondingLottoNumber.stream().filter(c -> c == rankNumber).count();
            rankLottos.add((int) countRankNumber);
        }
        return rankLottos;
    }

    private List<Integer> checkCorrespondingLottoNumber() {
        List<Lotto> lottosBundle = lottos.getLottos();
        List<Integer> correspondingLottoNumber = new ArrayList<>();
        for (Lotto lotto : lottosBundle) {
            LottoRank lottoRank = new LottoRank(lotto, lastWeekLottoNumber);
            correspondingLottoNumber.add(lottoRank.getSameLottoNumber());
        }
        return correspondingLottoNumber;
    }
}

 

 LottoRank 에서 몇 개 맞았는지 반환 받은 후, 3개 일치 ~ 6개 일치가 몇 개인지 확인할 수 있도록 하는 클래스이다.

사실 이 부분 하면서 LottoRank와 합칠까 고민을 정말 많이 하였는데, 메서드가 너무 길어지는 바람에 그냥 나누었다. 아직까지도 나누는게 맞는지 잘 모르겠음.


*LottoReturnRate

package domain;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Random;

public class LottoReturnRate {

    private static final int FIRST_RANKER_PRICE = 2000000000;
    private static final int SECOND_RANKER_PRICE = 1500000;
    private static final int THIRD_RANKER_PRICE = 50000;
    private static final int FOURTH_RANKER_PRICE = 5000;
    private static final int RESET_NUMBER = 0;
    private static final double MAKE_RETURN_RATE_DEVIDE_NUMBER = 100.0;
    private static final int MAKE_RETURN_RATE_MULTIPLE_NUMBER = 100;

    private final List<Integer> lottoRank;
    private final int getLottoMoney;

    public LottoReturnRate(List<Integer> lottoRank, int getLottoMoney) {
        this.lottoRank = lottoRank;
        this.getLottoMoney = getLottoMoney;
    }

    public List<Integer> makeLottoPrice() {
        return List.of(FOURTH_RANKER_PRICE, THIRD_RANKER_PRICE, SECOND_RANKER_PRICE, FIRST_RANKER_PRICE);
    }

    public double calculateLottoReturnRate() {
        List<Integer> lottoRankPrice = makeLottoPrice();
        int sumOfLottoMoney = RESET_NUMBER;
        for (int i = RESET_NUMBER; i < lottoRankPrice.size(); i++) {
            sumOfLottoMoney += lottoRank.get(i) * lottoRankPrice.get(i);
        }
        double lottoReturnRate = (double) sumOfLottoMoney / getLottoMoney;
        return Math.floor(lottoReturnRate * MAKE_RETURN_RATE_MULTIPLE_NUMBER) / MAKE_RETURN_RATE_DEVIDE_NUMBER;
    }
}

 

로또의 수익률을 정해주는 기능을 가지고 있는 클래스이다. 이 부분도 그렇게 어려운 메서드는 없는 것 같다.


*InputView

package view;

import java.util.Scanner;

public class InputView {

    private final Scanner input = new Scanner(System.in);

    public int getLottoMoney() {
        return input.nextInt();
    }

    public String inputLastWeekLottoNumber() {
        return input.next();
    }
}

 

Inputview는 로또 머니와 저번주 당첨 번호를 입력받는 메서드를 구현하였다.


*OutputView

package view;

import java.util.List;

public class OutputView {

    private static final int START_STATISTICS_NUMBER = 3;
    private static final int DEVIDE_LOTTO_COUNT_NUMBER = 1000;

    public void printGetLottoMoney() {
        System.out.println("구입금액을 입력해 주세요.");
    }

    public void printLottoCount(int lottoCount) {
        System.out.println("\n" + lottoCount / DEVIDE_LOTTO_COUNT_NUMBER + "개를 구매했습니다.");
    }

    public void printLotto(List<Integer> lottoNumber) {
        System.out.println(lottoNumber);
    }

    public void LastWeekLottoNumber() {
        System.out.println("\n" + "지난 주 당첨 번호를 입력해 주세요.");
    }

    public void printLottoStatistics() {
        System.out.println("\n당첨 통계");
        System.out.println("---------");
    }

    public void printLottoRanker(List<Integer> lottoRank, List<Integer> lottoPrice) {
        for (int i = START_STATISTICS_NUMBER; i < lottoRank.size() + START_STATISTICS_NUMBER; i++) {
            System.out.println(i + "개 일치 (" + lottoPrice.get(i - START_STATISTICS_NUMBER) + "원)- " + lottoRank.get(i - START_STATISTICS_NUMBER) + "개");
        }
    }

    public void printRateOfReturn(double rateOfReturn) {
        System.out.println("총 수익률은 " + rateOfReturn + "입니다.");
    }
}

 

OutputView는 도메인과의 의존성을 끊어내려고 많이 노력하였고, 최종적으로 잘 끊어냈다. 구현한 메서드는 위와 같다.


*Application

import controller.LottoController;
import view.InputView;
import view.OutputView;

public class Application {

    public static void main(String[] args) {
        OutputView outputView = new OutputView();
        InputView inputView = new InputView();
        LottoController buyLottoController = new LottoController(inputView, outputView);
        buyLottoController.startLotto();
    }
}

 

요기서 최종적으로 작동.

 

최종 작동 사진


정리.

 

 테스트 코드도 구현하였는데, 글이 너무 길어질 거 같아서 다음 글에 테스트 코드 작성할 것이다. 너무 길면 안읽고 싶어지니까.......

 

 일단 잘 구현은 한 것 같은데, 일급컬렉션과 원시값을 잘 포장하였는지 의문이다. 이 두 가지 개념은 계속 찾아보고 실습해보고 익히며 누구 알려줄 수 있을 정도까지 숙달한 후 이에 관한 글을 쓰고 싶다. 자바 넘 어려운데 재밌어. 중독적이야.

 그리고 메서드 구현하면서 걸리는 점은 RottoRank 와 RottosRank에 관한 부분........ 아무리 생각해도 합치면 코드가 좀 길어지더라도 굳이 안나눠도 될 것 같은데 괜히 나눈 느낌이다 ㅠㅠ 그래도 이대로 PR 요청 드려보고 코드 리뷰 받으며 다시 고쳐봐야겠다.