Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
c3baf1e
Feat: 로또 번호 범위를 검증하는 LottoNumber 클래스 구현
haeun92e0 Mar 30, 2026
fbd8146
Feat: 로또 한 장의 번호 개수 검증 및 정렬 기능 구현
haeun92e0 Mar 30, 2026
cde2dfa
Feat: 구매한 여러 장의 로또를 관리하는 일급 컬렉션 구현
haeun92e0 Mar 30, 2026
0de51be
Feat: 일치 개수에 따른 당첨 순위 및 상금을 관리하는 Enum 구현
haeun92e0 Mar 30, 2026
33ed28c
Feat: 구매 금액 및 당첨 번호 입력을 담당하는 InputView 구현
haeun92e0 Mar 30, 2026
cfa1bfe
Feat: 로또 구매 목록 및 당첨 통계 출력을 담당하는 OutputView 구현
haeun92e0 Mar 30, 2026
5d6d0cb
Feat: 로또 구입, 발급, 당첨 확인 전체 흐름 제어 로직 구현
haeun92e0 Mar 30, 2026
dd90144
Test: 로또 번호가 1~45 범위를 벗어날 경우 예외 발생 테스트 구현
haeun92e0 Mar 30, 2026
1f49977
Test: 구입한 로또와 당첨 번호를 비교하여 일치 개수를 정확히 반환하는지 확인
haeun92e0 Mar 30, 2026
2a497c6
Test: 일치 개수에 따른 당첨 순위(Rank) 매핑 로직 검증
haeun92e0 Mar 30, 2026
325628b
Refactor: LottoGame의 static 키워드 제거 및 Application 분리
haeun92e0 Apr 1, 2026
c8a9957
Refactor: 당첨 결과 집계 책임을 LottoTickets 일급 컬렉션으로 이동
haeun92e0 Apr 1, 2026
243ce72
Refactor: MVC 패턴 적용을 위한 Controller 로직 분리 및 도메인 객체 추가
haeun92e0 Apr 1, 2026
810a0ac
Refactor: 로또 번호 개수를 매직 넘버 대신 상수로 관리하도록 변경
haeun92e0 Apr 2, 2026
a3b0e97
Refactor: 성능 최적화를 위해 Rank 키를 사용하는 Map을 EnumMap으로 변경
haeun92e0 Apr 2, 2026
da40440
Docs: (복습용) LottoNumber 클래스에 코드 설명 주석 추가
haeun92e0 Apr 2, 2026
76785ae
Refactor: 정적 팩토리 메서드 적용을 통한 생성 로직 도메인 이동 및 불필요 클래스 제거
haeun92e0 Apr 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/main/java/lotto/Application.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package lotto;

public class Application {
public static void main(String[] args) {
LottoGame lottoGame = new LottoGame();
lottoGame.run(); // static이 아닌 메서드를 호출!
}
}
42 changes: 42 additions & 0 deletions src/main/java/lotto/LottoGame.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package lotto;

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

import java.util.List;

public class LottoGame {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Application.java와 LottoGame.java를 분리해두면 좋을 것 같은데요
static이 어떤 역할을 수행하는지, 어떤 상황에 사용하는게 좋을까요??

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

static은 클래스가 메모리에 로드될 때 한번 생성되며, 객체 생성 없이도 사용할 수 있게 해줍니다. static은 상태 없이 기능만 제공하는 클래스에서 사용하기 좋고 모든 객체가 공유해야하는 상수일 때 사용하면 좋습니다.
리뷰어님의 제안대로 application은 프로그램 시작점의 역할만 수행하고 lottogame은 게임의 흐름을 관리하는 객체로 분리하여 역할을 명확히 해보겠습니다.

private static final int LOTTO_PRICE = 1000;

public void run() {
int money = Integer.parseInt(InputView.inputMoney());
LottoTickets tickets = purchase(money);
OutputView.printTickets(tickets);

Lotto winningLotto = askWinningLotto();
showResult(tickets, winningLotto, money);
}
Comment on lines +12 to +19
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이제 LottoGame을 다시 보면 중요 로직은 도메인에 있기 때문에 컨트롤러는 마치 관리자처럼 객체에게 일을 시키는 존재로 변하게 되는 것을 알 수 있습니다

여기서 좀 더 깔끔한 코딩을 가져가자면 View와 Controller 사이에 String 대신 Integer, List로 변환해주는 클래스도 있으면 좋다는 생각이 드네요. 그러면 컨트롤러는 데이터 가공할 필요도 없이 완전히 객체들의 행동을 연결시키는 역할만 수행할 것 같아요


private LottoTickets purchase(int money) {
int count = money / LOTTO_PRICE;
OutputView.printTicketCount(count);
return LottoTickets.generate(count);
}

private Lotto askWinningLotto() {
String input = InputView.inputWinningNumbers();
List<Integer> numbers = java.util.Arrays.stream(input.split(","))
.map(String::trim)
.map(Integer::parseInt)
.collect(java.util.stream.Collectors.toList());
return Lotto.from(numbers);
}

private void showResult(LottoTickets tickets, Lotto winningLotto, int money) {
LottoResult lottoResult = new LottoResult(tickets.matchAll(winningLotto));
double yield = lottoResult.calculateYield(money);

OutputView.printStatistics(lottoResult.getResult(), yield);
}
}
41 changes: 41 additions & 0 deletions src/main/java/lotto/domain/Lotto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package lotto.domain;

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

public class Lotto {
private static final int LOTTO_SIZE = 6;
private final List<LottoNumber> numbers;

private Lotto(List<LottoNumber> numbers) {
validate(numbers);
this.numbers = new ArrayList<>(numbers);
Collections.sort(this.numbers);
}

public static Lotto from(List<Integer> numbers) {
List<LottoNumber> lottoNumbers = numbers.stream()
.map(LottoNumber::new)
.collect(Collectors.toList());
return new Lotto(lottoNumbers);
}

private void validate(List<LottoNumber> numbers) {
if (numbers.size() != LOTTO_SIZE) {
throw new IllegalArgumentException("로또 번호는 6개여야 합니다.");
}
}

public int countMatch(Lotto winningLotto) {
return (int) numbers.stream()
.filter(winningLotto.numbers::contains)
.count();
}

@Override
public String toString() {
return numbers.toString();
}
}
55 changes: 55 additions & 0 deletions src/main/java/lotto/domain/LottoNumber.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package lotto.domain;

import java.util.Objects;
//자바는 integer의 비교 방법은 이미 알고 있지만, 우리는 LottoNumber을 객체로 만들어서
//관리하고 있기 때문에, 비교 방법에 대해서 알려줘야 한다. > comparable<LottoNumber>
public class LottoNumber implements Comparable<LottoNumber> {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

이거는 Lotto에서 List로도 미션 진행할 수 있었을텐데 LottoNumber를 따로 만든 이유가 무엇인지 궁금해요

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

int를 그대로 썼다면 번호를 다루는 모든 곳에서 이 숫자가 1에서 45사이인지를 매번 확인해야했지만 LottoNumber 클래스 안에서 검증하게 함으로써 다른 클래스들이 번호의 유효성을 의심하지 않고 자신의 로직에 집중하게 됐습니다. 역할을 나누니 코드가 단순해지고 각 클래스의 역할이 명확해 진 것 같습니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋습니다~

private static final int MIN_NUMBER = 1;
private static final int MAX_NUMBER = 45;
private final int number;

public LottoNumber(int number) {
validate(number);
this.number = number;
}

private void validate(int number) {
if (number < MIN_NUMBER || number > MAX_NUMBER) {
throw new IllegalArgumentException("로또 번호는 1~45 사이여야 합니다.");
}
}

//모든 클래스는 자동으로 boolean equals, haseCode, toString을 상속한다.
//그러므로 LottoNumber도 이미 가지고 있음
//우리가 다시 override하는 이유는, 기본 equals는 주소 비교여서다. 우리가 원하는 것은
//값이 같으면 같은 객체로 두게끔이다. 그래서 override해서 수정한다.
//equals가 같으면 hashCode도 같아야 하므로 같이 수정.

@Override
public boolean equals(Object o) {//주소가 같아야 동일한게 아니라, 숫자가 같으면 동일함을 알려주기 위한 메서드
if (this == o) return true;
//getClass는 해당 객체의 타입을 알려주는 메서드
if (o == null || getClass() != o.getClass()) return false;
//if문 다 통과했으니 o는 null이 아니고 o의 클래스가 LottoNumber과 같다고 볼 수 있음
LottoNumber that = (LottoNumber) o; //형변환. o는 object지만 LottoNumber임에 틀림없으니 형변환을 해준다
//that에 저장
return number == that.number; //boolean이므로 같으면 true를 다르면 false를 반환함
}

@Override
public int hashCode() {
return Objects.hash(number);
}
Comment on lines +28 to +42
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

요거는 동등성과 동일성에 대해 학습하고 진행하신건가요??

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 이 코드를 짜면서 자바의 동일성과 동등성의 차이에 대해 학습했습니다. 동일성은 ==연산자로 두 객체의 메모리 주소가 같은지를 확인하는 것이고 동등성은 equals 메서드를 통해 두 객체가 가진 값이 같은지를 확인하는 것입니다. 로또 게임에서는 서로 다른 위치에 저장된 객체들이어도 숫자가 같다면 같은 번호로 취급해야하므로 equals를 재정의했습니다.


//compareTo : 정렬 순서가 오름차순임을 알려줌. Integer.compare 하기 때문
@Override
public int compareTo(LottoNumber other) {
return Integer.compare(this.number, other.number);
}

//toString으로 바꿔주지 않는다면 LottoNumber 객체를 출력했을 때 주소값이 출력됨
@Override
public String toString() {
return String.valueOf(number);
}
}
22 changes: 22 additions & 0 deletions src/main/java/lotto/domain/LottoResult.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package lotto.domain;
import java.util.EnumMap;
import java.util.Map;

public class LottoResult {
private final Map<Rank, Long> result;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 EnumMap을 사용해봐도 좋아보여요~


public LottoResult(Map<Rank, Long> result) {
this.result = new EnumMap<>(result);
}

public double calculateYield(int investment) {
long totalPrize = result.entrySet().stream()
.mapToLong(entry -> entry.getKey().getWinningMoney() * entry.getValue())
.sum();
return (double) totalPrize / investment;
}

public Map<Rank, Long> getResult() {
return result;
}
}
41 changes: 41 additions & 0 deletions src/main/java/lotto/domain/LottoTickets.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package lotto.domain;

import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class LottoTickets {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일급컬렉션은 만들어졌는데, 이 객체가 주도적으로 할 수 있는 책임은 따로 없는 것 같아요 ㅠㅠ
LottoTickets을 의인화해서 혼자 주도적으로 움직이는 객체라고 생각하면 다른 클래스에 있는 어떤 메서드를 여기 안으로 가져올 수 있을까요??

private final List<Lotto> tickets;

public LottoTickets(List<Lotto> tickets) {
this.tickets = tickets;
}

public static LottoTickets generate(int count) {
List<Integer> allNumbers = IntStream.rangeClosed(1, 45).boxed().collect(Collectors.toList());
List<Lotto> tickets = IntStream.range(0, count)
.mapToObj(i -> {
Collections.shuffle(allNumbers);
return Lotto.from(allNumbers.subList(0, 6));
})
.collect(Collectors.toList());
return new LottoTickets(tickets);
}

public Map<Rank, Long> matchAll(Lotto winningLotto) {
return tickets.stream()
.map(ticket -> Rank.valueOf(ticket.countMatch(winningLotto)))
.collect(Collectors.groupingBy(rank -> rank, () -> new EnumMap<>(Rank.class), Collectors.counting()));
}

public List<Lotto> getTickets() {
return tickets;
}

public int size() {
return tickets.size();
}
}
34 changes: 34 additions & 0 deletions src/main/java/lotto/domain/Rank.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package lotto.domain;

import java.util.Arrays;

public enum Rank {
MISS(0, 0),
FIFTH(3, 5_000),
FOURTH(4, 50_000),
THIRD(5, 1_500_000),
FIRST(6, 2_000_000_000);

private final int matchCount;
private final int winningMoney;

Rank(int matchCount, int winningMoney) {
this.matchCount = matchCount;
this.winningMoney = winningMoney;
}

public static Rank valueOf(int matchCount) {
return Arrays.stream(values())
.filter(rank -> rank.matchCount == matchCount)
.findFirst()
.orElse(MISS);
}

public int getMatchCount() {
return matchCount;
}

public int getWinningMoney() {
return winningMoney;
}
}
17 changes: 17 additions & 0 deletions src/main/java/lotto/view/InputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package lotto.view;

import java.util.Scanner;

public class InputView {
private static final Scanner scanner = new Scanner(System.in);

public static String inputMoney() {
System.out.println("구입금액을 입력해 주세요.");
return scanner.nextLine();
}

public static String inputWinningNumbers() {
System.out.println("지난 주 당첨 번호를 입력해 주세요.");
return scanner.nextLine();
}
Comment on lines +8 to +16
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

질문

InputView에서는 Integer나 List를 반환 할 수도 있어 보이는데 String으로 처리해주신 이유가 있나요?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

View의 역할에 대한 고민이 있었습니다. view에서 입력값이 숫자인지, 콤마로 구분된 리스트인지 판단하고 변환하는 로직은 비지니스 로직에 가깝다고 판단했고 그래서 view는 사용자가 입력한 문자열을 그대로 전달하는 역할만 맡도록 짰습니다.

}
34 changes: 34 additions & 0 deletions src/main/java/lotto/view/OutputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package lotto.view;

import lotto.domain.Lotto;
import lotto.domain.LottoTickets;
import lotto.domain.Rank;
import java.util.List;
import java.util.Map;

public class OutputView {
public static void printTicketCount(int count) {
System.out.println("\n" + count + "개를 구매했습니다.");
}

public static void printTickets(LottoTickets tickets) {
for (Lotto ticket : tickets.getTickets()) {
System.out.println(ticket);
}
System.out.println();
}

public static void printStatistics(Map<Rank, Long> result, double yield) {
System.out.println("\n당첨 통계\n---------");
printRank(Rank.FIFTH, result.getOrDefault(Rank.FIFTH, 0L));
printRank(Rank.FOURTH, result.getOrDefault(Rank.FOURTH, 0L));
printRank(Rank.THIRD, result.getOrDefault(Rank.THIRD, 0L));
printRank(Rank.FIRST, result.getOrDefault(Rank.FIRST, 0L));
System.out.printf("총 수익률은 %.2f입니다.\n", yield);
}

private static void printRank(Rank rank, long count) {
System.out.printf("%d개 일치 (%d원)- %d개\n",
rank.getMatchCount(), rank.getWinningMoney(), count);
}
}
19 changes: 19 additions & 0 deletions src/test/java/lotto/domain/LottoNumberTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package lotto.domain;

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.assertThatThrownBy;

class LottoNumberTest {

@ParameterizedTest
@ValueSource(ints = {0, 46, -1})
@DisplayName("로또 번호가 1~45 범위를 벗어나면 예외가 발생한다.")
void invalidNumberRange(int number) {
assertThatThrownBy(() -> new LottoNumber(number))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("로또 번호는 1~45 사이여야 합니다.");
}
}
33 changes: 33 additions & 0 deletions src/test/java/lotto/domain/LottoTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package lotto.domain;

import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

@SuppressWarnings("NonAsciiCharacters")
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class LottoTest {

@Test
void 로또_번호가_6개가_아니면_예외가_발생한다() {
// given
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

assertThatThrownBy(() -> Lotto.from(numbers))
.isInstanceOf(IllegalArgumentException.class);
}

@Test
void 당첨_번호와_몇_개의_번호가_일치하는지_계산한다() {
Lotto ticket = Lotto.from(Arrays.asList(1, 2, 3, 4, 5, 6));
Lotto winningLotto = Lotto.from(Arrays.asList(1, 2, 3, 10, 11, 12));

int matchCount = ticket.countMatch(winningLotto);
assertThat(matchCount).isEqualTo(3);
}
}
17 changes: 17 additions & 0 deletions src/test/java/lotto/domain/RankTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package lotto.domain;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class RankTest {

@Test
@DisplayName("일치 개수에 따라 올바른 Rank를 반환한다.")
void valueOfTest() {
assertThat(Rank.valueOf(6)).isEqualTo(Rank.FIRST);
assertThat(Rank.valueOf(3)).isEqualTo(Rank.FIFTH);
assertThat(Rank.valueOf(0)).isEqualTo(Rank.MISS);
}
}