Repository: next-step/java-lotto-clean-playground
Pull request #86: [LBP] 김준 로또 미션 제출합니다.
Author: login: kjoon418
Created: 2025-02-28T15:31:43Z | Merged: 2025-03-05T08:36:12Z

로또 미션의 5단계 리펙터링까지 수행했습니다.
미션을 수행하며 든 궁금점들을 질문 드리고 싶습니다.

### 질문
- 무작위 `LottoNumber`로 구성된 `LottoNumbers` 인스턴스를 외부(`RandomLottoNumbersGenerator`)에서 만들고자 했습니다.
   - 그를 위해 LottoNumber가 `1` ~ `45` 범위의 숫자로만 구성되어야 하는 것을 `LottoNumber`와 `RandomLottoNumbersGenerator` 클래스 모두에서 알고 있어야 했기에, 코드의 중복을 방지하고자 로또의 제약조건들을 `LottoConstraints`라는 별도의 클래스가 지니도록 했습니다.
   - 이런 설계에 미흡한 점이나 제가 놓친 문제점이 있는지 궁금합니다.
- 미션을 수행하며 모델 클래스와 유틸 클래스을 구분짓는 차이점이 무엇인지 고민이 많았습니다.
  현재는 사용자로부터 받은 입력값을 변환하는 `InputConverter`와, 무작위 값을 지닌 `LottoNumbers`를 생성하는 `RandomLottoNumbersGenerator`를 유틸 클래스로 구분지었습니다.
   - 리뷰어님은 어떤 기준으로 유틸 클래스와 모델 클래스를 구분하시는지 궁금합니다!
- 가독성이 좋은 코드를 어떻게 짤 수 있을지 많이 고민하며 작성했습니다.
   - 부족한 점이 많을 것 같은데, 가독성 측면에서 미흡한 코드가 있다면 피드백 달아주시면 정말 큰 도움이 될 것 같습니다!

리뷰어님 덕분에 이번 미션을 수행하면서도 많은 고민을 하며 성장할 수 있었습니다.
피드백 정말 감사합니다!


Diff:
diff --git a/src/main/java/LottoApplication.java b/src/main/java/LottoApplication.java
index d125fd276..71b6072c1 100644
--- a/src/main/java/LottoApplication.java
+++ b/src/main/java/LottoApplication.java
@@ -4,7 +4,8 @@ public class LottoApplication {
 
     public static void main(String[] args) {
         try {
-            new LottoController().run();
+            LottoController lottoController = LottoController.getInstance();
+            lottoController.run();
         } catch (Exception e) {
             e.printStackTrace();
         }
diff --git a/src/main/java/controller/LottoController.java b/src/main/java/controller/LottoController.java
index 61a1adf48..5903b31ee 100644
--- a/src/main/java/controller/LottoController.java
+++ b/src/main/java/controller/LottoController.java
@@ -1,31 +1,115 @@
 package controller;
 
-import model.Lotto;
-import dto.LottoDto;
-import model.LottoPurchase;
-import view.*;
-import java.util.*;
+import dto.LottoNumbersDto;
+import dto.LottoResultDto;
+import dto.LottosDto;
+import model.*;
+import view.LottoInputView;
+import view.LottoOutputView;
+
+import java.text.DecimalFormat;
+import java.util.List;
 
 public class LottoController {
 
-    private final LottoInputView lottoInputView = new LottoInputView();
-    private final LottoOutputView lottoOutputView = new LottoOutputView();
-    private final LottoPurchase lottoPurchase = new LottoPurchase();
+    private static final DecimalFormat TOTAL_PROFIT_RATE_FORMATTER = new DecimalFormat("0.00");
+
+    private final LottoInputView lottoInputView;
+    private final LottoOutputView lottoOutputView;
+
+    private static final LottoController lottoController = new LottoController();
+
+    private LottoController() {
+        this.lottoInputView = LottoInputView.getInstance();
+        this.lottoOutputView = LottoOutputView.getInstance();
+    }
+
+    public static LottoController getInstance() {
+        return lottoController;
+    }
 
     public void run() {
         int purchaseAmount = lottoInputView.getPurchaseAmount();
-        lottoInputView.printEmptyLine();
+        Lottos lottos = purchaseLottos(purchaseAmount);
+
+        LottoNumbers winningLottoNumbers = getWinningLottoNumbers();
+        LottoNumber bonusBall = getBonusBall();
+
+        LottoResults lottoResults = new LottoResults(lottos, winningLottoNumbers, bonusBall);
+        printLottoResults(lottoResults);
+        printTotalProfitRate(lottoResults, purchaseAmount);
+    }
+
+    private Lottos purchaseLottos(int purchaseAmount) {
+        int manualLottoAmount = lottoInputView.getManualLottoAmount();
+        List<LottoNumbers> manualLottoNumbersCollection = getManualLottoNumbers(manualLottoAmount);
+        Lottos lottos = Lottos.purchase(purchaseAmount, manualLottoNumbersCollection);
+
+        int randomLottoAmount = lottos.getLottoAmount() - manualLottoAmount;
+        lottoOutputView.printLottoAmount(manualLottoAmount, randomLottoAmount);
+        lottoOutputView.printLottos(new LottosDto(lottos));
+
+        return lottos;
+    }
+
+    private List<LottoNumbers> getManualLottoNumbers(int manualLottoAmount) {
+        validateManualLottoAmount(manualLottoAmount);
+        if (isZero(manualLottoAmount)) {
+            return List.of();
+        }
+
+        List<LottoNumbersDto> manualLottoNumbersDtos = lottoInputView.getManualLottoNumbers(manualLottoAmount);
+
+        return manualLottoNumbersDtos.stream()
+                .map(LottoNumbersDto::toLottoNumbers)
+                .toList();
+    }
+
+    private void validateManualLottoAmount(int manualLottoAmount) {
+        if (isNegative(manualLottoAmount)) {
+            throw new IllegalArgumentException("수동으로 구매할 로또의 수는 음수일 수 없습니다: " + manualLottoAmount);
+        }
+    }
+
+    private boolean isNegative(int number) {
+        return number < 0;
+    }
+
+    private boolean isZero(int number) {
+        return number == 0;
+    }
+
+    private LottoNumbers getWinningLottoNumbers() {
+        LottoNumbersDto winningLottoNumbersDto = lottoInputView.getWinningLottoNumbers();
 
-        List<Lotto> lottos = lottoPurchase.purchaseRandomLottos(purchaseAmount);
-        lottoOutputView.printLottoAmount(lottos.size());
+        return winningLottoNumbersDto.toLottoNumbers();
+    }
+
+    private LottoNumber getBonusBall() {
+        int bonusBallInput = lottoInputView.getBonusBall();
 
-        printLottoList(lottos);
+        return new LottoNumber(bonusBallInput);
     }
 
-    private void printLottoList(List<Lotto> lottos) {
-        for (Lotto lotto : lottos) {
-            lottoOutputView.printLotto(LottoDto.from(lotto));
+    private void printLottoResults(LottoResults lottoResults) {
+        lottoOutputView.printLottoResultHeader();
+
+        for (LottoResult lottoResult : lottoResults.getLottoResultCollection()) {
+            lottoOutputView.printLottoResult(new LottoResultDto(lottoResult));
         }
     }
 
+    private void printTotalProfitRate(LottoResults lottoResults, int purchaseAmount) {
+        double totalProfitRate = lottoResults.getTotalProfitRate(purchaseAmount);
+        double formattedTotalProfitRate = formatTotalProfitRate(totalProfitRate);
+
+        lottoOutputView.printTotalProfitRate(formattedTotalProfitRate);
+    }
+
+    private double formatTotalProfitRate(double totalProfitRate) {
+        String formattedTotalProfitRate = TOTAL_PROFIT_RATE_FORMATTER.format(totalProfitRate);
+
+        return Double.parseDouble(formattedTotalProfitRate);
+    }
+
 }
diff --git a/src/main/java/dto/LottoDto.java b/src/main/java/dto/LottoDto.java
deleted file mode 100644
index 582e7ea81..000000000
--- a/src/main/java/dto/LottoDto.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package dto;
-
-import model.Lotto;
-import model.LottoNumber;
-
-import java.util.List;
-
-public class LottoDto {
-
-    private List<Integer> lottoNumbers;
-
-    public static LottoDto from(Lotto lotto) {
-        LottoDto lottoDto = new LottoDto();
-
-        lottoDto.lottoNumbers = lotto.getLottoNumbers().stream()
-                .map(LottoNumber::getValue)
-                .toList();
-
-        return lottoDto;
-    }
-
-    @Override
-    public String toString() {
-        return lottoNumbers.toString();
-    }
-
-}
diff --git a/src/main/java/dto/LottoNumbersDto.java b/src/main/java/dto/LottoNumbersDto.java
new file mode 100644
index 000000000..2c2ce7171
--- /dev/null
+++ b/src/main/java/dto/LottoNumbersDto.java
@@ -0,0 +1,27 @@
+package dto;
+
+import model.LottoNumber;
+import model.LottoNumbers;
+
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class LottoNumbersDto {
+
+    private final List<String> stringLottoNumbers;
+
+    public LottoNumbersDto(List<String> stringLottoNumbers) {
+        this.stringLottoNumbers = List.copyOf(stringLottoNumbers);
+    }
+
+    public LottoNumbers toLottoNumbers() {
+        Set<LottoNumber> lottoNumbers = stringLottoNumbers.stream()
+                .map(Integer::parseInt)
+                .map(LottoNumber::new)
+                .collect(Collectors.toUnmodifiableSet());
+
+        return new LottoNumbers(lottoNumbers);
+    }
+
+}
diff --git a/src/main/java/dto/LottoResultDto.java b/src/main/java/dto/LottoResultDto.java
new file mode 100644
index 000000000..ad3a0d3af
--- /dev/null
+++ b/src/main/java/dto/LottoResultDto.java
@@ -0,0 +1,35 @@
+package dto;
+
+import model.LottoResult;
+
+public class LottoResultDto {
+
+    private final int equalCount;
+    private final int prizeAmount;
+    private final int lottoAmount;
+    private final boolean bonusBallEqual;
+
+    public LottoResultDto(LottoResult lottoResult) {
+        this.equalCount = lottoResult.getEqualCount();
+        this.prizeAmount = lottoResult.getPrizeAmount();
+        this.lottoAmount = lottoResult.getLottoAmount();
+        this.bonusBallEqual = lottoResult.isBonusBallResult();
+    }
+
+    public int getEqualCount() {
+        return equalCount;
+    }
+
+    public int getPrizeAmount() {
+        return prizeAmount;
+    }
+
+    public int getLottoAmount() {
+        return lottoAmount;
+    }
+
+    public boolean isBonusBallEqual() {
+        return bonusBallEqual;
+    }
+
+}
diff --git a/src/main/java/dto/LottosDto.java b/src/main/java/dto/LottosDto.java
new file mode 100644
index 000000000..4ad82d36f
--- /dev/null
+++ b/src/main/java/dto/LottosDto.java
@@ -0,0 +1,24 @@
+package dto;
+
+import model.LottoNumbers;
+import model.Lottos;
+
+import java.util.List;
+
+public class LottosDto {
+
+    private final List<String> lottoStrings;
+
+    public LottosDto(Lottos lottos) {
+        List<LottoNumbers> lottoNumbersCollection = lottos.getLottoNumbersCollection();
+
+        this.lottoStrings = lottoNumbersCollection.stream()
+                .map(LottoNumbers::toString)
+                .toList();
+    }
+
+    public List<String> getValues() {
+        return List.copyOf(lottoStrings);
+    }
+
+}
diff --git a/src/main/java/model/Lotto.java b/src/main/java/model/Lotto.java
deleted file mode 100644
index 9dbe7c3fd..000000000
--- a/src/main/java/model/Lotto.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package model;
-
-import java.util.*;
-
-public class Lotto {
-
-    private final LottoNumbers lottoNumbers;
-
-    private Lotto(LottoNumbers lottoNumbers) {
-        this.lottoNumbers = lottoNumbers;
-    }
-
-    public static Lotto getRandomLotto() {
-        return new Lotto(LottoNumbers.getRandomLottoNumbers());
-    }
-
-    public Set<LottoNumber> getLottoNumbers() {
-        return lottoNumbers.getCopy();
-    }
-
-}
diff --git a/src/main/java/model/LottoConstraints.java b/src/main/java/model/LottoConstraints.java
new file mode 100644
index 000000000..c378aced2
--- /dev/null
+++ b/src/main/java/model/LottoConstraints.java
@@ -0,0 +1,13 @@
+package model;
+
+public class LottoConstraints {
+
+    public static final int LOTTO_PRICE = 1_000;
+    public static final int MINIMUM_LOTTO_NUMBER = 1;
+    public static final int MAXIMUM_LOTTO_NUMBER = 45;
+    public static final int LOTTO_SIZE = 6;
+
+    private LottoConstraints() {
+    }
+
+}
diff --git a/src/main/java/model/LottoNumber.java b/src/main/java/model/LottoNumber.java
index 9e0b7821d..a1a8981fb 100644
--- a/src/main/java/model/LottoNumber.java
+++ b/src/main/java/model/LottoNumber.java
@@ -1,28 +1,29 @@
 package model;
 
 import java.util.Objects;
-import java.util.Random;
 
-public class LottoNumber {
+import static model.LottoConstraints.MAXIMUM_LOTTO_NUMBER;
+import static model.LottoConstraints.MINIMUM_LOTTO_NUMBER;
 
-    private final static int MINIMUM_LOTTO_NUMBER = 1;
-    private final static int MAXIMUM_LOTTO_NUMBER = 45;
+public record LottoNumber(int value) {
 
-    private final static Random RANDOM = new Random();
-
-    private final int value;
-
-    private LottoNumber(int value) {
+    public LottoNumber {
         validateLottoNumber(value);
-        this.value = value;
     }
 
-    public static LottoNumber getRandomLottoNumber() {
-        return new LottoNumber(getRandomNumber());
+    private static void validateLottoNumber(int value) {
+        if (value < MINIMUM_LOTTO_NUMBER) {
+            throw new IllegalArgumentException(String.format("입력된 값이 %d보다 작습니다: %d", MINIMUM_LOTTO_NUMBER, value));
+        }
+
+        if (value > MAXIMUM_LOTTO_NUMBER) {
+            throw new IllegalArgumentException(String.format("입력된 값이 %d보다 큽니다.: %d", MAXIMUM_LOTTO_NUMBER, value));
+        }
     }
 
-    public int getValue() {
-        return value;
+    @Override
+    public String toString() {
+        return Integer.toString(value);
     }
 
     @Override
@@ -38,18 +39,4 @@ public int hashCode() {
         return Objects.hashCode(value);
     }
 
-    private static int getRandomNumber() {
-        return RANDOM.nextInt(MINIMUM_LOTTO_NUMBER, MAXIMUM_LOTTO_NUMBER + 1);
-    }
-
-    private static void validateLottoNumber(int value) {
-        if (value < MINIMUM_LOTTO_NUMBER) {
-            throw new IllegalArgumentException(String.format("입력된 값이 %d보다 작습니다: %d", MINIMUM_LOTTO_NUMBER, value));
-        }
-
-        if (value > MAXIMUM_LOTTO_NUMBER) {
-            throw new IllegalArgumentException(String.format("입력된 값이 %d보다 큽니다.: %d", MAXIMUM_LOTTO_NUMBER, value));
-        }
-    }
-
 }
diff --git a/src/main/java/model/LottoNumberMatcher.java b/src/main/java/model/LottoNumberMatcher.java
new file mode 100644
index 000000000..425e6ff11
--- /dev/null
+++ b/src/main/java/model/LottoNumberMatcher.java
@@ -0,0 +1,19 @@
+package model;
+
+import java.util.function.Supplier;
+
+public class LottoNumberMatcher {
+
+    private int numberEqualCount;
+
+    public void increaseIfMatch(Supplier<Boolean> lottoNumberCompareExpression) {
+        if (lottoNumberCompareExpression.get()) {
+            this.numberEqualCount++;
+        }
+    }
+
+    public int getNumberEqualCount() {
+        return this.numberEqualCount;
+    }
+
+}
diff --git a/src/main/java/model/LottoNumbers.java b/src/main/java/model/LottoNumbers.java
index 4e0ec844a..ac6676311 100644
--- a/src/main/java/model/LottoNumbers.java
+++ b/src/main/java/model/LottoNumbers.java
@@ -1,32 +1,44 @@
 package model;
 
-import java.util.*;
+import java.util.Comparator;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import static model.LottoConstraints.LOTTO_SIZE;
 
 public class LottoNumbers {
 
-    private static final Comparator<LottoNumber> COMPARATOR = Comparator.comparingInt(LottoNumber::getValue);
-    private static final int LOTTO_SIZE = 6;
+    private static final Comparator<LottoNumber> NUMBER_COMPARATOR = Comparator.comparingInt(LottoNumber::value);
 
-    private final SortedSet<LottoNumber> lottoNumbers = new TreeSet<>(COMPARATOR);
+    private final SortedSet<LottoNumber> lottoNumbers;
 
-    private LottoNumbers(Set<LottoNumber> lottoNumbers) {
+    public LottoNumbers(Set<LottoNumber> lottoNumbers) {
         validateSize(lottoNumbers.size());
+        this.lottoNumbers = new TreeSet<>(NUMBER_COMPARATOR);
         this.lottoNumbers.addAll(lottoNumbers);
     }
 
-    public static LottoNumbers getRandomLottoNumbers() {
-        Set<LottoNumber> lottoNumbers = new HashSet<>();
+    public LottoNumbers getCopy() {
+        return new LottoNumbers(Set.copyOf(this.lottoNumbers));
+    }
+
+    public int getEqualNumbersCount(LottoNumbers otherLottoNumbers) {
+        LottoNumberMatcher lottoNumberMatcher = new LottoNumberMatcher();
 
-        while (isSmallThanMaxSize(lottoNumbers.size())) {
-            LottoNumber randomLottoNumber = LottoNumber.getRandomLottoNumber();
-            lottoNumbers.add(randomLottoNumber);
+        for (LottoNumber lottoNumber : this.lottoNumbers) {
+            lottoNumberMatcher.increaseIfMatch(() -> otherLottoNumbers.contains(lottoNumber));
         }
 
-        return new LottoNumbers(lottoNumbers);
+        return lottoNumberMatcher.getNumberEqualCount();
+    }
+
+    public boolean hasEqualNumberWithBonusBall(LottoNumber bonusBall) {
+        return this.lottoNumbers.contains(bonusBall);
     }
 
-    public SortedSet<LottoNumber> getCopy() {
-        return Collections.unmodifiableSortedSet(lottoNumbers);
+    private boolean contains(LottoNumber lottoNumber) {
+        return this.lottoNumbers.contains(lottoNumber);
     }
 
     private void validateSize(int size) {
@@ -39,8 +51,9 @@ private boolean isIllegalSize(int size) {
         return size != LOTTO_SIZE;
     }
 
-    private static boolean isSmallThanMaxSize(int size) {
-        return size < LOTTO_SIZE;
+    @Override
+    public String toString() {
+        return lottoNumbers.toString();
     }
 
 }
diff --git a/src/main/java/model/LottoPurchase.java b/src/main/java/model/LottoPurchase.java
deleted file mode 100644
index b6bf59640..000000000
--- a/src/main/java/model/LottoPurchase.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package model;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-public class LottoPurchase {
-
-    private final static int LOTTO_PRICE = 1_000;
-
-    public List<Lotto> purchaseRandomLottos(int purchaseAmount) {
-        validatePurchaseAmount(purchaseAmount);
-        int lottoAmount = calculateLottoAmount(purchaseAmount);
-
-        return createRandomLottos(lottoAmount);
-    }
-
-    private void validatePurchaseAmount(int purchaseAmount) {
-        if (purchaseAmount < LOTTO_PRICE) {
-            throw new IllegalArgumentException("최소 구매 금액은 " + LOTTO_PRICE + "입니다.");
-        }
-
-        if (purchaseAmount % LOTTO_PRICE != 0) {
-            throw new IllegalArgumentException("구입 금액 단위는 " + LOTTO_PRICE + "입니다.");
-        }
-    }
-
-    private int calculateLottoAmount(int purchaseAmount) {
-        return purchaseAmount / LOTTO_PRICE;
-    }
-
-    private List<Lotto> createRandomLottos(int lottoAmount) {
-        List<Lotto> randomLottos = new ArrayList<>();
-
-        for (int i = 0; i < lottoAmount; i++) {
-            randomLottos.add(Lotto.getRandomLotto());
-        }
-
-        return Collections.unmodifiableList(randomLottos);
-    }
-
-}
diff --git a/src/main/java/model/LottoRank.java b/src/main/java/model/LottoRank.java
new file mode 100644
index 000000000..2788c363d
--- /dev/null
+++ b/src/main/java/model/LottoRank.java
@@ -0,0 +1,52 @@
+package model;
+
+public enum LottoRank {
+
+    NO_PRIZE(0, 0) {
+        @Override
+        boolean isCorrectPrize(int equalCount, boolean bonusBallEqual) {
+            return equalCount < THREE_EQUALS.equalCount;
+        }
+    },
+    THREE_EQUALS(5_000, 3) {
+        @Override
+        boolean isCorrectPrize(int equalCount, boolean bonusBallEqual) {
+            return equalCount == this.equalCount;
+        }
+    },
+    FOUR_EQUALS(50_000, 4) {
+        @Override
+        boolean isCorrectPrize(int equalCount, boolean bonusBallEqual) {
+            return equalCount == this.equalCount;
+        }
+    },
+    FIVE_EQUALS(1_500_000, 5) {
+        @Override
+        boolean isCorrectPrize(int equalCount, boolean bonusBallEqual) {
+            return equalCount == this.equalCount && !bonusBallEqual;
+        }
+    },
+    FIVE_WITH_BONUS_EQUALS(30_000_000, 5) {
+        @Override
+        boolean isCorrectPrize(int equalCount, boolean bonusBallEqual) {
+            return equalCount == this.equalCount && bonusBallEqual;
+        }
+    },
+    SIX_EQUALS(2_000_000_000, 6) {
+        @Override
+        boolean isCorrectPrize(int equalCount, boolean bonusBallEqual) {
+            return equalCount == this.equalCount;
+        }
+    };
+
+    public final int prizeAmount;
+    public final int equalCount;
+
+    LottoRank(int prizeAmount, int equalCount) {
+        this.prizeAmount = prizeAmount;
+        this.equalCount = equalCount;
+    }
+
+    abstract boolean isCorrectPrize(int equalCount, boolean bonusBallEqual);
+
+}
diff --git a/src/main/java/model/LottoResult.java b/src/main/java/model/LottoResult.java
new file mode 100644
index 000000000..e42fb29f2
--- /dev/null
+++ b/src/main/java/model/LottoResult.java
@@ -0,0 +1,42 @@
+package model;
+
+public class LottoResult implements Comparable<LottoResult> {
+
+    private final LottoRank lottoRank;
+    private int lottoAmount;
+
+    public LottoResult(LottoRank lottoRank) {
+        this.lottoRank = lottoRank;
+        this.lottoAmount = 0;
+    }
+
+    public void increaseLottoAmount() {
+        lottoAmount++;
+    }
+
+    public int getEqualCount() {
+        return lottoRank.equalCount;
+    }
+
+    public int getPrizeAmount() {
+        return lottoRank.prizeAmount;
+    }
+
+    public int getLottoAmount() {
+        return lottoAmount;
+    }
+
+    public boolean isBonusBallResult() {
+        return lottoRank == LottoRank.FIVE_WITH_BONUS_EQUALS;
+    }
+
+    public int getTotalPrizeAmount() {
+        return lottoRank.prizeAmount * lottoAmount;
+    }
+
+    @Override
+    public int compareTo(LottoResult other) {
+        return this.lottoRank.compareTo(other.lottoRank);
+    }
+
+}
diff --git a/src/main/java/model/LottoResults.java b/src/main/java/model/LottoResults.java
new file mode 100644
index 000000000..f4fa53ce3
--- /dev/null
+++ b/src/main/java/model/LottoResults.java
@@ -0,0 +1,79 @@
+package model;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class LottoResults {
+
+    private final List<LottoResult> lottoResults;
+
+    public LottoResults(Lottos lottos, LottoNumbers winningLottoNumbers, LottoNumber bonusBall) {
+        Map<LottoRank, LottoResult> lottoResultMap = initializeLottoResultMap();
+
+        for (LottoNumbers lotto : lottos.getLottoNumbersCollection()) {
+            LottoRank lottoRank = calculateLottoPrize(lotto, winningLottoNumbers, bonusBall);
+            updateLottoResult(lottoResultMap, lottoRank);
+        }
+
+        removeUnnecessaryLottoResult(lottoResultMap);
+
+        this.lottoResults = convertMapIntoSortedList(lottoResultMap);
+    }
+
+    public double getTotalProfitRate(int purchaseAmount) {
+        return getTotalLottoPrize() / (double) purchaseAmount;
+    }
+
+    public List<LottoResult> getLottoResultCollection() {
+        return List.copyOf(lottoResults);
+    }
+
+    private Map<LottoRank, LottoResult> initializeLottoResultMap() {
+        Map<LottoRank, LottoResult> lottoResultMap = new HashMap<>();
+
+        for (LottoRank lottoRank : LottoRank.values()) {
+            lottoResultMap.put(lottoRank, new LottoResult(lottoRank));
+        }
+
+        return lottoResultMap;
+    }
+
+    private LottoRank calculateLottoPrize(LottoNumbers targetLotto, LottoNumbers winningLotto, LottoNumber bonusBall) {
+        int equalNumberCount = targetLotto.getEqualNumbersCount(winningLotto);
+        boolean containBonusBall = targetLotto.hasEqualNumberWithBonusBall(bonusBall);
+
+        return Arrays.stream(LottoRank.values())
+                .filter(lottoRank -> lottoRank.isCorrectPrize(equalNumberCount, containBonusBall))
+                .findAny()
+                .orElseThrow(() -> new IllegalStateException("Lotto의 상금을 계산할 수 없습니다."));
+    }
+
+    private void updateLottoResult(Map<LottoRank, LottoResult> lottoResultMap, LottoRank lottoRank) {
+        LottoResult lottoResult = lottoResultMap.get(lottoRank);
+        lottoResult.increaseLottoAmount();
+    }
+
+    private void removeUnnecessaryLottoResult(Map<LottoRank, LottoResult> lottoResultMap) {
+        lottoResultMap.remove(LottoRank.NO_PRIZE);
+    }
+
+    private List<LottoResult> convertMapIntoSortedList(Map<LottoRank, LottoResult> lottoResultMap) {
+        return lottoResultMap.values()
+                .stream()
+                .sorted()
+                .toList();
+    }
+
+    private int getTotalLottoPrize() {
+        int sum = 0;
+
+        for (LottoResult lottoResult : lottoResults) {
+            sum += lottoResult.getTotalPrizeAmount();
+        }
+
+        return sum;
+    }
+
+}
diff --git a/src/main/java/model/Lottos.java b/src/main/java/model/Lottos.java
new file mode 100644
index 000000000..c5ddd7d3a
--- /dev/null
+++ b/src/main/java/model/Lottos.java
@@ -0,0 +1,66 @@
+package model;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static model.LottoConstraints.LOTTO_PRICE;
+
+public class Lottos {
+
+    private final List<LottoNumbers> lottoNumbersCollection;
+
+    private Lottos(List<LottoNumbers> lottoNumbersCollection) {
+        this.lottoNumbersCollection = new ArrayList<>(lottoNumbersCollection);
+    }
+
+    public static Lottos purchase(int purchaseAmount, List<LottoNumbers> manualLottoNumbersCollection) {
+        validatePurchase(purchaseAmount, manualLottoNumbersCollection.size());
+
+        List<LottoNumbers> lottos = new ArrayList<>();
+        for (LottoNumbers manualLottoNumbers : manualLottoNumbersCollection) {
+            lottos.add(manualLottoNumbers.getCopy());
+        }
+        for (int i = 0; i < calculateRandomLottoAmount(purchaseAmount, manualLottoNumbersCollection.size()); i++) {
+            lottos.add(RandomLottoNumbersGenerator.getRandomLottoNumbers());
+        }
+
+        return new Lottos(lottos);
+    }
+
+    public int getLottoAmount() {
+        return lottoNumbersCollection.size();
+    }
+
+    public List<LottoNumbers> getLottoNumbersCollection() {
+        return List.copyOf(lottoNumbersCollection);
+    }
+
+    private static void validatePurchase(int purchaseAmount, int manualLottoAmount) {
+        if (isLessThanPrice(purchaseAmount)) {
+            throw new IllegalArgumentException(String.format("금액은 최소 %d원 이상이어야 합니다 - 실제 값: %d", LOTTO_PRICE, purchaseAmount));
+        }
+        if (isNotDivisible(purchaseAmount)) {
+            throw new IllegalArgumentException(String.format("금액은 %d원 단위여야 합니다 - 실제 값: %d", LOTTO_PRICE, purchaseAmount));
+        }
+        if (isNotPurchasable(purchaseAmount, manualLottoAmount)) {
+            throw new IllegalArgumentException(String.format("입력된 금액으로 구매하기에 너무 많은 로또입니다 - 한 장당 가격: %d", LOTTO_PRICE));
+        }
+    }
+
+    private static boolean isLessThanPrice(int purchaseAmount) {
+        return purchaseAmount < LOTTO_PRICE;
+    }
+
+    private static boolean isNotDivisible(int purchaseAmount) {
+        return (purchaseAmount % LOTTO_PRICE) != 0;
+    }
+
+    private static boolean isNotPurchasable(int purchaseAmount, int lottoAmount) {
+        return (purchaseAmount / LOTTO_PRICE) < lottoAmount;
+    }
+
+    private static int calculateRandomLottoAmount(int purchaseAmount, int manualLottoAmount) {
+        return (purchaseAmount / LOTTO_PRICE) - manualLottoAmount;
+    }
+
+}
diff --git a/src/main/java/model/RandomLottoNumbersGenerator.java b/src/main/java/model/RandomLottoNumbersGenerator.java
new file mode 100644
index 000000000..d169eaea5
--- /dev/null
+++ b/src/main/java/model/RandomLottoNumbersGenerator.java
@@ -0,0 +1,38 @@
+package model;
+
+import java.util.HashSet;
+import java.util.Random;
+import java.util.Set;
+
+import static model.LottoConstraints.*;
+
+public class RandomLottoNumbersGenerator {
+
+    private static final Random RANDOM = new Random();
+
+    public static LottoNumbers getRandomLottoNumbers() {
+        Set<LottoNumber> lottoNumbers = new HashSet<>();
+
+        while (smallThanMaxSize(lottoNumbers)) {
+            LottoNumber randomLottoNumber = createRandomLottoNumber();
+            lottoNumbers.add(randomLottoNumber);
+        }
+
+        return new LottoNumbers(lottoNumbers);
+    }
+
+    private static LottoNumber createRandomLottoNumber() {
+        int randomInteger = createRandomInteger();
+
+        return new LottoNumber(randomInteger);
+    }
+
+    private static int createRandomInteger() {
+        return RANDOM.nextInt(MINIMUM_LOTTO_NUMBER, MAXIMUM_LOTTO_NUMBER);
+    }
+
+    private static boolean smallThanMaxSize(Set<LottoNumber> lottoNumbers) {
+        return lottoNumbers.size() < LOTTO_SIZE;
+    }
+
+}
diff --git a/src/main/java/view/LottoInputView.java b/src/main/java/view/LottoInputView.java
index 0e9c8b6b7..9d2a7fa84 100644
--- a/src/main/java/view/LottoInputView.java
+++ b/src/main/java/view/LottoInputView.java
@@ -1,15 +1,75 @@
 package view;
 
-import java.util.Scanner;
+import dto.LottoNumbersDto;
+
+import java.util.*;
 
 public class LottoInputView implements LottoView {
 
+    private static final String INPUT_SEPARATOR = ", ";
+
     private final Scanner scanner = new Scanner(System.in);
 
+    private static final LottoInputView lottoInputView = new LottoInputView();
+
+    private LottoInputView() {
+    }
+
+    public static LottoInputView getInstance() {
+        return lottoInputView;
+    }
+
     public int getPurchaseAmount() {
         System.out.println("구입금액을 입력해 주세요.");
+        int purchaseAmountInput = Integer.parseInt(scanner.nextLine());
+
+        printEmptyLine();
+
+        return purchaseAmountInput;
+    }
+
+    public int getManualLottoAmount() {
+        System.out.println("수동으로 구매할 로또 수를 입력해 주세요.");
+        int manualLottoAmountInput = Integer.parseInt(scanner.nextLine());
+
+        printEmptyLine();
+
+        return manualLottoAmountInput;
+    }
+
+    public List<LottoNumbersDto> getManualLottoNumbers(int manualLottoAmount) {
+        System.out.println("수동으로 구매할 번호를 입력해 주세요");
+
+        List<LottoNumbersDto> lottoNumbersDtos = new ArrayList<>();
+        for (int i = 0; i < manualLottoAmount; i++) {
+            List<String> manualLottoNumbersInput = scanNextLineAsList();
+            lottoNumbersDtos.add(new LottoNumbersDto(manualLottoNumbersInput));
+        }
+
+        return Collections.unmodifiableList(lottoNumbersDtos);
+    }
+
+    public LottoNumbersDto getWinningLottoNumbers() {
+        System.out.println("지난 주 당첨 번호를 입력해 주세요.");
+        List<String> winningLottoNumbersInput = scanNextLineAsList();
+        LottoNumbersDto winningLottoNumbersDto = new LottoNumbersDto(winningLottoNumbersInput);
+
+        printEmptyLine();
+
+        return winningLottoNumbersDto;
+    }
+
+    public int getBonusBall() {
+        System.out.println("보너스 볼을 입력해 주세요.");
+        int bonusBallInput = Integer.parseInt(scanner.nextLine());
+
+        printEmptyLine();
+
+        return bonusBallInput;
+    }
 
-        return scanner.nextInt();
+    private List<String> scanNextLineAsList() {
+        return Arrays.asList(scanner.nextLine().split(INPUT_SEPARATOR));
     }
 
 }
diff --git a/src/main/java/view/LottoOutputView.java b/src/main/java/view/LottoOutputView.java
index 331749d32..4c09b012a 100644
--- a/src/main/java/view/LottoOutputView.java
+++ b/src/main/java/view/LottoOutputView.java
@@ -1,15 +1,62 @@
 package view;
 
-import dto.LottoDto;
+import dto.LottoResultDto;
+import dto.LottosDto;
 
 public class LottoOutputView implements LottoView {
 
-    public void printLotto(LottoDto lottoDto) {
-        System.out.println(lottoDto);
+    private static final LottoOutputView lottoOutputView = new LottoOutputView();
+
+    private LottoOutputView() {
+    }
+
+    public static LottoOutputView getInstance() {
+        return lottoOutputView;
+    }
+
+    public void printLottoAmount(int manualLottoAmount, int randomLottoAmount) {
+        System.out.println("수동으로 " + manualLottoAmount + "장, 자동으로 " + randomLottoAmount + "개를 구매했습니다.");
+    }
+
+    public void printLottos(LottosDto lottosDto) {
+        for (String output : lottosDto.getValues()) {
+            System.out.println(output);
+        }
+
+        printEmptyLine();
+    }
+
+    public void printLottoResultHeader() {
+        System.out.println("당첨 통계");
+        System.out.println("---------");
+    }
+
+    public void printLottoResult(LottoResultDto lottoResultDto) {
+        boolean bonusBallEqual = lottoResultDto.isBonusBallEqual();
+        int equalCount = lottoResultDto.getEqualCount();
+        int prizeAmount = lottoResultDto.getPrizeAmount();
+        int lottoAmount = lottoResultDto.getLottoAmount();
+
+        if (bonusBallEqual) {
+            System.out.printf("%d개 일치, 보너스 볼 일치 (%d원)- %d개\n", equalCount, prizeAmount, lottoAmount);
+            return;
+        }
+
+        System.out.printf("%d개 일치 (%d원)- %d개\n", equalCount, prizeAmount, lottoAmount);
+    }
+
+    public void printTotalProfitRate(double totalProfitRate) {
+        StringBuilder output = new StringBuilder("총 수익률은 " + totalProfitRate + "입니다.");
+
+        if (isLoss(totalProfitRate)) {
+            output.append("(기준이 1이기 때문에 결과적으로 손해라는 의미임)");
+        }
+
+        System.out.println(output);
     }
 
-    public void printLottoAmount(int lottoAmount) {
-        System.out.println(lottoAmount + "개를 구매했습니다.");
+    private boolean isLoss(double totalProfitRate) {
+        return totalProfitRate < 1;
     }
 
 }
diff --git a/src/test/java/fixture/LottoNumbersFixture.java b/src/test/java/fixture/LottoNumbersFixture.java
new file mode 100644
index 000000000..319882e5e
--- /dev/null
+++ b/src/test/java/fixture/LottoNumbersFixture.java
@@ -0,0 +1,52 @@
+package fixture;
+
+import model.LottoNumber;
+import model.LottoNumbers;
+import model.LottoRank;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public enum LottoNumbersFixture {
+
+    WINNING_LOTTO(getLottoNumbersFromIntegers(1, 2, 3, 4, 5, 6), LottoRank.SIX_EQUALS),
+    THREE_EQUALS_LOTTO(getLottoNumbersFromIntegers(1, 2, 3, 43, 44, 45), LottoRank.THREE_EQUALS),
+    FOUR_EQUALS_LOTTO(getLottoNumbersFromIntegers(1, 2, 3, 4, 44, 45), LottoRank.FOUR_EQUALS),
+    FIVE_EQUALS_LOTTO(getLottoNumbersFromIntegers(1, 2, 3, 4, 5, 45), LottoRank.FIVE_EQUALS),
+    SIX_EQUALS_LOTTO(getLottoNumbersFromIntegers(1, 2, 3, 4, 5, 6), LottoRank.SIX_EQUALS);
+
+    public static final LottoNumber NOT_EQUALS_BONUS_BALL = new LottoNumber(33);
+
+    private final LottoNumbers lottoNumbers;
+    private final LottoRank expectedRank;
+
+    LottoNumbersFixture(LottoNumbers lottoNumbers, LottoRank expectedRank) {
+        this.lottoNumbers = lottoNumbers;
+        this.expectedRank = expectedRank;
+    }
+
+    public static List<LottoNumbersFixture> getFixturesExcludeWinningLotto() {
+        return Arrays.stream(values())
+                .filter(lottoNumbersFixture -> lottoNumbersFixture != WINNING_LOTTO)
+                .toList();
+    }
+
+    public LottoNumbers getValue() {
+        return lottoNumbers;
+    }
+
+    public LottoRank getExpectedRank() {
+        return expectedRank;
+    }
+
+    private static LottoNumbers getLottoNumbersFromIntegers(int... numbers) {
+        Set<LottoNumber> lottoNumbers = Arrays.stream(numbers)
+                .mapToObj(LottoNumber::new)
+                .collect(Collectors.toUnmodifiableSet());
+
+        return new LottoNumbers(lottoNumbers);
+    }
+
+}
diff --git a/src/test/java/fixture/LottoResultFixture.java b/src/test/java/fixture/LottoResultFixture.java
new file mode 100644
index 000000000..16f9c5d70
--- /dev/null
+++ b/src/test/java/fixture/LottoResultFixture.java
@@ -0,0 +1,37 @@
+package fixture;
+
+import model.LottoRank;
+import model.LottoResult;
+
+import java.util.Arrays;
+import java.util.List;
+
+public enum LottoResultFixture {
+
+    THREE_EQUALS_RESULT(LottoRank.THREE_EQUALS),
+    FOUR_EQUALS_RESULT(LottoRank.FOUR_EQUALS),
+    FIVE_EQUALS_RESULT(LottoRank.FIVE_EQUALS),
+    FIVE_WITH_BONUS_BALL_EQUALS_RESULT(LottoRank.FIVE_WITH_BONUS_EQUALS),
+    SIX_EQUALS_RESULT(LottoRank.SIX_EQUALS);
+
+    private final LottoRank lottoRank;
+
+    LottoResultFixture(LottoRank lottoRank) {
+        this.lottoRank = lottoRank;
+    }
+
+    public static List<LottoResult> getEveryInstance() {
+        return Arrays.stream(values())
+                .map(LottoResultFixture::getValue)
+                .toList();
+    }
+
+    public LottoRank getLottoRank() {
+        return lottoRank;
+    }
+
+    public LottoResult getValue() {
+        return new LottoResult(lottoRank);
+    }
+
+}
diff --git a/src/test/java/model/LottoNumberTest.java b/src/test/java/model/LottoNumberTest.java
new file mode 100644
index 000000000..b2e725cde
--- /dev/null
+++ b/src/test/java/model/LottoNumberTest.java
@@ -0,0 +1,35 @@
+package model;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class LottoNumberTest {
+
+    @ParameterizedTest
+    @DisplayName("적정 범위 내의 숫자를 통해 인스턴스를 생성한다")
+    @ValueSource(ints = {1, 10, 20, 30, 45})
+    void createByProperNumber(int properNumber) {
+        assertThatCode(() -> new LottoNumber(properNumber)).doesNotThrowAnyException();
+    }
+
+    @ParameterizedTest
+    @DisplayName("최소치보다 작은 숫자를 전달시 예외가 발생한다")
+    @ValueSource(ints = {Integer.MIN_VALUE, -1000, -100, -10, -1, 0})
+    void ifLessThanMinimumThenThrowException(int illegalNumber) {
+        assertThatThrownBy(() -> new LottoNumber(illegalNumber))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @ParameterizedTest
+    @DisplayName("최대치보다 높은 숫자를 전달시 예외가 발생한다")
+    @ValueSource(ints = {46, 100, 999, 15000, Integer.MAX_VALUE})
+    void ifBiggerThanMaximumThenThrowException(int illegalNumber) {
+        assertThatThrownBy(() -> new LottoNumber(illegalNumber))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+}
diff --git a/src/test/java/model/LottoNumbersTest.java b/src/test/java/model/LottoNumbersTest.java
new file mode 100644
index 000000000..4d6681e4a
--- /dev/null
+++ b/src/test/java/model/LottoNumbersTest.java
@@ -0,0 +1,73 @@
+package model;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class LottoNumbersTest {
+
+    private static final String STRING_SEPARATOR = " ";
+
+    @Test
+    @DisplayName("LottoNumber 6개를 통해 인스턴스를 생성한다")
+    void createBySixLottoNumbers() {
+        Set<LottoNumber> lottoNumberCollection = createLottoNumberCollection(1, 2, 3, 4, 5, 6);
+
+        assertThatCode(() -> new LottoNumbers(lottoNumberCollection)).doesNotThrowAnyException();
+    }
+
+    @ParameterizedTest
+    @DisplayName("LottoNumber의 개수가 6개 미만이면 예외가 발생한다")
+    @CsvSource({
+            "20",
+            "1 45",
+            "10 20 30",
+            "11 22 33 44",
+            "1 2 3 4 5"
+    })
+    void ifLottoNumberCountLessThanSixThenThrowException(String numbersString) {
+        Set<LottoNumber> illegalLottoNumberCollection = convertNumbersStringIntoLottoNumberCollection(numbersString);
+
+        assertThatThrownBy(() -> new LottoNumbers(illegalLottoNumberCollection))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @ParameterizedTest
+    @DisplayName("LottoNumber의 개수가 6개 초과면 예외가 발생한다")
+    @CsvSource({
+            "1 2 3 4 5 6 7",
+            "10 11 20 21 30 31 40 41",
+            "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15"
+    })
+    void ifLottoNumberCountBiggerThanSixThenThrowException(String numbersString) {
+        Set<LottoNumber> illegalLottoNumberCollection = convertNumbersStringIntoLottoNumberCollection(numbersString);
+
+        assertThatThrownBy(() -> new LottoNumbers(illegalLottoNumberCollection))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    private Set<LottoNumber> convertNumbersStringIntoLottoNumberCollection(String numbersString) {
+        String[] splitedNumbersString = numbersString.split(STRING_SEPARATOR);
+
+        return Arrays.stream(splitedNumbersString)
+                .map(Integer::parseInt)
+                .map(LottoNumber::new)
+                .collect(Collectors.toUnmodifiableSet());
+    }
+
+    private Set<LottoNumber> createLottoNumberCollection(int... numbers) {
+        return Arrays.stream(numbers)
+                .boxed()
+                .map(LottoNumber::new)
+                .collect(Collectors.toUnmodifiableSet());
+    }
+
+}
diff --git a/src/test/java/model/LottoResultTest.java b/src/test/java/model/LottoResultTest.java
new file mode 100644
index 000000000..60c5a66a6
--- /dev/null
+++ b/src/test/java/model/LottoResultTest.java
@@ -0,0 +1,185 @@
+package model;
+
+import fixture.LottoResultFixture;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class LottoResultTest {
+
+    private static final int TEST_REPEAT_COUNT = 100;
+    private static final Comparator<LottoResult> NATURAL_COMPARATOR = null;
+
+    @Test
+    @DisplayName("increaseLottoAmount() 호출시마다 lottoAmount를 1 증가시킨다")
+    void increaseLottoAmount() {
+        LottoResult lottoResult = LottoResultFixture.THREE_EQUALS_RESULT.getValue();
+
+        for (int expectedLottoAmount = 0; expectedLottoAmount < TEST_REPEAT_COUNT; expectedLottoAmount++) {
+            int actualLottoAmount = lottoResult.getLottoAmount();
+            assertThat(actualLottoAmount).isEqualTo(expectedLottoAmount);
+
+            lottoResult.increaseLottoAmount();
+        }
+    }
+
+    @Test
+    @DisplayName("내부적으로 보유한 LottoRank를 기준으로 정렬된다")
+    void sortedByLottoRank() {
+        List<LottoResult> lottoResultCollection = new ArrayList<>(LottoResultFixture.getEveryInstance());
+        lottoResultCollection.sort(NATURAL_COMPARATOR);
+        List<LottoRank> sortedLottoRanks = getSortedLottoRanks();
+
+        for (int i = 0; i < lottoResultCollection.size(); i++) {
+            LottoResult lottoResult = lottoResultCollection.get(i);
+            LottoRank lottoRank = sortedLottoRanks.get(i);
+
+            assertThat(lottoResult.getPrizeAmount()).isEqualTo(lottoRank.prizeAmount);
+        }
+    }
+
+    @Test
+    @DisplayName("해당 로또 결과에 대한 equalCount를 반환한다")
+    void getEqualsCountAboutLottoResult() {
+        LottoResultFixture[] lottoResultFixtures = LottoResultFixture.values();
+
+        for (LottoResultFixture lottoResultFixture : lottoResultFixtures) {
+            LottoResult actualLottoResult = lottoResultFixture.getValue();
+            LottoRank expectedLottoRank = lottoResultFixture.getLottoRank();
+
+            int actualEqualCount = actualLottoResult.getEqualCount();
+            int expectedEqualCount = expectedLottoRank.equalCount;
+
+            assertThat(actualEqualCount).isEqualTo(expectedEqualCount);
+        }
+    }
+
+    @Test
+    @DisplayName("해당 로또 결과에 대한 prizeAmount를 반환한다")
+    void getPrizeAmountAboutLottoResult() {
+        LottoResultFixture[] lottoResultFixtures = LottoResultFixture.values();
+
+        for (LottoResultFixture lottoResultFixture : lottoResultFixtures) {
+            LottoResult actualLottoResult = lottoResultFixture.getValue();
+            LottoRank expectedLottoRank = lottoResultFixture.getLottoRank();
+
+            int actualPrizeAmount = actualLottoResult.getPrizeAmount();
+            int expectedPrizeAmount = expectedLottoRank.prizeAmount;
+
+            assertThat(actualPrizeAmount).isEqualTo(expectedPrizeAmount);
+        }
+    }
+
+    @Test
+    @DisplayName("해당 로또 결과가 보너스볼 관련 결과인지를 반환한다")
+    void getBonusBallResult() {
+        LottoResult bonusBallResult = LottoResultFixture.FIVE_WITH_BONUS_BALL_EQUALS_RESULT.getValue();
+        LottoResult notBonusBallResult = LottoResultFixture.SIX_EQUALS_RESULT.getValue();
+
+        assertThat(bonusBallResult.isBonusBallResult()).isTrue();
+        assertThat(notBonusBallResult.isBonusBallResult()).isFalse();
+    }
+
+    @Nested
+    @DisplayName("해당 로또 결과의 총 상금을 반환한다")
+    class getTotalPrizeAmount {
+
+        @ParameterizedTest
+        @DisplayName("3개 일치 로또의 총 상금을 반환한다")
+        @ValueSource(ints = {1, 10, 100, 1000})
+        void getTotalPrizeAmountOfThreeEqualsLotto(int lottoAmount) {
+            LottoResult lottoResultAboutSixEquals = LottoResultFixture.THREE_EQUALS_RESULT.getValue();
+
+            for (int i = 0; i < lottoAmount; i++) {
+                lottoResultAboutSixEquals.increaseLottoAmount();
+            }
+
+            int actualTotalPrizeAmount = lottoResultAboutSixEquals.getTotalPrizeAmount();
+            int expectedTotalPrizeAmount = LottoRank.THREE_EQUALS.prizeAmount * lottoAmount;
+
+            assertThat(actualTotalPrizeAmount).isEqualTo(expectedTotalPrizeAmount);
+        }
+
+        @ParameterizedTest
+        @DisplayName("4개 일치 로또의 총 상금을 반환한다")
+        @ValueSource(ints = {1, 10, 100, 1000})
+        void getTotalPrizeAmountOfFourEqualsLotto(int lottoAmount) {
+            LottoResult lottoResultAboutSixEquals = LottoResultFixture.FOUR_EQUALS_RESULT.getValue();
+
+            for (int i = 0; i < lottoAmount; i++) {
+                lottoResultAboutSixEquals.increaseLottoAmount();
+            }
+
+            int actualTotalPrizeAmount = lottoResultAboutSixEquals.getTotalPrizeAmount();
+            int expectedTotalPrizeAmount = LottoRank.FOUR_EQUALS.prizeAmount * lottoAmount;
+
+            assertThat(actualTotalPrizeAmount).isEqualTo(expectedTotalPrizeAmount);
+        }
+
+        @ParameterizedTest
+        @DisplayName("5개 일치 로또의 총 상금을 반환한다")
+        @ValueSource(ints = {1, 10, 100, 1000})
+        void getTotalPrizeAmountOfFiveEqualsLotto(int lottoAmount) {
+            LottoResult lottoResultAboutSixEquals = LottoResultFixture.FIVE_EQUALS_RESULT.getValue();
+
+            for (int i = 0; i < lottoAmount; i++) {
+                lottoResultAboutSixEquals.increaseLottoAmount();
+            }
+
+            int actualTotalPrizeAmount = lottoResultAboutSixEquals.getTotalPrizeAmount();
+            int expectedTotalPrizeAmount = LottoRank.FIVE_EQUALS.prizeAmount * lottoAmount;
+
+            assertThat(actualTotalPrizeAmount).isEqualTo(expectedTotalPrizeAmount);
+        }
+
+        @ParameterizedTest
+        @DisplayName("5개와 보너스 볼 일치 로또의 총 상금을 반환한다")
+        @ValueSource(ints = {1, 10, 100, 1000})
+        void getTotalPrizeAmountOfFiveEqualsWithBonusBallLotto(int lottoAmount) {
+            LottoResult lottoResultAboutSixEquals = LottoResultFixture.FIVE_WITH_BONUS_BALL_EQUALS_RESULT.getValue();
+
+            for (int i = 0; i < lottoAmount; i++) {
+                lottoResultAboutSixEquals.increaseLottoAmount();
+            }
+
+            int actualTotalPrizeAmount = lottoResultAboutSixEquals.getTotalPrizeAmount();
+            int expectedTotalPrizeAmount = LottoRank.FIVE_WITH_BONUS_EQUALS.prizeAmount * lottoAmount;
+
+            assertThat(actualTotalPrizeAmount).isEqualTo(expectedTotalPrizeAmount);
+        }
+
+        @ParameterizedTest
+        @DisplayName("6개 일치 로또의 총 상금을 반환한다")
+        @ValueSource(ints = {1, 10, 100, 1000})
+        void getTotalPrizeAmountOfSixEqualsLotto(int lottoAmount) {
+            LottoResult lottoResultAboutSixEquals = LottoResultFixture.SIX_EQUALS_RESULT.getValue();
+
+            for (int i = 0; i < lottoAmount; i++) {
+                lottoResultAboutSixEquals.increaseLottoAmount();
+            }
+
+            int actualTotalPrizeAmount = lottoResultAboutSixEquals.getTotalPrizeAmount();
+            int expectedTotalPrizeAmount = LottoRank.SIX_EQUALS.prizeAmount * lottoAmount;
+
+            assertThat(actualTotalPrizeAmount).isEqualTo(expectedTotalPrizeAmount);
+        }
+
+    }
+
+    private List<LottoRank> getSortedLottoRanks() {
+        return Arrays.stream(LottoRank.values())
+                .filter(lottoRank -> lottoRank != LottoRank.NO_PRIZE)
+                .sorted()
+                .toList();
+    }
+
+}
diff --git a/src/test/java/model/LottoResultsTest.java b/src/test/java/model/LottoResultsTest.java
new file mode 100644
index 000000000..9979f5745
--- /dev/null
+++ b/src/test/java/model/LottoResultsTest.java
@@ -0,0 +1,67 @@
+package model;
+
+import fixture.LottoNumbersFixture;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class LottoResultsTest {
+
+    private static final int PURCHASE_AMOUNT_FOR_ONE_LOTTO = 1_000;
+
+    private LottoNumbers winningLottoNumbers;
+    private List<LottoNumbersFixture> lottoNumbersFixtures;
+
+    @BeforeEach
+    void initTestData() {
+        winningLottoNumbers = LottoNumbersFixture.WINNING_LOTTO.getValue();
+        lottoNumbersFixtures = LottoNumbersFixture.getFixturesExcludeWinningLotto();
+    }
+
+    @Test
+    @DisplayName("로또 구매 가격 대비 총 수익률을 반환한다")
+    void getTotalProfitRate() {
+        for (LottoNumbersFixture lottoNumbersFixture : lottoNumbersFixtures) {
+            LottoRank expectedRank = lottoNumbersFixture.getExpectedRank();
+            LottoResults lottoResults = createLottoResultsFromLottoNumbersFixture(lottoNumbersFixture);
+
+            double actualTotalProfitRate = lottoResults.getTotalProfitRate(PURCHASE_AMOUNT_FOR_ONE_LOTTO);
+            double expectedTotalProfitRate = expectedRank.prizeAmount / (double) PURCHASE_AMOUNT_FOR_ONE_LOTTO;
+
+            assertThat(actualTotalProfitRate).isEqualTo(expectedTotalProfitRate);
+        }
+    }
+
+    @Test
+    @DisplayName("로또 당첨 결과를 담은 컬렉션을 반환한다")
+    void getLottoResultCollection() {
+        for (LottoNumbersFixture lottoNumbersFixture : lottoNumbersFixtures) {
+            LottoRank expectedRank = lottoNumbersFixture.getExpectedRank();
+            LottoResults lottoResults = createLottoResultsFromLottoNumbersFixture(lottoNumbersFixture);
+
+            LottoResult lottoResult = extractLottoResultByLottoRank(lottoResults, expectedRank);
+
+            assertThat(lottoResult.getLottoAmount()).isEqualTo(1);
+        }
+    }
+
+    private LottoResult extractLottoResultByLottoRank(LottoResults lottoResults, LottoRank lottoRank) {
+        return lottoResults.getLottoResultCollection()
+                .stream()
+                .filter(lottoResult -> lottoResult.getPrizeAmount() == lottoRank.prizeAmount)
+                .findAny()
+                .orElseThrow(() -> new IllegalStateException("lottoRank와 일치하는 대한 로또 결과가 없습니다."));
+    }
+
+    private LottoResults createLottoResultsFromLottoNumbersFixture(LottoNumbersFixture lottoNumbersFixture) {
+        List<LottoNumbers> lottoNumbersCollection = List.of(lottoNumbersFixture.getValue());
+        Lottos lottos = Lottos.purchase(PURCHASE_AMOUNT_FOR_ONE_LOTTO, lottoNumbersCollection);
+
+        return new LottoResults(lottos, winningLottoNumbers, LottoNumbersFixture.NOT_EQUALS_BONUS_BALL);
+    }
+
+}
diff --git a/src/test/java/model/LottosTest.java b/src/test/java/model/LottosTest.java
new file mode 100644
index 000000000..7a5fbf571
--- /dev/null
+++ b/src/test/java/model/LottosTest.java
@@ -0,0 +1,82 @@
+package model;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static model.LottoConstraints.LOTTO_PRICE;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class LottosTest {
+
+    private static final int PURCHASE_AMOUNT_FOR_FIVE_LOTTOS = LOTTO_PRICE * 5;
+    private static final int BASIC_MANUAL_LOTTO_AMOUNT = 5;
+    private static final List<LottoNumbers> BASIC_MANUAL_LOTTO_NUMBERS = createBasicManualLottoNumbers();
+    private static final List<LottoNumbers> EMPTY_MANUAL_LOTTO_NUMBERS = List.of();
+
+    private static List<LottoNumbers> createBasicManualLottoNumbers() {
+        List<LottoNumbers> basicManualLottoNumbers = new ArrayList<>();
+
+        for (int i = 0; i < BASIC_MANUAL_LOTTO_AMOUNT; i++) {
+            basicManualLottoNumbers.add(RandomLottoNumbersGenerator.getRandomLottoNumbers());
+        }
+
+        return Collections.unmodifiableList(basicManualLottoNumbers);
+    }
+
+    @ParameterizedTest
+    @DisplayName("구매 금액이 로또 가격보다 적으면 예외가 발생한다")
+    @ValueSource(ints = {Integer.MIN_VALUE, -99999, -1000, -1, 0, 999})
+    void ifPurchaseAmountLessThanLottoPriceThenThrowException(int illegalPurchaseAmount) {
+        assertThatThrownBy(() -> Lottos.purchase(illegalPurchaseAmount, EMPTY_MANUAL_LOTTO_NUMBERS))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @ParameterizedTest
+    @DisplayName("구매 가격이 로또 가격으로 나누어 떨어지지 않으면 예외가 발생한다")
+    @ValueSource(ints = {1001, 9999, 12345, 50001, Integer.MAX_VALUE})
+    void ifPurchaseAmountNotDivisibleThenThrowException(int illegalPurchaseAmount) {
+        assertThatThrownBy(() -> Lottos.purchase(illegalPurchaseAmount, BASIC_MANUAL_LOTTO_NUMBERS))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @ParameterizedTest
+    @DisplayName("구매 가능한 로또 수보다 많은 수동 로또를 발급하려 하면 예외가 발생한다")
+    @ValueSource(ints = {6, 10, 11, 100, 1000})
+    void ifWantToIssueTooManyManualLottoThenThrowException(int illegalManualLottoAmount) {
+        List<LottoNumbers> illegalManualLottoNumbers = createRandomLottoNumbersCollection(illegalManualLottoAmount);
+        assertThatThrownBy(() -> Lottos.purchase(PURCHASE_AMOUNT_FOR_FIVE_LOTTOS, illegalManualLottoNumbers));
+    }
+
+    @ParameterizedTest
+    @DisplayName("로또의 개수를 반환한다")
+    @ValueSource(ints = {1, 10, 100, 1000, 10000})
+    void getLottoAmount(int lottoAmount) {
+        Lottos lottos = createLottosFromLottoAmount(lottoAmount);
+
+        int actualLottoAmount = lottos.getLottoAmount();
+
+        assertThat(actualLottoAmount).isEqualTo(lottoAmount);
+    }
+
+    private List<LottoNumbers> createRandomLottoNumbersCollection(int lottoAmount) {
+        List<LottoNumbers> randomLottoNumbersCollection = new ArrayList<>();
+
+        for (int i = 0; i < lottoAmount; i++) {
+            randomLottoNumbersCollection.add(RandomLottoNumbersGenerator.getRandomLottoNumbers());
+        }
+
+        return Collections.unmodifiableList(randomLottoNumbersCollection);
+    }
+
+    private Lottos createLottosFromLottoAmount(int lottoAmount) {
+        int purchaseAmount = lottoAmount * LOTTO_PRICE;
+        return Lottos.purchase(purchaseAmount, EMPTY_MANUAL_LOTTO_NUMBERS);
+    }
+
+}


Reviews:
user: login: hangillee
body: 안녕하세요, 준! 👋
미션 하시느라 고생 많으셨습니다!

남겨주신 다른 질문에 대한 답은 코멘트로 남겨드렸습니다!

> - 리뷰어님은 어떤 기준으로 유틸 클래스와 모델 클래스를 구분하시는지 궁금합니다!

저의 기준이 궁금하신 것도 충분히 이해되지만 지금 우리가 하고 있는 미션과 스터디의 가장 큰 목적이 `나의 클린 코드 기준 세우기`임을 상기하셨으면 좋겠습니다.

제 생각이 정답이 아닐 뿐더러, 제 생각으로 인해 준의 생각의 한계가 결정되는 것도 원하지 않습니다! 😁 즉, 코드에 오답은 있어도 정답은 없습니다!

그래도 제 생각을 말씀드리자면, 핵심 도메인에 깊게 연관되거나 수정에 영향을 주고 받는다면 **도메인**(모델) 클래스, 도메인을 가리지 않고 어디서나 활용되는 클래스(ex. `Math`)를 **유틸** 클래스로 봅니다.

사실, 생각해보면 `RandomLottoNumbersGenerator`는 유틸이라기엔 `Lottos` 도메인 외에는 활용될 여지가 없지 않은가요? 😁

코멘트 확인해주시고, RC 남겨주세요! 파이팅! 💪
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-03-01T19:41:21Z
user: login: kjoon418
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-02T00:14:54Z
user: login: kjoon418
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-02T00:28:52Z
user: login: kjoon418
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-02T00:34:50Z
user: login: kjoon418
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-02T00:35:49Z
user: login: kjoon418
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-02T00:48:33Z
user: login: kjoon418
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-02T00:51:16Z
user: login: kjoon418
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-02T00:53:59Z
user: login: kjoon418
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-02T00:58:56Z
user: login: kjoon418
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-02T01:11:21Z
user: login: kjoon418
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-02T14:16:18Z
user: login: kjoon418
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-02T14:22:47Z
user: login: kjoon418
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-02T14:25:23Z
user: login: kjoon418
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-02T14:36:21Z
user: login: kjoon418
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-02T14:42:33Z
user: login: kjoon418
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-03T02:44:10Z
user: login: kjoon418
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-03T03:08:35Z
user: login: kjoon418
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-03T10:26:35Z
user: login: kjoon418
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-03T13:47:55Z
user: login: kjoon418
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-04T00:11:26Z
user: login: hangillee
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-05T00:12:44Z
user: login: hangillee
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-05T00:16:16Z
user: login: hangillee
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-05T00:16:54Z
user: login: hangillee
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-05T00:22:53Z
user: login: hangillee
body: 안녕하세요, 준! 👋
개강의 주가 다가오고 말았네요.. 이번 학기 파이팅입니다!

남겨주신 질문에 대해 답변 드리겠습니다!

> - assert를 하지 않는 의미 없는 테스트 코드'라고 조언해주셨는데, 모든 테스트 코드에 assert가 존재하는게 바람직한건지 궁금합니다.
>   - 코멘트를 달아 주신 테스트 코드의 경우, '올바른 방법으로 인스턴스를 생성할 경우 예외가 발생하지 않는다'를 검증하고 싶었습니다.
>   - @DisplayName으로 의도를 제대로 전달하면 괜찮을까요? 아니면 위 상황에 사용할 수 있는 더 적절한 방법이 존재하는건가요?

이에 대한 답은 코멘트로 남겨드렸습니다! 다시 한번 간단히 설명드리자면, `예외를 던지지 않음`을 검증해 정상적인 인스턴스화를 반증하면 됩니다! 😁

> - Test Fixture를 사용한게 처음이라 미숙한 부분이 많을 것 같습니다.
부적절하게 사용하고 있다면 조언해주시면 정말 감사하겠습니다!

훌륭하게 활용하셨습니다! 아직은 만들어 둔 Fixture를 모두 활용하진 않으신 것 같은데, 테스트 케이스를 추가해보면서 모든 경우에 대해 검사해보는 경험도 가져가보시면 좋을 것 같습니다! 😁

크게 고치실 부분은 없어서 한번 더 RC 주시면 머지하도록 하겠습니다!
미션 고생 많으셨어요! 마지막 미션도 파이팅! 💪
state: APPROVED
author_association: NONE
submitted_at: 2025-03-05T00:52:40Z
user: login: kjoon418
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-05T03:02:51Z
user: login: kjoon418
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-05T03:05:11Z
user: login: kjoon418
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-05T03:07:48Z
user: login: kjoon418
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-05T03:10:01Z
user: login: kjoon418
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-05T03:15:20Z
user: login: kjoon418
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-05T03:25:48Z
user: login: kjoon418
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-05T03:29:23Z
user: login: hangillee
state: APPROVED
author_association: NONE
submitted_at: 2025-03-05T08:35:28Z

Review comments:
diff_hunk: @@ -1,10 +1,12 @@
-import controller.LottoController;
+import config.LottoConfiguration;
 
 public class LottoApplication {
 
+    private static final LottoConfiguration lottoConfiguration = new LottoConfiguration();
+
     public static void main(String[] args) {
         try {
-            new LottoController().run();
+            lottoConfiguration.lottoController().run();
         } catch (Exception e) {
path: src/main/java/LottoApplication.java
user: login: hangillee
body: [디미터의 법칙](https://tecoble.techcourse.co.kr/post/2020-06-02-law-of-demeter/)을 준수할 순 없었을까요? 🤔
created_at: 2025-03-01T13:37:33Z
author_association: NONE
diff_hunk: @@ -0,0 +1,30 @@
+package config;
+
+import controller.LottoController;
+import util.InputConverter;
+import view.LottoInputView;
+import view.LottoOutputView;
+
+public class LottoConfiguration {
+
+    public LottoController lottoController() {
+        return new LottoController(
+                lottoInputView(),
+                lottoOutputView(),
+                inputConverter()
+        );
+    }
path: src/main/java/config/LottoConfiguration.java
user: login: hangillee
body: 제 생각에는 `Controller`와 `View`는 이 어플리케이션에서 단 하나만 존재해도 될 것 같은데 준은 어떻게 생각하시나요? 정적 팩토리 메소드여도 괜찮았을 것 같아요. 🤔
created_at: 2025-03-01T13:38:40Z
author_association: NONE
diff_hunk: @@ -1,31 +1,90 @@
 package controller;
 
-import model.Lotto;
-import dto.LottoDto;
-import model.LottoPurchase;
-import view.*;
-import java.util.*;
+import dto.LottoResultDto;
+import dto.LottosDto;
+import model.*;
+import util.InputConverter;
+import view.LottoInputView;
+import view.LottoOutputView;
+
+import java.text.DecimalFormat;
+import java.util.List;
 
 public class LottoController {
 
-    private final LottoInputView lottoInputView = new LottoInputView();
-    private final LottoOutputView lottoOutputView = new LottoOutputView();
-    private final LottoPurchase lottoPurchase = new LottoPurchase();
+    private static final String TOTAL_PROFIT_FORMAT_FORM = "0.00";
+    private static final DecimalFormat TOTAL_PROFIT_RATE_FORMATTER = new DecimalFormat(TOTAL_PROFIT_FORMAT_FORM);
+
+    private final LottoInputView lottoInputView;
+    private final LottoOutputView lottoOutputView;
+    private final InputConverter inputConverter;
+
+    public LottoController(
+            LottoInputView lottoInputView,
+            LottoOutputView lottoOutputView,
+            InputConverter inputConverter
+    ) {
+        this.lottoInputView = lottoInputView;
+        this.lottoOutputView = lottoOutputView;
+        this.inputConverter = inputConverter;
+    }
 
     public void run() {
         int purchaseAmount = lottoInputView.getPurchaseAmount();
-        lottoInputView.printEmptyLine();
+        Lottos lottos = purchaseLottos(purchaseAmount);
+
+        LottoNumbers winningLottoNumbers = getWinningLottoNumbers();
+        LottoNumber bonusBall = getBonusBall();
+
+        LottoResults lottoResults = new LottoResults(lottos, winningLottoNumbers, bonusBall);
+        printLottoResults(lottoResults);
+        printTotalProfitRate(lottoResults, purchaseAmount);
+    }
+
+    private Lottos purchaseLottos(int purchaseAmount) {
+        int manualLottoAmount = lottoInputView.getManualLottoAmount();
+
+        List<String> manualLottoNumbersInput = lottoInputView.getManualLottoNumbers(manualLottoAmount);
+        List<LottoNumbers> manualLottoNumbersList = inputConverter.getLottoNumbersListFromInputs(manualLottoNumbersInput);
path: src/main/java/controller/LottoController.java
user: login: hangillee
body: `InputConverter`가 존재해야 하는 이유를 잘 모르겠습니다.. 🤔 `View` 계층에서 입력된 로또 번호를 `split`하고 `Lottos` 같은 일급 컬렉션에서 `List<LottoNumbers>`를 직접 생성하도록 하는 것으로도 충분하지 않았을까요?

준이 생각한 중간 유틸 클래스가 등장하게 된 계기가 궁금합니다!
created_at: 2025-03-01T16:18:18Z
author_association: NONE
diff_hunk: @@ -0,0 +1,28 @@
+package dto;
+
+import model.LottoResult;
+
+public class LottoResultDto {
+
+    private final int equalCount;
+    private final int prizeAmount;
+    private final int lottoAmount;
+    private final boolean bonusBallEqual;
+
+    public LottoResultDto(LottoResult lottoResult) {
+        this.equalCount = lottoResult.getEqualCount();
+        this.prizeAmount = lottoResult.getPrizeAmount();
+        this.lottoAmount = lottoResult.getLottoAmount();
+        this.bonusBallEqual = lottoResult.isBonusBallResult();
+    }
+
+    @Override
+    public String toString() {
+        if (bonusBallEqual) {
+            return String.format("%d개 일치, 보너스 볼 일치 (%d원)- %d개", equalCount, prizeAmount, lottoAmount);
+        }
+
+        return String.format("%d개 일치 (%d원)- %d개", equalCount, prizeAmount, lottoAmount);
+    }
path: src/main/java/dto/LottoResultDto.java
user: login: hangillee
body: DTO에서 `View`의 요구 사항을 알아야 할 필요가 있을까요? 🤔 알게 된다면 무슨 문제가 발생할 수 있나요?
created_at: 2025-03-01T16:19:13Z
author_association: NONE
path: src/main/java/model/LottoRank.java
user: login: hangillee
body: 열거형 활용 좋네요! 👍
코드 파일 마지막에 [POSIX New Line](https://hyeon9mak.github.io/github-no-newline-at-a-end-of-file/)만 준수해줍시다!
created_at: 2025-03-01T16:30:49Z
author_association: NONE
diff_hunk: @@ -1,32 +1,51 @@
 package model;
 
-import java.util.*;
+import util.Counter;
+
+import java.util.Comparator;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import static model.LottoConstraints.LOTTO_SIZE;
 
 public class LottoNumbers {
 
-    private static final Comparator<LottoNumber> COMPARATOR = Comparator.comparingInt(LottoNumber::getValue);
-    private static final int LOTTO_SIZE = 6;
+    private static final Comparator<LottoNumber> NUMBER_COMPARATOR = Comparator.comparingInt(LottoNumber::value);
 
-    private final SortedSet<LottoNumber> lottoNumbers = new TreeSet<>(COMPARATOR);
+    private final SortedSet<LottoNumber> lottoNumbers;
 
-    private LottoNumbers(Set<LottoNumber> lottoNumbers) {
+    public LottoNumbers(Set<LottoNumber> lottoNumbers) {
         validateSize(lottoNumbers.size());
+        this.lottoNumbers = new TreeSet<>(NUMBER_COMPARATOR);
         this.lottoNumbers.addAll(lottoNumbers);
     }
 
-    public static LottoNumbers getRandomLottoNumbers() {
-        Set<LottoNumber> lottoNumbers = new HashSet<>();
+    public LottoNumbers getCopy() {
+        return new LottoNumbers(Set.copyOf(this.lottoNumbers));
+    }
 
-        while (isSmallThanMaxSize(lottoNumbers.size())) {
-            LottoNumber randomLottoNumber = LottoNumber.getRandomLottoNumber();
-            lottoNumbers.add(randomLottoNumber);
+    public int getEqualNumbersCount(LottoNumbers otherLottoNumbers) {
+        Counter counter = new Counter();
+
+        for (LottoNumber lottoNumber : this.lottoNumbers) {
+            counter.increaseIfTrue(() -> otherLottoNumbers.contains(lottoNumber));
         }
 
-        return new LottoNumbers(lottoNumbers);
+        return counter.getCount();
+    }
+
+    public boolean hasEqualNumberWithBonusBall(LottoNumber bonusBall) {
+        return this.lottoNumbers.contains(bonusBall);
+    }
+
+    @Override
+    public String toString() {
+        return lottoNumbers.toString();
     }
 
-    public SortedSet<LottoNumber> getCopy() {
-        return Collections.unmodifiableSortedSet(lottoNumbers);
+    private boolean contains(LottoNumber lottoNumber) {
+        return this.lottoNumbers.contains(lottoNumber);
     }
path: src/main/java/model/LottoNumbers.java
user: login: hangillee
body: 저는 `@Override`된 메소드는 보통 코드 최하단에 오는 편이 가독성 측면에서 낫다고 생각합니다!
전에 적어주셨던 기준대로 작성해주신 것은 이해하지만, 준이 직접 작성한 `private` 메소드들이
코드를 읽는 사람 입장에서는 훨씬 중요할 것 같아요. 😁

오버라이딩 된 메소드는 예측이 어느 정도 가능하지만, 직접 작성한 메소드는 그렇지 않으니까요!
created_at: 2025-03-01T16:33:39Z
author_association: NONE
diff_hunk: @@ -0,0 +1,68 @@
+package model;
+
+import util.RandomLottoNumbersGenerator;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static model.LottoConstraints.LOTTO_PRICE;
+
+public class Lottos {
+
+    private final List<LottoNumbers> lottoNumbersList;
path: src/main/java/model/Lottos.java
user: login: hangillee
body: 컬렉션의 이름이 변수명에 들어가는 것은 좋지 않습니다!
관련 [아티클](https://tecoble.techcourse.co.kr/post/2020-04-24-variable_naming/)입니다! 😁
created_at: 2025-03-01T17:46:55Z
author_association: NONE
diff_hunk: @@ -0,0 +1,19 @@
+package util;
+
+import java.util.function.Supplier;
+
+public class Counter {
+
+    private int count;
+
+    public void increaseIfTrue(Supplier<Boolean> conditionExpression) {
+        if (conditionExpression.get()) {
+            this.count++;
+        }
+    }
path: src/main/java/util/Counter.java
user: login: hangillee
body: 구매한 `Lotto`의 숫자와 당첨 번호의 숫자가 일치한다면 증가하는 메소드의 이름은 `increaseIfMatch`가 더 이해하기 쉽지 않을까요? 🤔 준의 생각은 어떤가요?

또한 이 클래스의 이름도 `LottoNumberMatcher` 쪽이 되면 더 이해하기 쉬울 것 같아요. 거기다 유틸 클래스라기보단 핵심 도메인 역할을 가지고 있다고 생각합니다. 로또 번호 맞추기만큼 핵심인 기능이 없지 않을까요?
created_at: 2025-03-01T18:52:06Z
author_association: NONE
diff_hunk: @@ -0,0 +1,59 @@
+package util;
+
+import model.LottoNumber;
+import model.LottoNumbers;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+public class InputConverter {
+
+    private static final String INPUT_SEPARATOR = "[\\s|,]";
+
+    public List<LottoNumbers> getLottoNumbersListFromInputs(List<String> lottoNumbersInputs) {
+        List<LottoNumbers> lottoNumbersList = new ArrayList<>();
+
+        for (String lottoNumbersInput : lottoNumbersInputs) {
+            String[] lottoNumberStrings = splitLottoNumbersInput(lottoNumbersInput);
+            Set<Integer> parsedIntegers = parseStringsToIntegers(lottoNumberStrings);
+            lottoNumbersList.add(createLottoNumbersFromIntegers(parsedIntegers));
+        }
+
+        return Collections.unmodifiableList(lottoNumbersList);
+    }
+
+    public LottoNumbers getLottoNumbersFromInput(String lottoNumbersInput) {
+        String[] lottoNumberStrings = splitLottoNumbersInput(lottoNumbersInput);
+        Set<Integer> parsedIntegers = parseStringsToIntegers(lottoNumberStrings);
+
+        return createLottoNumbersFromIntegers(parsedIntegers);
+    }
+
+    private String[] splitLottoNumbersInput(String winningLottoInput) {
+        return winningLottoInput.split(INPUT_SEPARATOR);
+    }
+
+    private Set<Integer> parseStringsToIntegers(String[] lottoNumberStrings) {
+        Set<Integer> parsedIntegers = new HashSet<>();
+
+        for (String lottoNumberString : lottoNumberStrings) {
+            int parsedNumber = Integer.parseInt(lottoNumberString);
+            parsedIntegers.add(parsedNumber);
+        }
+
+        return Collections.unmodifiableSet(parsedIntegers);
+    }
path: src/main/java/util/InputConverter.java
user: login: hangillee
body: ![image](https://github.com/user-attachments/assets/76d91f8c-33e1-4deb-b805-56ff972c6e27)

입력된 문자열이 제대로 파싱되고 있는 것 같지 않습니다. 요구 사항에서 안내한 입력대로 입력했지만 계속 예외가 발생하네요! 😢
created_at: 2025-03-01T18:56:40Z
author_association: NONE
diff_hunk: @@ -0,0 +1,10 @@
+package model;
+
+public class LottoConstraints {
+
+    public static final int LOTTO_PRICE = 1_000;
+    public static final int MINIMUM_LOTTO_NUMBER = 1;
+    public static final int MAXIMUM_LOTTO_NUMBER = 45;
+    public static final int LOTTO_SIZE = 6;
path: src/main/java/model/LottoConstraints.java
user: login: hangillee
body: 상수를 별도의 클래스로 관리하게 되었을 때 무슨 문제가 일어날 수 있을까요? 만약, 이 클래스에서 관리하는 상수가 수백 개가 넘어가면 혼동하지 않을 자신이 있을까요? 🤔 한 번 생각해봅시다!

물론 하나의 클래스로 관리하는 것 자체는 큰 문제가 없습니다! 굳이 수정하지 않으셔도 됩니다만, 이 클래스는 상수만 존재하기 때문에 생성자의 접근제어자를 `private`으로 닫아주는 편이 좋겠네요. 😁
created_at: 2025-03-01T19:12:32Z
author_association: NONE
diff_hunk: @@ -0,0 +1,41 @@
+package util;
+
+import model.LottoNumber;
+import model.LottoNumbers;
+
+import java.util.HashSet;
+import java.util.Random;
+import java.util.Set;
+
+import static model.LottoConstraints.*;
+
+public class RandomLottoNumbersGenerator {
+
+    private static final Random RANDOM = new Random();
+
+    public static LottoNumbers getRandomLottoNumbers() {
+        Set<LottoNumber> lottoNumbers = new HashSet<>();
+
+        while (smallThanMaxSize(lottoNumbers)) {
+            LottoNumber randomLottoNumber = createRandomLottoNumber();
+            lottoNumbers.add(randomLottoNumber);
+        }
+
+        return new LottoNumbers(lottoNumbers);
+    }
+
+    private static LottoNumber createRandomLottoNumber() {
+        int randomInteger = createRandomInteger();
+
+        return new LottoNumber(randomInteger);
+    }
+
+    private static int createRandomInteger() {
+        return RANDOM.nextInt(MINIMUM_LOTTO_NUMBER, MAXIMUM_LOTTO_NUMBER);
+    }
path: src/main/java/util/RandomLottoNumbersGenerator.java
user: login: hangillee
body: 사실, 상수를 하나의 클래스에서 관리했을 때의 문제가 **해당 상수가 활용되는 클래스에서 바로 값을 확인할 수 없다**는 것입니다. 😢

보통 다른 개발자가 **랜덤 로또 번호를 생성하는 로직을 수정**한다고 했을 때 이 클래스를 확인할텐데, 이 클래스만 읽어서는 `MINIMUM_LOTTO_NUMBER`, `MAXIMUM_LOTTO_NUMBER`가 무슨 값을 가지는지 바로 확인할 수 없겠죠..

물론 정답은 없습니다. 준이 생각하는 방식이 타당한 이유만 확실하면 됩니다.
created_at: 2025-03-01T19:16:33Z
author_association: NONE
diff_hunk: @@ -8,8 +11,56 @@ public class LottoInputView implements LottoView {
 
     public int getPurchaseAmount() {
         System.out.println("구입금액을 입력해 주세요.");
+        int purchaseAmountInput = Integer.parseInt(scanner.nextLine());
 
-        return scanner.nextInt();
+        printEmptyLine();
+
+        return purchaseAmountInput;
+    }
+
+    public int getManualLottoAmount() {
+        System.out.println("수동으로 구매할 로또 수를 입력해 주세요.");
+        int manualLottoAmountInput = Integer.parseInt(scanner.nextLine());
+
+        printEmptyLine();
+
+        return manualLottoAmountInput;
+    }
+
+    public List<String> getManualLottoNumbers(int manualLottoAmount) {
+        if (isNotPositive(manualLottoAmount)) {
+            return List.of();
+        }
path: src/main/java/view/LottoInputView.java
user: login: hangillee
body: 수동 구매할 로또의 개수를 0이나 음수로 입력했을 때는 예외가 발생하는 편이 맞지 않을까요? 준은 어떻게 생각하나요? 🤔
created_at: 2025-03-01T19:18:05Z
author_association: NONE
path: src/test/java/model/LottoNumberTest.java
user: login: hangillee
body: 모든 테스트 코드에 `EOL`이 없는 것 같습니다! 확인해주세요!
created_at: 2025-03-01T19:19:33Z
author_association: NONE
diff_hunk: @@ -0,0 +1,52 @@
+package model;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static model.LottoConstraints.MAXIMUM_LOTTO_NUMBER;
+import static model.LottoConstraints.MINIMUM_LOTTO_NUMBER;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class LottoNumberTest {
+
+    @Test
+    @DisplayName("적정 범위 내의 숫자를 통해 인스턴스를 생성한다")
+    void createLottoNumberByInteger() {
+        for (Integer legalNumber : getLegalNumbers()) {
+            LottoNumber lottoNumber = new LottoNumber(legalNumber);
+        }
+    }
path: src/test/java/model/LottoNumberTest.java
user: login: hangillee
body: 무엇을 검증하는 것인가요? 🤔 의미가 없는 테스트 코드 같아요. 생성 될 때 예외가 발생하지 않음을 검증하고 싶었던 걸까요?
created_at: 2025-03-01T19:22:44Z
author_association: NONE
diff_hunk: @@ -0,0 +1,52 @@
+package model;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static model.LottoConstraints.MAXIMUM_LOTTO_NUMBER;
+import static model.LottoConstraints.MINIMUM_LOTTO_NUMBER;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class LottoNumberTest {
+
+    @Test
+    @DisplayName("적정 범위 내의 숫자를 통해 인스턴스를 생성한다")
+    void createLottoNumberByInteger() {
+        for (Integer legalNumber : getLegalNumbers()) {
+            LottoNumber lottoNumber = new LottoNumber(legalNumber);
+        }
+    }
+
+    @ParameterizedTest
+    @DisplayName("최소치보다 작은 숫자를 전달시 예외가 발생한다")
+    @ValueSource(ints = {Integer.MIN_VALUE, -1000, -100, -10, -1, 0})
+    void ifLessThanMinimumThenThrowException(int illegalNumber) {
+        assertThatThrownBy(() -> new LottoNumber(illegalNumber))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @ParameterizedTest
+    @DisplayName("최대치보다 높은 숫자를 전달시 예외가 발생한다")
+    @ValueSource(ints = {46, 100, 999, 15000, Integer.MAX_VALUE})
+    void ifBiggerThanMaximumThenThrowException(int illegalNumber) {
+        assertThatThrownBy(() -> new LottoNumber(illegalNumber))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
path: src/test/java/model/LottoNumberTest.java
user: login: hangillee
body: 경계값 테스트 👍
created_at: 2025-03-01T19:23:04Z
author_association: NONE
diff_hunk: @@ -0,0 +1,87 @@
+package model;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static model.LottoConstraints.LOTTO_SIZE;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class LottoNumbersTest {
+
+    private static final int BASIC_MINIMUM_LOTTO_NUMBER = 1;
+    private static final String STRING_SEPARATOR = " ";
+
+    @Test
+    @DisplayName("LottoNumber 6개를 통해 인스턴스를 생성한다")
+    void createBySixLottoNumber() {
+        Set<LottoNumber> legalLottoNumberSet = getLegalLottoNumberSet();
+
+        LottoNumbers lottoNumbers = new LottoNumbers(legalLottoNumberSet);
+    }
path: src/test/java/model/LottoNumbersTest.java
user: login: hangillee
body: 마찬가지로 `assert`를 하지 않고 있는 코드로 의미 없는 테스트 코드 같습니다! 😢
created_at: 2025-03-01T19:23:38Z
author_association: NONE
diff_hunk: @@ -0,0 +1,87 @@
+package model;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static model.LottoConstraints.LOTTO_SIZE;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class LottoNumbersTest {
+
+    private static final int BASIC_MINIMUM_LOTTO_NUMBER = 1;
+    private static final String STRING_SEPARATOR = " ";
+
+    @Test
+    @DisplayName("LottoNumber 6개를 통해 인스턴스를 생성한다")
+    void createBySixLottoNumber() {
+        Set<LottoNumber> legalLottoNumberSet = getLegalLottoNumberSet();
+
+        LottoNumbers lottoNumbers = new LottoNumbers(legalLottoNumberSet);
+    }
+
+    @ParameterizedTest
+    @DisplayName("LottoNumber의 개수가 6개 미만이면 예외가 발생한다")
+    @CsvSource({
+            "20",
+            "1 45",
+            "10 20 30",
+            "11 22 33 44",
+            "1 2 3 4 5"
+    })
+    void ifLottoNumberCountLessThanSixThenThrowException(String numbersString) {
+        Set<LottoNumber> illegalLottoNumberSet = convertNumbersStringIntoLottoNumberSet(numbersString);
+
+        assertThatThrownBy(() -> new LottoNumbers(illegalLottoNumberSet))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @ParameterizedTest
+    @DisplayName("LottoNumber의 개수가 6개 초과면 예외가 발생한다")
+    @CsvSource({
+            "1 2 3 4 5 6 7",
+            "10 11 20 21 30 31 40 41",
+            "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15"
+    })
+    void ifLottoNumberCountBiggerThanSixThenThrowException(String numbersString) {
+        Set<LottoNumber> illegalLottoNumberSet = convertNumbersStringIntoLottoNumberSet(numbersString);
+
+        assertThatThrownBy(() -> new LottoNumbers(illegalLottoNumberSet))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    private Set<LottoNumber> getLegalLottoNumberSet() {
+        int lottoNumberValue = BASIC_MINIMUM_LOTTO_NUMBER;
+        HashSet<LottoNumber> lottoNumbers = new HashSet<>();
+
+        while (isSmallThanMaxSize(lottoNumbers)) {
+            LottoNumber lottoNumber = new LottoNumber(lottoNumberValue);
+            lottoNumbers.add(lottoNumber);
+
+            lottoNumberValue++;
+        }
+
+        return Collections.unmodifiableSet(lottoNumbers);
+    }
path: src/test/java/model/LottoNumbersTest.java
user: login: hangillee
body: 초기 테스트 데이터를 구축해두기 위해서라면 `Test Fixture`라는 개념을 도입해보는 것은 어떨까요? 이번 미션에서 `Enum`을 적극적으로 활용할 것을 권장하고 있는데, `Enum`을 활용한 `Test Fixture`라면 충분히 도입해볼 만한 것 같아요. 😁
created_at: 2025-03-01T19:26:40Z
author_association: NONE
path: src/test/java/model/LottoResultTest.java
user: login: hangillee
body: `LottoResult` 클래스의 모든 메소드가 검증되고 있나요? 테스트 커버리지가 절대적인 기준이 될 순 없지만, 테스트를 작성할 때 참고해보면 좋을 것 같아요! 😁 테스트 코드는 일종의 사용 설명서, 즉 API 문서의 역할도 하기 때문이에요.

![image](https://github.com/user-attachments/assets/4ff8086c-250d-4be4-829f-6811a97e2190)
created_at: 2025-03-01T19:30:53Z
author_association: NONE
diff_hunk: @@ -0,0 +1,68 @@
+package model;
+
+import util.RandomLottoNumbersGenerator;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static model.LottoConstraints.LOTTO_PRICE;
+
+public class Lottos {
+
+    private final List<LottoNumbers> lottoNumbersList;
path: src/main/java/model/Lottos.java
user: login: kjoon418
body: 좋은 아티클 공유 감사합니다!

사실 이번에 처음으로 일급 컬렉션을 사용하며, 위와 같은 상황에 고민이 많았습니다.
기존에는 `LottoNumber`의 컬렉션에 복수형을 사용해 `lotterNumbers`라는 네이밍을 사용했으나, 위와 같은 경우엔 `LottoNumbers`의 컬렉션이므로 복수형으로 해결할 수 없었습니다.

`lottoNumbersCollection`처럼 명명하는 것이 최선일까요?
리팩터링하며 다시 고민해 보겠습니다...!
created_at: 2025-03-02T00:14:54Z
author_association: NONE
path: src/test/java/model/LottoNumberTest.java
user: login: kjoon418
body: 인텔리제이의 `Ensure every saved file ends with a line break` 설정을 활성화 해 두어서 따로 확인하지 않았는데 무슨 이유에선지 `EOL`이 없군요...!

한번 더 확인하는 습관을 지니도록 하겠습니다. 피드백 감사합니다!
created_at: 2025-03-02T00:28:52Z
author_association: NONE
diff_hunk: @@ -0,0 +1,52 @@
+package model;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static model.LottoConstraints.MAXIMUM_LOTTO_NUMBER;
+import static model.LottoConstraints.MINIMUM_LOTTO_NUMBER;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class LottoNumberTest {
+
+    @Test
+    @DisplayName("적정 범위 내의 숫자를 통해 인스턴스를 생성한다")
+    void createLottoNumberByInteger() {
+        for (Integer legalNumber : getLegalNumbers()) {
+            LottoNumber lottoNumber = new LottoNumber(legalNumber);
+        }
+    }
path: src/test/java/model/LottoNumberTest.java
user: login: kjoon418
body: > 테스트 코드는 일종의 사용 설명서, 즉 API 문서의 역할도 하기 때문이에요.

다른 리뷰이의 PR을 보고 테스트 코드가 API 문서의 역할도 할 수 있도록 만들어보자고 생각했습니다.
그래서 `의도대로 인스턴스를 생성`하는 방법 역시 테스트 코드로 남기고자 했습니다.

또한, 1부터 45 범위 내의 숫자를 통해 인스턴스를 생성할 때는 예외가 발생하지 않는 것 역시 검증하고 싶었습니다.
이것은 불필요한 테스트인가요? 혹은 어떤 것을 테스트하는지 의도를 제대로 표현하지 않은 것이 문제일까요?

피드백 감사합니다!
created_at: 2025-03-02T00:34:50Z
author_association: NONE
diff_hunk: @@ -0,0 +1,87 @@
+package model;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static model.LottoConstraints.LOTTO_SIZE;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class LottoNumbersTest {
+
+    private static final int BASIC_MINIMUM_LOTTO_NUMBER = 1;
+    private static final String STRING_SEPARATOR = " ";
+
+    @Test
+    @DisplayName("LottoNumber 6개를 통해 인스턴스를 생성한다")
+    void createBySixLottoNumber() {
+        Set<LottoNumber> legalLottoNumberSet = getLegalLottoNumberSet();
+
+        LottoNumbers lottoNumbers = new LottoNumbers(legalLottoNumberSet);
+    }
path: src/test/java/model/LottoNumbersTest.java
user: login: kjoon418
body: 궁금증이 있어 위에 남겨주신 리뷰에 추가 질문 남겼습니다.
감사합니다!
created_at: 2025-03-02T00:35:49Z
author_association: NONE
diff_hunk: @@ -0,0 +1,41 @@
+package util;
+
+import model.LottoNumber;
+import model.LottoNumbers;
+
+import java.util.HashSet;
+import java.util.Random;
+import java.util.Set;
+
+import static model.LottoConstraints.*;
+
+public class RandomLottoNumbersGenerator {
+
+    private static final Random RANDOM = new Random();
+
+    public static LottoNumbers getRandomLottoNumbers() {
+        Set<LottoNumber> lottoNumbers = new HashSet<>();
+
+        while (smallThanMaxSize(lottoNumbers)) {
+            LottoNumber randomLottoNumber = createRandomLottoNumber();
+            lottoNumbers.add(randomLottoNumber);
+        }
+
+        return new LottoNumbers(lottoNumbers);
+    }
+
+    private static LottoNumber createRandomLottoNumber() {
+        int randomInteger = createRandomInteger();
+
+        return new LottoNumber(randomInteger);
+    }
+
+    private static int createRandomInteger() {
+        return RANDOM.nextInt(MINIMUM_LOTTO_NUMBER, MAXIMUM_LOTTO_NUMBER);
+    }
path: src/main/java/util/RandomLottoNumbersGenerator.java
user: login: kjoon418
body: 확실히 그런 부작용이 발생하겠군요...!

로또가 1 ~ 45 범위의 숫자로 구성된다는 사실은 쉽게 변하지 않으니 `MINIMUM_LOTTO_NUMBER`와 `MAXIMUM_LOTTO_NUMBER`을 각 클래스에 중복해 선언해도 괜찮을 것 같기도 하지만, 이후 요구사항이 변경된다면 여러 클래스를 수정해야 해서 번거롭고 실수가 발생하기 쉬워질 것 같습니다.

우선은 현재의 방식을 사용하되, 이 방법이 어떤 단점을 지니고 있는지를 염두에 두고 사용하도록 하겠습니다.
피드백 감사합니다!
created_at: 2025-03-02T00:48:33Z
author_association: NONE
diff_hunk: @@ -8,8 +11,56 @@ public class LottoInputView implements LottoView {
 
     public int getPurchaseAmount() {
         System.out.println("구입금액을 입력해 주세요.");
+        int purchaseAmountInput = Integer.parseInt(scanner.nextLine());
 
-        return scanner.nextInt();
+        printEmptyLine();
+
+        return purchaseAmountInput;
+    }
+
+    public int getManualLottoAmount() {
+        System.out.println("수동으로 구매할 로또 수를 입력해 주세요.");
+        int manualLottoAmountInput = Integer.parseInt(scanner.nextLine());
+
+        printEmptyLine();
+
+        return manualLottoAmountInput;
+    }
+
+    public List<String> getManualLottoNumbers(int manualLottoAmount) {
+        if (isNotPositive(manualLottoAmount)) {
+            return List.of();
+        }
path: src/main/java/view/LottoInputView.java
user: login: kjoon418
body: 모든 로또를 자동으로 구입하고 싶을 수도 있으니 0은 넘어갈 수 있지만, 음수의 경우엔 확실히 예외 처리해 주는 것이 명확하겠군요.
혹은 수동 로또 구입 개수가 0일 때는 컨트롤러 단에서 해당 메서드를 호출하지 않는 것이 좋은 방향인 것 같기도 합니다.

고민한 후 적절하게 코드를 수정하도록 하겠습니다.
피드백 감사합니다!
created_at: 2025-03-02T00:51:16Z
author_association: NONE
path: src/main/java/model/LottoRank.java
user: login: kjoon418
body: 인텔리제이의 자동 설정에만 의존하지 않고 스스로 한번 더 확인하는 습관 지니도록 하겠습니다.

피드백 감사합니다!
created_at: 2025-03-02T00:53:59Z
author_association: NONE
diff_hunk: @@ -0,0 +1,30 @@
+package config;
+
+import controller.LottoController;
+import util.InputConverter;
+import view.LottoInputView;
+import view.LottoOutputView;
+
+public class LottoConfiguration {
+
+    public LottoController lottoController() {
+        return new LottoController(
+                lottoInputView(),
+                lottoOutputView(),
+                inputConverter()
+        );
+    }
path: src/main/java/config/LottoConfiguration.java
user: login: kjoon418
body: 의존성 주입을 사용하자고 마음 먹었을 때, 기존에 사용하던 Spring의 `Configuration` 객체가 가장 먼저 생각났던 것 같습니다.

비슷한 구조로 클래스를 만들었는데, 현재 프로젝트엔 해당 인스턴스들을 빈으로 등록하고 싱글톤 패턴을 사용하도록 하는 장치가 존재하지 않다는 것을 간과했네요.

`Controller`와 `View`가 애플리케이션 내에 하나만 존재하도록 코드를 수정하도록 하겠습니다.
피드백 감사합니다!
created_at: 2025-03-02T00:58:56Z
author_association: NONE
diff_hunk: @@ -1,10 +1,12 @@
-import controller.LottoController;
+import config.LottoConfiguration;
 
 public class LottoApplication {
 
+    private static final LottoConfiguration lottoConfiguration = new LottoConfiguration();
+
     public static void main(String[] args) {
         try {
-            new LottoController().run();
+            lottoConfiguration.lottoController().run();
         } catch (Exception e) {
path: src/main/java/LottoApplication.java
user: login: kjoon418
body: 한 줄로 간결하게 표현하고자 메서드 체이닝을 사용했는데, 이것이 디미터의 법칙을 위배하고 있군요..!

이후로는 디미터의 법칙을 인지하고 코드를 작성하도록 하겠습니다.
피드백 감사합니다!
created_at: 2025-03-02T01:11:21Z
author_association: NONE
diff_hunk: @@ -1,31 +1,90 @@
 package controller;
 
-import model.Lotto;
-import dto.LottoDto;
-import model.LottoPurchase;
-import view.*;
-import java.util.*;
+import dto.LottoResultDto;
+import dto.LottosDto;
+import model.*;
+import util.InputConverter;
+import view.LottoInputView;
+import view.LottoOutputView;
+
+import java.text.DecimalFormat;
+import java.util.List;
 
 public class LottoController {
 
-    private final LottoInputView lottoInputView = new LottoInputView();
-    private final LottoOutputView lottoOutputView = new LottoOutputView();
-    private final LottoPurchase lottoPurchase = new LottoPurchase();
+    private static final String TOTAL_PROFIT_FORMAT_FORM = "0.00";
+    private static final DecimalFormat TOTAL_PROFIT_RATE_FORMATTER = new DecimalFormat(TOTAL_PROFIT_FORMAT_FORM);
+
+    private final LottoInputView lottoInputView;
+    private final LottoOutputView lottoOutputView;
+    private final InputConverter inputConverter;
+
+    public LottoController(
+            LottoInputView lottoInputView,
+            LottoOutputView lottoOutputView,
+            InputConverter inputConverter
+    ) {
+        this.lottoInputView = lottoInputView;
+        this.lottoOutputView = lottoOutputView;
+        this.inputConverter = inputConverter;
+    }
 
     public void run() {
         int purchaseAmount = lottoInputView.getPurchaseAmount();
-        lottoInputView.printEmptyLine();
+        Lottos lottos = purchaseLottos(purchaseAmount);
+
+        LottoNumbers winningLottoNumbers = getWinningLottoNumbers();
+        LottoNumber bonusBall = getBonusBall();
+
+        LottoResults lottoResults = new LottoResults(lottos, winningLottoNumbers, bonusBall);
+        printLottoResults(lottoResults);
+        printTotalProfitRate(lottoResults, purchaseAmount);
+    }
+
+    private Lottos purchaseLottos(int purchaseAmount) {
+        int manualLottoAmount = lottoInputView.getManualLottoAmount();
+
+        List<String> manualLottoNumbersInput = lottoInputView.getManualLottoNumbers(manualLottoAmount);
+        List<LottoNumbers> manualLottoNumbersList = inputConverter.getLottoNumbersListFromInputs(manualLottoNumbersInput);
path: src/main/java/controller/LottoController.java
user: login: kjoon418
body: 제가 `View` 계층의 역할을 오해했기에 중간 유틸 클래스가 필요했던 것 같습니다.
`View는 입출력 역할에 충실해야 하기에 데이터를 가공하면 안된다`라 생각해 `View`는 입력된 로또 번호를 단순히 반환하기만 하고, 그 입력값을 `split`하는 코드를 `View` 외부에 두고자 했습니다.

이 과정에서 날것의 입력값을 가공(`String` -> `String[]` -> `LottoNumbers`)할 클래스의 필요성을 느껴 그것을 `InputConverter`가 담당하도록 설계했습니다.

말씀하신 것 처럼 입력값을 `split`하는 것까진 `View`에서 담당하는 것이 더욱 직관적인 것 같네요.
그렇게 하면 `Lottos`에서 `List<LottoNumbers>`를 직접 생성하는 것이 자연스러워질 것 같습니다.
말씀하신대로 코드를 개선해보도록 하겠습니다.
피드백 감사합니다!
created_at: 2025-03-02T14:16:18Z
author_association: NONE
diff_hunk: @@ -1,32 +1,51 @@
 package model;
 
-import java.util.*;
+import util.Counter;
+
+import java.util.Comparator;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import static model.LottoConstraints.LOTTO_SIZE;
 
 public class LottoNumbers {
 
-    private static final Comparator<LottoNumber> COMPARATOR = Comparator.comparingInt(LottoNumber::getValue);
-    private static final int LOTTO_SIZE = 6;
+    private static final Comparator<LottoNumber> NUMBER_COMPARATOR = Comparator.comparingInt(LottoNumber::value);
 
-    private final SortedSet<LottoNumber> lottoNumbers = new TreeSet<>(COMPARATOR);
+    private final SortedSet<LottoNumber> lottoNumbers;
 
-    private LottoNumbers(Set<LottoNumber> lottoNumbers) {
+    public LottoNumbers(Set<LottoNumber> lottoNumbers) {
         validateSize(lottoNumbers.size());
+        this.lottoNumbers = new TreeSet<>(NUMBER_COMPARATOR);
         this.lottoNumbers.addAll(lottoNumbers);
     }
 
-    public static LottoNumbers getRandomLottoNumbers() {
-        Set<LottoNumber> lottoNumbers = new HashSet<>();
+    public LottoNumbers getCopy() {
+        return new LottoNumbers(Set.copyOf(this.lottoNumbers));
+    }
 
-        while (isSmallThanMaxSize(lottoNumbers.size())) {
-            LottoNumber randomLottoNumber = LottoNumber.getRandomLottoNumber();
-            lottoNumbers.add(randomLottoNumber);
+    public int getEqualNumbersCount(LottoNumbers otherLottoNumbers) {
+        Counter counter = new Counter();
+
+        for (LottoNumber lottoNumber : this.lottoNumbers) {
+            counter.increaseIfTrue(() -> otherLottoNumbers.contains(lottoNumber));
         }
 
-        return new LottoNumbers(lottoNumbers);
+        return counter.getCount();
+    }
+
+    public boolean hasEqualNumberWithBonusBall(LottoNumber bonusBall) {
+        return this.lottoNumbers.contains(bonusBall);
+    }
+
+    @Override
+    public String toString() {
+        return lottoNumbers.toString();
     }
 
-    public SortedSet<LottoNumber> getCopy() {
-        return Collections.unmodifiableSortedSet(lottoNumbers);
+    private boolean contains(LottoNumber lottoNumber) {
+        return this.lottoNumbers.contains(lottoNumber);
     }
path: src/main/java/model/LottoNumbers.java
user: login: kjoon418
body: 말씀하신대로, 저는 외부에서 사용되는 `public` 메소드들을 위에 배치하는 것이 좋다 생각했습니다.
하지만 오버라이딩 된 메소드는 어떤 동작을 할지 예측하기 쉬우므로, 제가 직접 작성한 메서드에 비해 중요도가 떨어지겠군요.

결국 이런 배치는 다른 사람이 제 코드를 읽기 쉽게 만들기 위함이란 것을 간과했던 것 같습니다.
단순히 접근 지정자로만 분류하지 않고, 위와 같이 다양한 요소를 고려하여 고민하도록 하겠습니다.

좋은 조언 감사합니다!
created_at: 2025-03-02T14:22:47Z
author_association: NONE
diff_hunk: @@ -0,0 +1,19 @@
+package util;
+
+import java.util.function.Supplier;
+
+public class Counter {
+
+    private int count;
+
+    public void increaseIfTrue(Supplier<Boolean> conditionExpression) {
+        if (conditionExpression.get()) {
+            this.count++;
+        }
+    }
path: src/main/java/util/Counter.java
user: login: kjoon418
body: 처음에는 단순히 `조건이 true일 경우 값을 1 증가시키는 카운터`를 만들고자 하였기에, 이를 로또 이외에도 범용적으로 사용할 수 있는 유틸 클래스로 만들었습니다.

하지만 현재 해당 클래스는 '로또 번호 맞추기'라는 핵심 기능을 담당하고 있으므로, 유틸 클래스로 분류하지 않고 핵심 도메인 클래스로 분류하는 것이 더 나을 수 있겠군요!
그에 따라 말씀하신 것 처럼 네이밍을 같이 수정하도록 하겠습니다.

피드백 감사합니다!
created_at: 2025-03-02T14:25:23Z
author_association: NONE
diff_hunk: @@ -0,0 +1,59 @@
+package util;
+
+import model.LottoNumber;
+import model.LottoNumbers;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+public class InputConverter {
+
+    private static final String INPUT_SEPARATOR = "[\\s|,]";
+
+    public List<LottoNumbers> getLottoNumbersListFromInputs(List<String> lottoNumbersInputs) {
+        List<LottoNumbers> lottoNumbersList = new ArrayList<>();
+
+        for (String lottoNumbersInput : lottoNumbersInputs) {
+            String[] lottoNumberStrings = splitLottoNumbersInput(lottoNumbersInput);
+            Set<Integer> parsedIntegers = parseStringsToIntegers(lottoNumberStrings);
+            lottoNumbersList.add(createLottoNumbersFromIntegers(parsedIntegers));
+        }
+
+        return Collections.unmodifiableList(lottoNumbersList);
+    }
+
+    public LottoNumbers getLottoNumbersFromInput(String lottoNumbersInput) {
+        String[] lottoNumberStrings = splitLottoNumbersInput(lottoNumbersInput);
+        Set<Integer> parsedIntegers = parseStringsToIntegers(lottoNumberStrings);
+
+        return createLottoNumbersFromIntegers(parsedIntegers);
+    }
+
+    private String[] splitLottoNumbersInput(String winningLottoInput) {
+        return winningLottoInput.split(INPUT_SEPARATOR);
+    }
+
+    private Set<Integer> parseStringsToIntegers(String[] lottoNumberStrings) {
+        Set<Integer> parsedIntegers = new HashSet<>();
+
+        for (String lottoNumberString : lottoNumberStrings) {
+            int parsedNumber = Integer.parseInt(lottoNumberString);
+            parsedIntegers.add(parsedNumber);
+        }
+
+        return Collections.unmodifiableSet(parsedIntegers);
+    }
path: src/main/java/util/InputConverter.java
user: login: kjoon418
body: 요구사항에서는 `, `로 입력값을 나누고 있지만, `1,2,3,4,5,6` 혹은 `1 2 3 4 5 6` 혹은 `1, 2, 3, 4, 5, 6`과 같은 입력을 모두 처리할 수 있도록 정규 표현식을 사용했습니다.
그러나 정규 표현식을 잘못 사용해 `, `로 숫자를 구분할 경우 제대로 동작하지 않고 있네요...!
분명 요구 사항대로 입력받는 것도 테스트해본 것 같은데 착각한 것 같습니다.

실수 죄송합니다. 관련 코드를 수정하도록 하겠습니다!
created_at: 2025-03-02T14:36:21Z
author_association: NONE
path: src/test/java/model/LottoNumberTest.java
user: login: kjoon418
body: 다시 확인해 보니 해당 설정이 꺼져 있어서 발생한 문제였습니다.
이전에 설정을 수정하며 잘못 변경한 것 같습니다.
이후에는 이와 같은 문제가 생기지 않도록 하겠습니다!
created_at: 2025-03-02T14:42:33Z
author_association: NONE
diff_hunk: @@ -0,0 +1,28 @@
+package dto;
+
+import model.LottoResult;
+
+public class LottoResultDto {
+
+    private final int equalCount;
+    private final int prizeAmount;
+    private final int lottoAmount;
+    private final boolean bonusBallEqual;
+
+    public LottoResultDto(LottoResult lottoResult) {
+        this.equalCount = lottoResult.getEqualCount();
+        this.prizeAmount = lottoResult.getPrizeAmount();
+        this.lottoAmount = lottoResult.getLottoAmount();
+        this.bonusBallEqual = lottoResult.isBonusBallResult();
+    }
+
+    @Override
+    public String toString() {
+        if (bonusBallEqual) {
+            return String.format("%d개 일치, 보너스 볼 일치 (%d원)- %d개", equalCount, prizeAmount, lottoAmount);
+        }
+
+        return String.format("%d개 일치 (%d원)- %d개", equalCount, prizeAmount, lottoAmount);
+    }
path: src/main/java/dto/LottoResultDto.java
user: login: kjoon418
body: 해당 DTO를 유연하게 사용하지 못하고, 오직 특정 `View`에서만 사용하게 될 것 같습니다.
클래스의 유연성을 고려하며 개선하도록 하겠습니다.
피드백 감사합니다!
created_at: 2025-03-03T02:44:10Z
author_association: NONE
diff_hunk: @@ -0,0 +1,10 @@
+package model;
+
+public class LottoConstraints {
+
+    public static final int LOTTO_PRICE = 1_000;
+    public static final int MINIMUM_LOTTO_NUMBER = 1;
+    public static final int MAXIMUM_LOTTO_NUMBER = 45;
+    public static final int LOTTO_SIZE = 6;
path: src/main/java/model/LottoConstraints.java
user: login: kjoon418
body: 확실히 현재는 상수가 4개뿐이니 괜찮지만, 이후에 애플리케이션의 규모가 커져 `LottoContraints`가 관리할 상수가 많아진다면 혼동하기 쉽겠군요...!
트레이드 오프를 고려하며 상황에 맞는 방법을 찾아야겠다는 생각이 드네요.

제가 놓치고 있던 단점들을 알려주셔서 정말 감사합니다!
생성자의 접근제어자 역시 `private`으로 닫아 놓도록 하겠습니다!
created_at: 2025-03-03T03:08:35Z
author_association: NONE
path: src/test/java/model/LottoResultTest.java
user: login: kjoon418
body: 제 테스트 코드가 현재 프로젝트 코드를 얼마나 사용하고 있는지를 백분율로 확인할 수 있는 기능이군요...!
이런 좋은 기능을 소개해 주셔서 정말 감사합니다.
충분한 테스트 코드가 작성되었는지 확인할 때 참고하도록 하겠습니다!
created_at: 2025-03-03T10:26:35Z
author_association: NONE
diff_hunk: @@ -0,0 +1,28 @@
+package dto;
+
+import model.LottoResult;
+
+public class LottoResultDto {
+
+    private final int equalCount;
+    private final int prizeAmount;
+    private final int lottoAmount;
+    private final boolean bonusBallEqual;
+
+    public LottoResultDto(LottoResult lottoResult) {
+        this.equalCount = lottoResult.getEqualCount();
+        this.prizeAmount = lottoResult.getPrizeAmount();
+        this.lottoAmount = lottoResult.getLottoAmount();
+        this.bonusBallEqual = lottoResult.isBonusBallResult();
+    }
+
+    @Override
+    public String toString() {
+        if (bonusBallEqual) {
+            return String.format("%d개 일치, 보너스 볼 일치 (%d원)- %d개", equalCount, prizeAmount, lottoAmount);
+        }
+
+        return String.format("%d개 일치 (%d원)- %d개", equalCount, prizeAmount, lottoAmount);
+    }
path: src/main/java/dto/LottoResultDto.java
user: login: kjoon418
body: 추가로, 다른 개발자 입장에서는 DTO가 단순히 데이터 전달 목적으로만 사용될 것이라고 예상하니, 코드를 이해하기 어려워지겠군요.
DTO 본연의 역할에만 충실하도록 만들겠습니다!
created_at: 2025-03-03T13:47:55Z
author_association: NONE
diff_hunk: @@ -0,0 +1,87 @@
+package model;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static model.LottoConstraints.LOTTO_SIZE;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class LottoNumbersTest {
+
+    private static final int BASIC_MINIMUM_LOTTO_NUMBER = 1;
+    private static final String STRING_SEPARATOR = " ";
+
+    @Test
+    @DisplayName("LottoNumber 6개를 통해 인스턴스를 생성한다")
+    void createBySixLottoNumber() {
+        Set<LottoNumber> legalLottoNumberSet = getLegalLottoNumberSet();
+
+        LottoNumbers lottoNumbers = new LottoNumbers(legalLottoNumberSet);
+    }
+
+    @ParameterizedTest
+    @DisplayName("LottoNumber의 개수가 6개 미만이면 예외가 발생한다")
+    @CsvSource({
+            "20",
+            "1 45",
+            "10 20 30",
+            "11 22 33 44",
+            "1 2 3 4 5"
+    })
+    void ifLottoNumberCountLessThanSixThenThrowException(String numbersString) {
+        Set<LottoNumber> illegalLottoNumberSet = convertNumbersStringIntoLottoNumberSet(numbersString);
+
+        assertThatThrownBy(() -> new LottoNumbers(illegalLottoNumberSet))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @ParameterizedTest
+    @DisplayName("LottoNumber의 개수가 6개 초과면 예외가 발생한다")
+    @CsvSource({
+            "1 2 3 4 5 6 7",
+            "10 11 20 21 30 31 40 41",
+            "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15"
+    })
+    void ifLottoNumberCountBiggerThanSixThenThrowException(String numbersString) {
+        Set<LottoNumber> illegalLottoNumberSet = convertNumbersStringIntoLottoNumberSet(numbersString);
+
+        assertThatThrownBy(() -> new LottoNumbers(illegalLottoNumberSet))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    private Set<LottoNumber> getLegalLottoNumberSet() {
+        int lottoNumberValue = BASIC_MINIMUM_LOTTO_NUMBER;
+        HashSet<LottoNumber> lottoNumbers = new HashSet<>();
+
+        while (isSmallThanMaxSize(lottoNumbers)) {
+            LottoNumber lottoNumber = new LottoNumber(lottoNumberValue);
+            lottoNumbers.add(lottoNumber);
+
+            lottoNumberValue++;
+        }
+
+        return Collections.unmodifiableSet(lottoNumbers);
+    }
path: src/test/java/model/LottoNumbersTest.java
user: login: kjoon418
body: 처음 알게 된 개념이라 몇 개의 관련 포스팅을 참고하며 학습했습니다.
`Enum`을 활용한 `Test Fixture`가 상당히 흥미롭네요.
아직 숙련도가 낮지만 자주 활용하며 체화하도록 하겠습니다.

피드백 감사합니다!
created_at: 2025-03-04T00:11:26Z
author_association: NONE
diff_hunk: @@ -1,10 +1,12 @@
-import controller.LottoController;
+import config.LottoConfiguration;
 
 public class LottoApplication {
 
+    private static final LottoConfiguration lottoConfiguration = new LottoConfiguration();
+
     public static void main(String[] args) {
         try {
-            new LottoController().run();
+            lottoConfiguration.lottoController().run();
         } catch (Exception e) {
path: src/main/java/LottoApplication.java
user: login: hangillee
body: 메소드 체이닝을 활용했다면 차라리 개행하는 편이 낫습니다! 의도가 있는 코드였다니 좋네요! 👍
created_at: 2025-03-05T00:12:44Z
author_association: NONE
diff_hunk: @@ -1,31 +1,90 @@
 package controller;
 
-import model.Lotto;
-import dto.LottoDto;
-import model.LottoPurchase;
-import view.*;
-import java.util.*;
+import dto.LottoResultDto;
+import dto.LottosDto;
+import model.*;
+import util.InputConverter;
+import view.LottoInputView;
+import view.LottoOutputView;
+
+import java.text.DecimalFormat;
+import java.util.List;
 
 public class LottoController {
 
-    private final LottoInputView lottoInputView = new LottoInputView();
-    private final LottoOutputView lottoOutputView = new LottoOutputView();
-    private final LottoPurchase lottoPurchase = new LottoPurchase();
+    private static final String TOTAL_PROFIT_FORMAT_FORM = "0.00";
+    private static final DecimalFormat TOTAL_PROFIT_RATE_FORMATTER = new DecimalFormat(TOTAL_PROFIT_FORMAT_FORM);
+
+    private final LottoInputView lottoInputView;
+    private final LottoOutputView lottoOutputView;
+    private final InputConverter inputConverter;
+
+    public LottoController(
+            LottoInputView lottoInputView,
+            LottoOutputView lottoOutputView,
+            InputConverter inputConverter
+    ) {
+        this.lottoInputView = lottoInputView;
+        this.lottoOutputView = lottoOutputView;
+        this.inputConverter = inputConverter;
+    }
 
     public void run() {
         int purchaseAmount = lottoInputView.getPurchaseAmount();
-        lottoInputView.printEmptyLine();
+        Lottos lottos = purchaseLottos(purchaseAmount);
+
+        LottoNumbers winningLottoNumbers = getWinningLottoNumbers();
+        LottoNumber bonusBall = getBonusBall();
+
+        LottoResults lottoResults = new LottoResults(lottos, winningLottoNumbers, bonusBall);
+        printLottoResults(lottoResults);
+        printTotalProfitRate(lottoResults, purchaseAmount);
+    }
+
+    private Lottos purchaseLottos(int purchaseAmount) {
+        int manualLottoAmount = lottoInputView.getManualLottoAmount();
+
+        List<String> manualLottoNumbersInput = lottoInputView.getManualLottoNumbers(manualLottoAmount);
+        List<LottoNumbers> manualLottoNumbersList = inputConverter.getLottoNumbersListFromInputs(manualLottoNumbersInput);
path: src/main/java/controller/LottoController.java
user: login: hangillee
body: 준의 생각은 제 생각에도 타당한 것 같습니다! 사실 도메인의 요구 사항이 View에 녹아 있는 것이 좋지 않을 수도 있습니다. 다만, 로또 같이 간단한 서비스에서는 전 최대한 관리 포인트를 줄이는 편이 유지 보수하기 쉽다고 생각해서 제 스타일을 말씀드린 겁니다! 😁
created_at: 2025-03-05T00:16:16Z
author_association: NONE
diff_hunk: @@ -0,0 +1,28 @@
+package dto;
+
+import model.LottoResult;
+
+public class LottoResultDto {
+
+    private final int equalCount;
+    private final int prizeAmount;
+    private final int lottoAmount;
+    private final boolean bonusBallEqual;
+
+    public LottoResultDto(LottoResult lottoResult) {
+        this.equalCount = lottoResult.getEqualCount();
+        this.prizeAmount = lottoResult.getPrizeAmount();
+        this.lottoAmount = lottoResult.getLottoAmount();
+        this.bonusBallEqual = lottoResult.isBonusBallResult();
+    }
+
+    @Override
+    public String toString() {
+        if (bonusBallEqual) {
+            return String.format("%d개 일치, 보너스 볼 일치 (%d원)- %d개", equalCount, prizeAmount, lottoAmount);
+        }
+
+        return String.format("%d개 일치 (%d원)- %d개", equalCount, prizeAmount, lottoAmount);
+    }
path: src/main/java/dto/LottoResultDto.java
user: login: hangillee
body: 다른 개발자의 입장에서 생각해보는 습관 훌륭합니다! 👍
created_at: 2025-03-05T00:16:54Z
author_association: NONE
diff_hunk: @@ -0,0 +1,52 @@
+package model;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static model.LottoConstraints.MAXIMUM_LOTTO_NUMBER;
+import static model.LottoConstraints.MINIMUM_LOTTO_NUMBER;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class LottoNumberTest {
+
+    @Test
+    @DisplayName("적정 범위 내의 숫자를 통해 인스턴스를 생성한다")
+    void createLottoNumberByInteger() {
+        for (Integer legalNumber : getLegalNumbers()) {
+            LottoNumber lottoNumber = new LottoNumber(legalNumber);
+        }
+    }
path: src/test/java/model/LottoNumberTest.java
user: login: hangillee
body: 테스트가 정말 정상적으로 통과되었는지 검증하려면 `assert`문이 있어야 합니다.
단순하게`assertThatCode().doesNotThrowAnyException()`를 쓰는 방법이 있겠네요!

말씀하신대로 인스턴스 생성이 잘 되었음을 반증하는 방법으로 설명할 수 있다고 생각합니다! 😁
created_at: 2025-03-05T00:22:53Z
author_association: NONE
diff_hunk: @@ -9,18 +9,12 @@ public class LottosDto {
 
     private final List<String> lottoStrings;
 
-    private LottosDto(List<String> lottoStrings) {
-        this.lottoStrings = lottoStrings;
-    }
-
-    public static LottosDto from(Lottos lottos) {
-        List<LottoNumbers> lottoNumbersList = lottos.getLottoNumbersList();
+    public LottosDto(Lottos lottos) {
+        List<LottoNumbers> lottoNumbersCollection = lottos.getLottoNumbersCollection();
 
-        List<String> lottoStrings = lottoNumbersList.stream()
+        this.lottoStrings = lottoNumbersCollection.stream()
                 .map(LottoNumbers::toString)
                 .toList();
path: src/main/java/dto/LottosDto.java
user: login: hangillee
body: 사실, `List<LottoNumbers>`가 발행된 로또들이랑 일치하는 개념이라서 이름 짓기가 어려운 건 사실인 것 같아요.
지금의 이름도 나쁘진 않아 보입니다! 😁 고민하느라 고생하셨어요!
created_at: 2025-03-05T00:38:01Z
author_association: NONE
diff_hunk: @@ -7,4 +7,7 @@ public class LottoConstraints {
     public static final int MAXIMUM_LOTTO_NUMBER = 45;
     public static final int LOTTO_SIZE = 6;
 
+    private LottoConstraints() {
+    }
path: src/main/java/model/LottoConstraints.java
user: login: hangillee
body: 인스턴스화 방지 👍
created_at: 2025-03-05T00:38:19Z
author_association: NONE
diff_hunk: @@ -0,0 +1,43 @@
+package fixture;
+
+import model.LottoNumber;
+import model.LottoNumbers;
+import model.LottoRank;
+
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public enum LottoNumbersFixture {
+
+    WINNING_LOTTO(getLottoNumbersFromIntegers(1, 2, 3, 4, 5, 6), LottoRank.SIX_EQUALS),
+    THREE_EQUALS_LOTTO(getLottoNumbersFromIntegers(1, 2, 3, 43, 44, 45), LottoRank.THREE_EQUALS),
+    FOUR_EQUALS_LOTTO(getLottoNumbersFromIntegers(1, 2, 3, 4, 44, 45), LottoRank.FOUR_EQUALS),
+    FIVE_EQUALS_LOTTO(getLottoNumbersFromIntegers(1, 2, 3, 4, 5, 45), LottoRank.FIVE_EQUALS),
+    SIX_EQUALS_LOTTO(getLottoNumbersFromIntegers(1, 2, 3, 4, 5, 6), LottoRank.SIX_EQUALS);
+
+    private final LottoNumbers lottoNumbers;
+    private final LottoRank expectedRank;
+
+    LottoNumbersFixture(LottoNumbers lottoNumbers, LottoRank expectedRank) {
+        this.lottoNumbers = lottoNumbers;
+        this.expectedRank = expectedRank;
+    }
+
+    public LottoNumbers getInstance() {
+        return lottoNumbers;
+    }
+
+    public LottoRank getExpectedRank() {
+        return expectedRank;
+    }
+
+    private static LottoNumbers getLottoNumbersFromIntegers(int... numbers) {
+        Set<LottoNumber> lottoNumbers = Arrays.stream(numbers)
+                .mapToObj(LottoNumber::new)
+                .collect(Collectors.toUnmodifiableSet());
+
+        return new LottoNumbers(lottoNumbers);
+    }
path: src/test/java/fixture/LottoNumbersFixture.java
user: login: hangillee
body: 가변 인자 활용 멋있네요! 😁
created_at: 2025-03-05T00:41:32Z
author_association: NONE
diff_hunk: @@ -0,0 +1,33 @@
+package fixture;
+
+import model.LottoRank;
+import model.LottoResult;
+
+import java.util.Arrays;
+import java.util.List;
+
+public enum LottoResultFixture {
+
+    THREE_EQUALS_RESULT(LottoRank.THREE_EQUALS),
+    FOUR_EQUALS_RESULT(LottoRank.FOUR_EQUALS),
+    FIVE_EQUALS_RESULT(LottoRank.FIVE_EQUALS),
+    FIVE_EQUALS_WITH_BONUS_BALL_RESULT(LottoRank.FIVE_WITH_BONUS_EQUALS),
+    SIX_EQUALS_RESULT(LottoRank.SIX_EQUALS);
+
+    private LottoRank lottoRank;
+
+    LottoResultFixture(LottoRank lottoRank) {
+        this.lottoRank = lottoRank;
+    }
path: src/test/java/fixture/LottoResultFixture.java
user: login: hangillee
body: IntelliJ와 같은 IDE에서도 추천하고 있지만, `private` 인스턴수 변수는 `final` 키워드를 붙여주는 것을 권장합니다! 😁
created_at: 2025-03-05T00:44:27Z
author_association: NONE
diff_hunk: @@ -0,0 +1,43 @@
+package fixture;
+
+import model.LottoNumber;
+import model.LottoNumbers;
+import model.LottoRank;
+
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public enum LottoNumbersFixture {
+
+    WINNING_LOTTO(getLottoNumbersFromIntegers(1, 2, 3, 4, 5, 6), LottoRank.SIX_EQUALS),
+    THREE_EQUALS_LOTTO(getLottoNumbersFromIntegers(1, 2, 3, 43, 44, 45), LottoRank.THREE_EQUALS),
+    FOUR_EQUALS_LOTTO(getLottoNumbersFromIntegers(1, 2, 3, 4, 44, 45), LottoRank.FOUR_EQUALS),
+    FIVE_EQUALS_LOTTO(getLottoNumbersFromIntegers(1, 2, 3, 4, 5, 45), LottoRank.FIVE_EQUALS),
+    SIX_EQUALS_LOTTO(getLottoNumbersFromIntegers(1, 2, 3, 4, 5, 6), LottoRank.SIX_EQUALS);
+
+    private final LottoNumbers lottoNumbers;
+    private final LottoRank expectedRank;
+
+    LottoNumbersFixture(LottoNumbers lottoNumbers, LottoRank expectedRank) {
+        this.lottoNumbers = lottoNumbers;
+        this.expectedRank = expectedRank;
+    }
+
+    public LottoNumbers getInstance() {
+        return lottoNumbers;
+    }
path: src/test/java/fixture/LottoNumbersFixture.java
user: login: hangillee
body: `Enum`은 상수를 나열한 열거형임을 고려해보면, `getValue()` 쪽이 더 자연스러울 것 같아요!
준의 생각은 어떤가요? 어느 쪽이 더 간단한 것 같나요? 😁
created_at: 2025-03-05T00:51:43Z
author_association: NONE
diff_hunk: @@ -0,0 +1,43 @@
+package fixture;
+
+import model.LottoNumber;
+import model.LottoNumbers;
+import model.LottoRank;
+
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public enum LottoNumbersFixture {
+
+    WINNING_LOTTO(getLottoNumbersFromIntegers(1, 2, 3, 4, 5, 6), LottoRank.SIX_EQUALS),
+    THREE_EQUALS_LOTTO(getLottoNumbersFromIntegers(1, 2, 3, 43, 44, 45), LottoRank.THREE_EQUALS),
+    FOUR_EQUALS_LOTTO(getLottoNumbersFromIntegers(1, 2, 3, 4, 44, 45), LottoRank.FOUR_EQUALS),
+    FIVE_EQUALS_LOTTO(getLottoNumbersFromIntegers(1, 2, 3, 4, 5, 45), LottoRank.FIVE_EQUALS),
+    SIX_EQUALS_LOTTO(getLottoNumbersFromIntegers(1, 2, 3, 4, 5, 6), LottoRank.SIX_EQUALS);
+
+    private final LottoNumbers lottoNumbers;
+    private final LottoRank expectedRank;
+
+    LottoNumbersFixture(LottoNumbers lottoNumbers, LottoRank expectedRank) {
+        this.lottoNumbers = lottoNumbers;
+        this.expectedRank = expectedRank;
+    }
+
+    public LottoNumbers getInstance() {
+        return lottoNumbers;
+    }
+
+    public LottoRank getExpectedRank() {
+        return expectedRank;
+    }
+
+    private static LottoNumbers getLottoNumbersFromIntegers(int... numbers) {
+        Set<LottoNumber> lottoNumbers = Arrays.stream(numbers)
+                .mapToObj(LottoNumber::new)
+                .collect(Collectors.toUnmodifiableSet());
+
+        return new LottoNumbers(lottoNumbers);
+    }
path: src/test/java/fixture/LottoNumbersFixture.java
user: login: kjoon418
body: 해당 메소드를 호출하는 클라이언트 측에서 `getLottoNumbersFromIntegers(1, 2, 3, 4, 5, 6)`처럼 어떤 숫자로 로또를 만들지 직관적으로 확인할 수 있도록 하기 위해 가변 인자를 활용했습니다.

코멘트 감사합니다!
created_at: 2025-03-05T03:02:51Z
author_association: NONE
diff_hunk: @@ -0,0 +1,33 @@
+package fixture;
+
+import model.LottoRank;
+import model.LottoResult;
+
+import java.util.Arrays;
+import java.util.List;
+
+public enum LottoResultFixture {
+
+    THREE_EQUALS_RESULT(LottoRank.THREE_EQUALS),
+    FOUR_EQUALS_RESULT(LottoRank.FOUR_EQUALS),
+    FIVE_EQUALS_RESULT(LottoRank.FIVE_EQUALS),
+    FIVE_EQUALS_WITH_BONUS_BALL_RESULT(LottoRank.FIVE_WITH_BONUS_EQUALS),
+    SIX_EQUALS_RESULT(LottoRank.SIX_EQUALS);
+
+    private LottoRank lottoRank;
+
+    LottoResultFixture(LottoRank lottoRank) {
+        this.lottoRank = lottoRank;
+    }
path: src/test/java/fixture/LottoResultFixture.java
user: login: kjoon418
body: 기본적으로 불변으로 만들어 놓고 필요할 때만 가변으로 여는 습관을 들이고 꼼꼼히 검토하도록 하겠습니다.
조언 감사합니다!
created_at: 2025-03-05T03:05:11Z
author_association: NONE
diff_hunk: @@ -0,0 +1,43 @@
+package fixture;
+
+import model.LottoNumber;
+import model.LottoNumbers;
+import model.LottoRank;
+
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public enum LottoNumbersFixture {
+
+    WINNING_LOTTO(getLottoNumbersFromIntegers(1, 2, 3, 4, 5, 6), LottoRank.SIX_EQUALS),
+    THREE_EQUALS_LOTTO(getLottoNumbersFromIntegers(1, 2, 3, 43, 44, 45), LottoRank.THREE_EQUALS),
+    FOUR_EQUALS_LOTTO(getLottoNumbersFromIntegers(1, 2, 3, 4, 44, 45), LottoRank.FOUR_EQUALS),
+    FIVE_EQUALS_LOTTO(getLottoNumbersFromIntegers(1, 2, 3, 4, 5, 45), LottoRank.FIVE_EQUALS),
+    SIX_EQUALS_LOTTO(getLottoNumbersFromIntegers(1, 2, 3, 4, 5, 6), LottoRank.SIX_EQUALS);
+
+    private final LottoNumbers lottoNumbers;
+    private final LottoRank expectedRank;
+
+    LottoNumbersFixture(LottoNumbers lottoNumbers, LottoRank expectedRank) {
+        this.lottoNumbers = lottoNumbers;
+        this.expectedRank = expectedRank;
+    }
+
+    public LottoNumbers getInstance() {
+        return lottoNumbers;
+    }
path: src/test/java/fixture/LottoNumbersFixture.java
user: login: kjoon418
body: 확실히, 상수를 반환하는 메서드 이름이  `getInstance()`인 것은 어색해 보입니다.
추천해 주신 `getValue()`가 더 간결하면서도 직관적인 것 같네요.
조언 감사합니다!
created_at: 2025-03-05T03:07:48Z
author_association: NONE
diff_hunk: @@ -9,18 +9,12 @@ public class LottosDto {
 
     private final List<String> lottoStrings;
 
-    private LottosDto(List<String> lottoStrings) {
-        this.lottoStrings = lottoStrings;
-    }
-
-    public static LottosDto from(Lottos lottos) {
-        List<LottoNumbers> lottoNumbersList = lottos.getLottoNumbersList();
+    public LottosDto(Lottos lottos) {
+        List<LottoNumbers> lottoNumbersCollection = lottos.getLottoNumbersCollection();
 
-        List<String> lottoStrings = lottoNumbersList.stream()
+        this.lottoStrings = lottoNumbersCollection.stream()
                 .map(LottoNumbers::toString)
                 .toList();
path: src/main/java/dto/LottosDto.java
user: login: kjoon418
body: `xxxCollection`이 직관적이라고 느껴지진 않았지만, 이를 대체할 좋은 네이밍을 생각해내지 못했습니다.
일급 컬렉션을 사용하면서 더 좋은 방법을 계속 고민해보겠습니다.
코멘트 감사합니다!
created_at: 2025-03-05T03:10:01Z
author_association: NONE
diff_hunk: @@ -0,0 +1,52 @@
+package model;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static model.LottoConstraints.MAXIMUM_LOTTO_NUMBER;
+import static model.LottoConstraints.MINIMUM_LOTTO_NUMBER;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class LottoNumberTest {
+
+    @Test
+    @DisplayName("적정 범위 내의 숫자를 통해 인스턴스를 생성한다")
+    void createLottoNumberByInteger() {
+        for (Integer legalNumber : getLegalNumbers()) {
+            LottoNumber lottoNumber = new LottoNumber(legalNumber);
+        }
+    }
path: src/test/java/model/LottoNumberTest.java
user: login: kjoon418
body: 이전에는 별도의 코드 없이도 예외가 발생하면 테스트가 성공하지 못하는 것을 알 수 있어서 괜찮다고 생각했습니다.

하지만 `assertThatCode().doesNotThrowAnyException()`을 사용하면 예외가 발생시 테스트가 실패한다는 것을 명시하므로 코드 이해도 쉬워지고, 테스트가 어째서 실패했는지를 쉽게 확인할 수 있을 것 같네요.

좋은 방법을 소개해 주셔서 감사합니다.
이후에 테스트 코드를 작성할 때 해당 메소드를 활용해보도록 하겠습니다!
created_at: 2025-03-05T03:15:20Z
author_association: NONE
diff_hunk: @@ -1,31 +1,90 @@
 package controller;
 
-import model.Lotto;
-import dto.LottoDto;
-import model.LottoPurchase;
-import view.*;
-import java.util.*;
+import dto.LottoResultDto;
+import dto.LottosDto;
+import model.*;
+import util.InputConverter;
+import view.LottoInputView;
+import view.LottoOutputView;
+
+import java.text.DecimalFormat;
+import java.util.List;
 
 public class LottoController {
 
-    private final LottoInputView lottoInputView = new LottoInputView();
-    private final LottoOutputView lottoOutputView = new LottoOutputView();
-    private final LottoPurchase lottoPurchase = new LottoPurchase();
+    private static final String TOTAL_PROFIT_FORMAT_FORM = "0.00";
+    private static final DecimalFormat TOTAL_PROFIT_RATE_FORMATTER = new DecimalFormat(TOTAL_PROFIT_FORMAT_FORM);
+
+    private final LottoInputView lottoInputView;
+    private final LottoOutputView lottoOutputView;
+    private final InputConverter inputConverter;
+
+    public LottoController(
+            LottoInputView lottoInputView,
+            LottoOutputView lottoOutputView,
+            InputConverter inputConverter
+    ) {
+        this.lottoInputView = lottoInputView;
+        this.lottoOutputView = lottoOutputView;
+        this.inputConverter = inputConverter;
+    }
 
     public void run() {
         int purchaseAmount = lottoInputView.getPurchaseAmount();
-        lottoInputView.printEmptyLine();
+        Lottos lottos = purchaseLottos(purchaseAmount);
+
+        LottoNumbers winningLottoNumbers = getWinningLottoNumbers();
+        LottoNumber bonusBall = getBonusBall();
+
+        LottoResults lottoResults = new LottoResults(lottos, winningLottoNumbers, bonusBall);
+        printLottoResults(lottoResults);
+        printTotalProfitRate(lottoResults, purchaseAmount);
+    }
+
+    private Lottos purchaseLottos(int purchaseAmount) {
+        int manualLottoAmount = lottoInputView.getManualLottoAmount();
+
+        List<String> manualLottoNumbersInput = lottoInputView.getManualLottoNumbers(manualLottoAmount);
+        List<LottoNumbers> manualLottoNumbersList = inputConverter.getLottoNumbersListFromInputs(manualLottoNumbersInput);
path: src/main/java/controller/LottoController.java
user: login: kjoon418
body: 좋은 의견 감사합니다 😄 

처음에는 과제를 구현하며 SRP를 과하게 의식하니 객체들이 너무 많아져 오히려 코드를 이해하기 어려워지는 것을 느꼈습니다.
그래서 중간에 리팩터링을 진행하며 최대한 적은 수의 객체만으로 SRP를 준수하며 요구사항을 완성하려고 노력했는데, `InputConverter가 반드시 필요한가?`에 대해서는 충분히 고민하지 않은 것 같습니다.

`InputConverter`가 가지는 책임을 `View`와 `Lottos`가 나누어 가져도 어색하지 않으니, 필수적이지 않은 클래스인 `InputConverter`는 없애는 것이 좋아 보이네요.

리뷰어님의 조언 덕에 이후에는 더욱 다양한 관점을 고려하며 코드를 작성할 수 있을 것 같습니다.
감사합니다!
created_at: 2025-03-05T03:25:48Z
author_association: NONE
diff_hunk: @@ -1,10 +1,12 @@
-import controller.LottoController;
+import config.LottoConfiguration;
 
 public class LottoApplication {
 
+    private static final LottoConfiguration lottoConfiguration = new LottoConfiguration();
+
     public static void main(String[] args) {
         try {
-            new LottoController().run();
+            lottoConfiguration.lottoController().run();
         } catch (Exception e) {
path: src/main/java/LottoApplication.java
user: login: kjoon418
body: 확실히, 개행 없이 메소드 체이닝을 사용하면 오히려 가독성이 떨어지는 것 같습니다.

좋은 내용을 알려주셔서 감사합니다!
created_at: 2025-03-05T03:29:22Z
author_association: NONE

PR comments:
user: login: kjoon418
created_at: 2025-03-04T01:25:13Z
body: 리뷰어님이 남겨주신 피드백을 반영해 코드를 수정했습니다!
상세한 조언 정말 감사합니다 😄 

### 답변

> 핵심 도메인에 깊게 연관되거나 수정에 영향을 주고 받는다면 도메인(모델) 클래스, 도메인을 가리지 않고 어디서나 활용되는 클래스(ex. Math)를 유틸 클래스로 봅니다.

해당 클래스를 범용적으로 사용할 수 있는지에 따라 구분할 수 있겠군요.
조언 감사합니다!
이후에 유틸 클래스와 모델 클래스를 구분할 때 참고하도록 하겠습니다.
리뷰어님의 말씀대로 **나의 클린 코드 기준**을 세우기 위해 많이 고민하도록 하겠습니다!

### 질문

- '**assert를 하지 않는 의미 없는 테스트 코드**'라고 조언해주셨는데, 모든 테스트 코드에 `assert`가 존재하는게 바람직한건지 궁금합니다.
   - 코멘트를 달아 주신 테스트 코드의 경우, '**올바른 방법으로 인스턴스를 생성할 경우 예외가 발생하지 않는다**'를 검증하고 싶었습니다.
   - `@DisplayName`으로 의도를 제대로 전달하면 괜찮을까요? 아니면 위 상황에 사용할 수 있는 더 적절한 방법이 존재하는건가요?
- `Test Fixture`를 사용한게 처음이라 미숙한 부분이 많을 것 같습니다.
   부적절하게 사용하고 있다면 조언해주시면 정말 감사하겠습니다!
author_association: NONE
user: login: kjoon418
created_at: 2025-03-05T05:09:27Z
body: 피드백을 반영해 코드를 개선했습니다.
특히, 다양한 케이스에 대한 테스트를 진행할 수 있도록 테스트 코드를 수정했습니다.
author_association: NONE