Skip to content

[SCG] 권희준 로또 미션 1~2 단계 제출합니다. #169

Merged
boorownie merged 9 commits intonext-step:heejoon-kwonfrom
Heejoon-Kwon:step/1-2
Apr 6, 2026
Merged

[SCG] 권희준 로또 미션 1~2 단계 제출합니다. #169
boorownie merged 9 commits intonext-step:heejoon-kwonfrom
Heejoon-Kwon:step/1-2

Conversation

@Heejoon-Kwon
Copy link
Copy Markdown

안녕하세요!

  • 수준: 자바는 기본적인 문법만 하고 스프링으로 넘어가서 자바 구현실력이 다소 미흡해요.
  • 지향하는 바: 객체지향설계, DDD(도메인 주도 설계)
  • 프로그램 구조: application에서 InputView와 OutputView로 사용자와 소통하며, 비즈니스 로직 구현을 위해 Lotto와 LottoTicket이라는 2개의 일급 컬렉션을 사용하고 있어요. 그외에는 인자와 반환값으로 사용할 다양한 Wrapper들이 정의되어 있어요.

Copy link
Copy Markdown

@seaniiio seaniiio left a comment

Choose a reason for hiding this comment

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

안녕하세요 희준님~ 로또 미션 잘 구현해주셨네요! 클래스, 메서드 분리가 아주 잘 되어있어서 읽기 편했습니다 ~ 👍

미션 요구사항에 집중해서 리뷰 남겨뒀어요!

준형님 리뷰까지 확인하신 뒤 천천히 다시 요청해주세요 (ง ˙˘˙ )ว

Comment on lines +59 to +63
private void validateTicketNumbers(List<Integer> ticket) {
if (ticket.get(0) < 1 || ticket.get(TICKET_LENGTH - 1) > 45) {
throw new WrongNumberInTicketException("wrong number in ticket");
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LottoTicket이 "로또 한 줄"을 표현하는 객체라면, 개별 로또 번호(1~45)의 유효성을 검증하는 것은 LottoTicket의 책임일까요?

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.

각 데이터의 유효성을 검증하는 건 그 데이터를 포함하는 최소단위 객체의 책임이라고 생각합니다. 예를 들어, 이것이 ProfitRate 클래스의 객체라고 한다면, 이것이 수익률으로서의 데이터 유효성은 당연히 검증되어 있다고 보는 것이 당연하므로, 수익률으로서의 데이터 유효성은 ProfitRate 클래스의 책임입니다.
LottoTicket의 경우에는 개별 로또 번호를 담고 있는 최소단위의 객체는 LottoTicket이므로 현재는 LottoTicket의 책임이 맞습니다. 다만, 개별 로또 번호를 나타내는 객체를 새롭게 정의하는 게 책임분배에 있어서 더 깔끔하므로 LottoNumber 클래스를 만들어서 해당 책임을 다하도록 하겠습니다!

Comment on lines +29 to +31
public List<Integer> getTicket() {
return ticket;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

getTicket()으로 티켓을 받아서 사용하는 곳에서 값을 추가하거나 빼면 어떻게 되나요? 그리고 그것은 희준님이 기대하신 바인가요? 외부에서의 List 변경이 영향을 미치지 않게 하려면 원천적으로 방지하려면 어떻게 하면 좋을까요?

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.

정말 감사합니다. 중요한 지점을 놓치고 있었네요. 제가 한 것처럼 List를 그대로 반환하면 메모리 주소를 반환하므로 사실상 getter를 사용한 의미가 없이, 필드에 직접 접근하는 것과 다를 바가 없어지네요. 새로운 리스트를 만들어 반환하도록 수정하겠습니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

좋습니다 ㅎㅎ 현재 LottoTicket가 불변 객체이지만, 만약 가변 객체라면 어떻게 리턴해줘야 안전할지도 생각해보셨으면 좋겠습니다!

Comment on lines +36 to +40
for (int lottoNumber : winnerTicket.ticket) {
if (ticket.contains(lottoNumber)) {
correctCount++;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

요구사항에 맞게 리팩터링 해보면 좋을 것 같아요!

indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다. 1까지만 허용한다.

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.

네 수정했습니다!

import java.util.List;

public class Lotto {
public static final int TICKET_LENGTH = 6;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

"숫자 6개 묶음"을 의미하는 LottoTicket 클래스가 별도로 존재하는데, Lotto 클래스에 해당 상수가 필요했던 이유가 무엇일까요?

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.

NumberListGenerator가 이 로또에 특화된 숫자 생성기가 아니라 범용적인 숫자 생성기로 만들고자 그랬는데.. 지금 보니 굳이 그럴 필요가 없는 것 같아요. generate할 숫자를 입력받지 않고 상수 파일에 정의된 TICKET_LENGTH만큼 숫자를 생성하도록 바꿨습니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

(지금은 상수로 분리하셨지만) 기존 6이라는 로또 티켓 길이에 대한 규칙(TICKET_LENGTH)은 LottoTicket이 관리했었죠! LottoTicket이 스스로의 규칙(숫자 6개)을 알고 있다면, 외부(Lotto)에서 이 LottoTicek의 규칙을 꺼내와서 숫자를 만들어 다시 LottoTicekt에 넣어주는 것보다, LottoTicket 스스로 생성하는 것이 더 자연스러울 수 있어요.

이러한 관점에서, 로또 티켓의 길이가 과연 상수로 관리할 만큼 범용적인 값인지, 혹은 객체 내부로 캡슐화할 수 있는 정보인지 고민해보시면 좋을 것 같아요.

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

public class Lotto {
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라는 클래스명으로 표현하기 충분한지, 책임을 더 잘 표현하기 위한 다른 이름이 없는지 고민해보시면 좋을 것 같습니다! 제가 봤을 때는 이 클래스의 역할은 "사용자가 가진 로또 티켓 묶음"으로 보여요.

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.

네, Lotto는 도메인이기에 해당 클래스의 이름은 LottoTicketBundle로 바꿨습니다.

private final int fiveCorrectCount;
private final int sixCorrectCount;

public LottoResult(List<Integer> correctCounts) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

여기서 해당 List의 원소가 어떤 숫자를 표현하는지 한 번에 알기 어려웠어요. 마침 이를 표현하는 적절한 객체도 만들어주셨는데, 이를 활용해보는건 어떨까요?

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.

네 수정했습니다!

Comment on lines +26 to +29
System.out.println("3개 일치 (5000원)- " + result.getThreeCorrectCount() + "개");
System.out.println("4개 일치 (50000원)- " + result.getFourCorrectCount() + "개");
System.out.println("5개 일치 (1500000원)- " + result.getFiveCorrectCount() + "개");
System.out.println("6개 일치 (2000000000원)- " + result.getSixCorrectCount() + "개");
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.

로또 관련된 상수를 분리하면서 당첨금액도 로또 상수 파일로 분리했습니다!

Comment on lines +6 to +8
public TicketCount(LottoPayment payment) {
this.value = payment.getValue() / 1000;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Image

지금 프로젝트에서 "1000"이라는 로또 한 장 가격이 여러 곳에서 사용되고 있어요. 만약 로또 한 장 가격이 변경된다면 이 모든 곳을 수정해줘야 할텐데, 어떻게 개선할 수 있을까요?

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.

lotto와 관련해서 두 개 이상의 파일에서 사용되는 상수들을 모아둔 LottoConstants 클래스를 따로 정의했습니다.

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.

VO와 상수의 차이를 물으시는 거라면, 여러 곳에서 쓰이지만 값의 불변성이 담보되어야 하는 상황에선 VO로 캡슐화해야하고 반대로 여러 곳에서 쓰이지만 값이 필요에 따라 변경될 수 있다면 상수로 사용해야한다고 생각합니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

희준님이 말씀하신 불변성이 런타임에 변경되지 않는다는 것을 의미한다면, 상수도 static final로 정의하니까 불변이지 않나요?

1000이라는 숫자는 지금처럼 공용 상수로 선언할 수도 있고, 희준님이 언급하신 VO로 만들 수도 있고, 기존 존재하는 객체에 캡슐화할 수도 있어요. 로또 한 장에 1000원이라는 것은 애플리케이션 전체가 알아야 할 규칙일까요, 아니면 특정 객체가 책임져야 할 내부 규칙일까요?

어떻게 구현하는게 희준님이 생각하는 객체지향에 가까운 방법인지 생각해보셨으면 좋겠어요!

@@ -0,0 +1,27 @@
# 1단계 기능 요구사항
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

(위치가 애매해서 아무곳에나 남깁니다!)

희준님이 페어 프로그래밍 이후에 혼자 진행하신 내용을 확인하고 싶어서 커밋 이력을 확인했는데, 보면서 든 생각이 두 가지가 있어요!

  1. 페어 프로그래밍 했을 때보다 많은 코드가 변경됐어요! 이는 페어와의 생각 차이로 인한건지, 아니면 시간 부족으로 페어 프로그래밍을 충분히 하지 못한건지 궁금해요.
  2. refactor: define wrappers 커밋에서 래퍼 클래스를 정의했을 뿐만 아니라 커스텀 예외 추가, 메서드 분리 등 다양한 작업을 하신 것 같아요. 작업별로 커밋을 분리하면, 추후 희준님 혹은 동료 개발자가 작업 목록, 순서를 대략적으로 확인할 때 용이하답니다 :)

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.

  1. 페어 프로그래밍 할 당시에는 현장에서 어떻게든 예상대로 돌아가게끔 페어와 빠르게 프로그래밍을 완료했지만, 이후에 제출하려고 코드를 확인해보니 '원시타입과 문자열 포장', 예외처리, 클래스간 책임 구분 등 너무 안되어 있는 게 많아, 이대로 제출했다간 앞서 언급한 기본 요구사항에 대한 수정만 하다 이번 미션 리뷰가 끝날 것 같아 제출 직전에 급하게 수정햇슴다,,
  2. 네, 이건 진짜 고쳐야 할 제 습관입니다. 하나의 작업을 완료할 때마다 커밋을 해서 관리하기 용이하도록 해야하는데 항상 다 수정하고 커밋하게 되네요.. 이번 리뷰 반영도 마지막 리뷰까지 와서야 커밋없이 계속 수정했다는 걸 깨달았습니다.. 꼭 고치도록 노력할게요

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

  1. 꼼꼼한 요구사항 확인 좋습니다 👍 그냥 비하인드가 궁금했어요 ㅎㅎ
  2. 몰입하다보면 충분히 그럴 수 있어요! 작업 흐름이 변경될 때를 의식하려고 노력하면 커밋할 타이밍을 잘 잡을 수 있을거에요.

import view.InputView;
import view.OutputView;

public class Application {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

희준님이 작성하신 로또 애플리케이션이 잘 돌아가는지 확인하려면, 직접 애플리케이션 실행시키면서 예외, 성공 등 모든 상황을 입력해봐야 하나요? ( T ⩌ T )

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.

테스트 추가하는 건 저번 미션처럼 3,4단계때 요구사항으로 나오는 거 아니였나요?? 일단 당첨횟수가 일치하는지 확인하는 테스트 추가햇습니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

테스트 추가를 단순히 요구사항으로 느끼셨을 수도 있을 것 같아요!

저는 테스트 코드가 희준님과 희준님의 코드를 보는 사람들의 소중한 시간을 아껴주는 강력한 도구라고 생각해요. 코드 한 줄을 수정할 때마다 매번 애플리케이션을 실행해 수동으로 값을 입력하는 과정은 꽤 비효율적일 수 있어요. 만약 견고한 테스트코드를 작성해뒀다면, 아무리 코드를 수정해도 테스트만 한 번 돌리면 마음의 안정(?)을 얻을 수 있어요! 리뷰어, 혹은 팀원에게 희준님의 코드가 정상적으로 동작한다는 것을 증명하기 위한 수단으로도 사용할 수 있을 것 같구요 ㅎㅎ

1, 2주차 미션을 통해 정의한 희준님만의 테스트 작성 기준이나 그 필요성에 대해 공유해 주실 수 있나요? 🙂

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.

기능 구현이 완료됐는지 확인할때마다 애플리케이션 전체를 돌려 입력값 넣고 결과 확인하고, 다시 애플리케이션 돌리고 입력 넣고 결과를 확인하는 번거로운 절차를 통합테스트를 이용하면 확실히 줄일 수 있기에 그 필요성은 항상 체감하고 있습니다.
하지만 그에 반해 제가 단위테스트를 활용하지 못하는 이유는 제 커밋 습관과 비슷하다고 생각합니다. 프로그램의 일정부분을 완성할때마다 단위테스트를 만들어 확인하면 견고하고 문제없는 프로그램이 만들어질테지만 저는 코드를 domian model부터 application까지 연속해서 짜기 때문에 이미 단위테스트를 할 시기를 놓쳐버렸고 통합테스트를 해서 성공하면 단위테스트를 굳이 해야하나?라는 생각이 들어 이 시점까지 오면 단위테스트를 포기하게 되버리는 것 같습니다.
또 다른 이유로는 아직 테스트 구현에 미숙해서 그런 것도 있습니다. 테스트를 구현하는 게 실제 로직을 구현하는 것보다 오래 걸리는 게 현상황이어서,, 통합테스트만 구현하고나서, 이제 단위테스트를 구현하려고 하면 살짝 엄두가 안 나는 것 같기도 해요.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

맞아요 ㅜㅜ 저도 일단 애플리케이션 다 구현해놓고 테스트코드는 나중에 작성하곤 했어요. 그리고 지금도 개발할 때 테스트코드 작성 시간이 오히려 더 많이 소요될 때가 많아서, 테스트코드에 대한 부담감 충분히 이해됩니다!

지금 구현하는 로또는 규모가 작아서 애플리케이션 돌려가면서 테스트해도 크게 문제가 없지만, 규모가 조금만 더 커져도 직접 테스트에는 한계가 있을 수밖에 없어요.

작업 단위 커밋, 테스트 코드 작성에 익숙해지기 위해 희준님께 추천드리고 싶은 방법이 두 가지가 있어요!

  1. 클래스 하나를 완성시키는 시점에 해당 클래스에 대한 테스트 코드까지 작성하고 커밋하기
  2. TDD 사이클 경험해보기 (짧은 사이클, 작은 단위로 테스트를 작성하는 것에 익숙해질 수 있어요!)

2번은 나중에 시간 많을 때 경험해보시는 것을 추천드리고, 1번은 다음 3-5단계 미션에 대한 추가적인 요구사항으로 드려볼게요 ㅎㅎ

@@ -0,0 +1,13 @@
package domain.wrappers;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

  1. 희준님이 생각하는 domain은 무엇인가요?
  2. wrappers로 구조를 분리한 이유는 무엇인가요? wrappers로 분리된 클래스와, 그렇지 않는 클래스는 어떤 차이가 있나요?

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.

  1. 소프트웨어가 타겟팅하고 있는 비즈니스 영역이라고 생각합니다.
  2. LottoTicket과 Lotto는 저희가 타겟팅하고 있는 도메인과 직접적인 관련성이 있는 모델이지만 Wrappers에 속해있는 클래스들은 인자로 주어지는 원시타입 혹은 원시타입의 묶음에 그저 의미를 부여하기 위해 존재하는 것이므로 본질적으로 다르다고 생각합니다.

@@ -0,0 +1,13 @@
package domain.wrappers;

public class CorrectCount {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CorrectCount라는 개념을 wrapping한 이유는 무엇인가요??

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타입이었지만 이번 요구사항인 '모든 원시타입과 문자열 포장'을 고려해 wrapping하였습니다. 근데 하고보니 원래 그냥 int타입일 땐 순서가 뒤바뀌면 잘못된 인자가 넘어갈수도 있지만 새롭게 클래스를 정의하니 인자 전달에 있어서 안정성도 나아지고 의미를 더욱 살릴 수 있어서 wrapping이 왜 필요한지 느낄 수 있었습니다.

return value;
}

private void validate(int value) {
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.

로또 지불금액에 대해 음수인지와 1000의 배수인지 둘 모두를 검증하므로 2개의 책임이 있다고 볼 수 있습니다. 이 둘 모두를 하나의 'validate'이란 이름으로 뭉뚱그리기엔 명확하지 않아 보이므로 둘로 분리하도록 하겠습니다.

@@ -0,0 +1,13 @@
package domain.wrappers;

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.

제가 그동안 알고 있던 객체지향은 인터페이스가 중심이 되는 설계였습니다. 특정 구현체에 인터페이스라는 역할을 부여하고 이러한 역할에 따라 책임(인터페이스가 제공하는 메서드)이 생기고 이러한 책임에 근거해서 객체들끼리 서로에게 편하게 일을 시키며(TDA) 협력할 수 있는 설계입니다. 그런데 저번 미션때 제가 쓸데없이 인터페이스를 남발해서 오버엔지니어링을 유발하는 게 아닌가? 하는 의문을 갖게 되어 이제는 책임, 역할, 협력을 인터페이스 중심에서 클래스 중심으로 내려와 생각해보고자 합니다.

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.

유연함? 자유로움? 이라고 생각합니다. 객체별로 하나의 책임, 역할 구분, 이에 따른 캡슐화, 상속과 구현 등을 통해 객체들간의 경계를 명확히 해서 레고 조립하듯이 객체들을 조립하고 상황과 필요에 따라 몇몇 객체를 교체하는 등 필요한 로직이나 기능을 유기적으로 연결하고 바꾸는 데 있어서 자유로움이 객체지향의 이점이라고 생각합니다.

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으로 두어 캡슐화시키는 것이 어떻게 유연함으로 이어지는건지 등을 한번 스스로 설명해보세요.

3~5단계에서도 다른 개념보다 유연한지, 자유로운지만을 생각하고 한번 구현해보시면 좋을 것 같습니다. 그러면서 이상한 부분이 있다면 내가 생각하는 객체지향이란 무엇인가를 조금씩 수정하면서 계속 검증해보시면 나만의 답을 찾을 수 있을 거에요!

다음 단계에서 계속 이야기해봅시다 ㅎㅎ 1~2단계 수고하셨어요!

Copy link
Copy Markdown

@seaniiio seaniiio left a comment

Choose a reason for hiding this comment

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

안녕하세요 희준님~ 리뷰 꼼꼼히 잘 반영해주셨네요! 👍

답글이랑 추가적인 리뷰 몇 개 남겨뒀습니다! 꼭 월요일 7시 전까지 요청 안 주셔도 되니, 천천히 확인하시고 희준님의 생각 편하게 공유해주세요 ~ ꒰´꒳`꒱

Comment on lines +32 to +38
List<Integer> ticketNumberList = ticket.stream().map(LottoNumber::getNumber).toList();
correctCount += (int) winnerTicket
.getTicket()
.stream()
.map(LottoNumber::getNumber)
.filter(ticketNumberList::contains)
.count();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

indent 요구사항을 지키기 위해 리팩터링 해주셨는데, 제가 보기에는 가독성이 나빠진 것 같아요.

// before
for (int lottoNumber : winnerTicket.ticket) {
    if (ticket.contains(lottoNumber)) {
        correctCount++;
    }
}

// after
List<Integer> ticketNumberList = ticket.stream().map(LottoNumber::getNumber).toList();
correctCount += (int) winnerTicket
        .getTicket()
        .stream()
        .map(LottoNumber::getNumber)
        .filter(ticketNumberList::contains)
        .count();

미션에서 indent 요구사항을 제시한 의도가 무엇일까요? stream api를 활용해서 indent를 줄이는 것이 해결법일까요?

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.

함수로 따로 분리했습니다!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

좋아요~ 기존에는 "반복"과 "숫자 포함 판단"을 하나의 Stream으로 처리하려다보니 가독성이 안 좋았다고 생각해요. 숫자 포함 판단 부분을 메서드로 잘 분리해주셨습니다!

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.

테스트 파일 안에 내부 클래스로 정의했습니다.

import java.util.List;

public class Lotto {
public static final int TICKET_LENGTH = 6;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

(지금은 상수로 분리하셨지만) 기존 6이라는 로또 티켓 길이에 대한 규칙(TICKET_LENGTH)은 LottoTicket이 관리했었죠! LottoTicket이 스스로의 규칙(숫자 6개)을 알고 있다면, 외부(Lotto)에서 이 LottoTicek의 규칙을 꺼내와서 숫자를 만들어 다시 LottoTicekt에 넣어주는 것보다, LottoTicket 스스로 생성하는 것이 더 자연스러울 수 있어요.

이러한 관점에서, 로또 티켓의 길이가 과연 상수로 관리할 만큼 범용적인 값인지, 혹은 객체 내부로 캡슐화할 수 있는 정보인지 고민해보시면 좋을 것 같아요.

return lottoTickets;
}

public void createRandomTickets(TicketCount ticketCount, NumberListGenerator numberListGenerator) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

좋습니다~ 👏

제가 생각했을 때 추가적인 장점은, TicketCount에 독자적인 로직이 생기는 경우(ex. 1인당 최대 1,000장 구매 제한' 같은 정책)이 추가되더라도, 변화에도 유연할 것 같아요.

객체 간의 협력에 대한 이해도가 높으신 것 같습니다 ㅎㅎ

Comment on lines +6 to +8
public TicketCount(LottoPayment payment) {
this.value = payment.getValue() / 1000;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

상수를 활용해 값을 외부로 노출했을 때 어떤 단점이 있을까요? 어떤 값을 상수로 사용해야 하고, 어떤 값을 캡슐화해야 하나요?

@@ -0,0 +1,27 @@
# 1단계 기능 요구사항
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

  1. 꼼꼼한 요구사항 확인 좋습니다 👍 그냥 비하인드가 궁금했어요 ㅎㅎ
  2. 몰입하다보면 충분히 그럴 수 있어요! 작업 흐름이 변경될 때를 의식하려고 노력하면 커밋할 타이밍을 잘 잡을 수 있을거에요.

Comment on lines +33 to +36
Assertions.assertThat(result.getThreeCorrectCount()).isEqualTo(expectedResult.get(0));
Assertions.assertThat(result.getFourCorrectCount()).isEqualTo(expectedResult.get(1));
Assertions.assertThat(result.getFiveCorrectCount()).isEqualTo(expectedResult.get(2));
Assertions.assertThat(result.getSixCorrectCount()).isEqualTo(expectedResult.get(3));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

여러 검증 로직을 수행할 때, JUnit의 assertAll()로 묶어주면 중간에 검증이 실패하더라도 남은 검증을 끝까지 수행해서 디버깅이 용이해져요!

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.

넵 수정했습니다!

Comment on lines +40 to +46
return Stream.of(
Arguments.of(List
.of(1, 2, 3, 40, 41, 42)
.stream()
.map(LottoNumber::new)
.toList(),
List.of(1, 0, 0, 0)),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Image

인텔리제이의 경고가 도움이 될 수 있어요!

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.

넵 수정완료 했습니다!

import view.InputView;
import view.OutputView;

public class Application {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

테스트 추가를 단순히 요구사항으로 느끼셨을 수도 있을 것 같아요!

저는 테스트 코드가 희준님과 희준님의 코드를 보는 사람들의 소중한 시간을 아껴주는 강력한 도구라고 생각해요. 코드 한 줄을 수정할 때마다 매번 애플리케이션을 실행해 수동으로 값을 입력하는 과정은 꽤 비효율적일 수 있어요. 만약 견고한 테스트코드를 작성해뒀다면, 아무리 코드를 수정해도 테스트만 한 번 돌리면 마음의 안정(?)을 얻을 수 있어요! 리뷰어, 혹은 팀원에게 희준님의 코드가 정상적으로 동작한다는 것을 증명하기 위한 수단으로도 사용할 수 있을 것 같구요 ㅎㅎ

1, 2주차 미션을 통해 정의한 희준님만의 테스트 작성 기준이나 그 필요성에 대해 공유해 주실 수 있나요? 🙂

Copy link
Copy Markdown

@seaniiio seaniiio left a comment

Choose a reason for hiding this comment

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

안녕하세요 희준님! 1-2단계 열심히 임해주셔서 감사해요~~

리뷰에도 언급했듯이, 다음 3-5단계 미션에서는 아래와 같은 규칙을 적용해 보셨으면 좋겠어요.

클래스 하나를 완성시키는 시점에 해당 클래스에 대한 테스트 코드까지 작성하고 커밋하기

만약 이 방식이 현 상황에서 불필요한 요구사항이라고 생각하신다면, 그렇게 판단하신 이유를 적어서 리뷰 요청을 보내주세요!

1-2단계 고생 많으셨고, 다음 단계에서 뵈어요 (~˘▾˘)~

Comment on lines +40 to 46
private static class From1to6NumberListGenerator implements LottoNumberListGenerator {
@Override
public List<Integer> generate() {
return List.of(1, 2, 3, 4, 5, 6);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LottoNumberListGenerator함수형 인터페이스였다면, 이렇게 클래스를 구현해줄 필요 없이 람다를 통해 테스트에서 바로 주입해줄 수도 있어요! 블로그

bundle.createRandomTickets(new TicketCount(new LottoPayment(1000)), () -> List.of(1, 2, 3, 4, 5, 6));

Comment on lines +32 to +38
List<Integer> ticketNumberList = ticket.stream().map(LottoNumber::getNumber).toList();
correctCount += (int) winnerTicket
.getTicket()
.stream()
.map(LottoNumber::getNumber)
.filter(ticketNumberList::contains)
.count();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

좋아요~ 기존에는 "반복"과 "숫자 포함 판단"을 하나의 Stream으로 처리하려다보니 가독성이 안 좋았다고 생각해요. 숫자 포함 판단 부분을 메서드로 잘 분리해주셨습니다!

Comment on lines +6 to +8
public TicketCount(LottoPayment payment) {
this.value = payment.getValue() / 1000;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

희준님이 말씀하신 불변성이 런타임에 변경되지 않는다는 것을 의미한다면, 상수도 static final로 정의하니까 불변이지 않나요?

1000이라는 숫자는 지금처럼 공용 상수로 선언할 수도 있고, 희준님이 언급하신 VO로 만들 수도 있고, 기존 존재하는 객체에 캡슐화할 수도 있어요. 로또 한 장에 1000원이라는 것은 애플리케이션 전체가 알아야 할 규칙일까요, 아니면 특정 객체가 책임져야 할 내부 규칙일까요?

어떻게 구현하는게 희준님이 생각하는 객체지향에 가까운 방법인지 생각해보셨으면 좋겠어요!

import view.InputView;
import view.OutputView;

public class Application {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

맞아요 ㅜㅜ 저도 일단 애플리케이션 다 구현해놓고 테스트코드는 나중에 작성하곤 했어요. 그리고 지금도 개발할 때 테스트코드 작성 시간이 오히려 더 많이 소요될 때가 많아서, 테스트코드에 대한 부담감 충분히 이해됩니다!

지금 구현하는 로또는 규모가 작아서 애플리케이션 돌려가면서 테스트해도 크게 문제가 없지만, 규모가 조금만 더 커져도 직접 테스트에는 한계가 있을 수밖에 없어요.

작업 단위 커밋, 테스트 코드 작성에 익숙해지기 위해 희준님께 추천드리고 싶은 방법이 두 가지가 있어요!

  1. 클래스 하나를 완성시키는 시점에 해당 클래스에 대한 테스트 코드까지 작성하고 커밋하기
  2. TDD 사이클 경험해보기 (짧은 사이클, 작은 단위로 테스트를 작성하는 것에 익숙해질 수 있어요!)

2번은 나중에 시간 많을 때 경험해보시는 것을 추천드리고, 1번은 다음 3-5단계 미션에 대한 추가적인 요구사항으로 드려볼게요 ㅎㅎ

@boorownie boorownie merged commit bbdec80 into next-step:heejoon-kwon Apr 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants