Skip to content

[🚀 사이클1 - 미션 (블랙잭 게임 실행)] 재키 미션 제출합니다.#1000

Open
zzaekkii wants to merge 47 commits intowoowacourse:zzaekkiifrom
zzaekkii:zzaekkii
Open

[🚀 사이클1 - 미션 (블랙잭 게임 실행)] 재키 미션 제출합니다.#1000
zzaekkii wants to merge 47 commits intowoowacourse:zzaekkiifrom
zzaekkii:zzaekkii

Conversation

@zzaekkii
Copy link

@zzaekkii zzaekkii commented Mar 7, 2026

체크 리스트

  • 미션의 필수 요구사항을 모두 구현했나요?
  • Gradle test를 실행했을 때, 모든 테스트가 정상적으로 통과했나요?
  • 애플리케이션이 정상적으로 실행되나요?

어떤 부분에 집중하여 리뷰해야 할까요?

1. TDD 개발 방식에 대해서

TDD로 개발을 하다 보니 실제 로직이 아니라 테스트가 먼저 작성되는 구조가 조금 어색하게 느껴졌습니다.
또한 시간이 더 오래 걸리는 것 같다는 생각도 들었습니다.

로직이 그렇게 복잡하지 않은 경우에도 TDD가 꼭 필요한지 의문이 들었고,
차라리 메서드 구현을 먼저 끝낸 뒤 테스트를 작성하는 방식이 더 낫지 않을까 하는 생각도 들었습니다.

그래서 실무에서는 TDD를 어떻게 적용하고 있는지 궁금합니다.
예를 들어 복잡한 메서드에 한해서만 TDD를 적용하는지,
아니면 모든 메서드에 대해 TDD를 실천하는지 등 실제로 어떤 방식으로 활용하고 계신지 알고 싶습니다.


2. 랜덤 같은 통제 어려운 값에 대한 테스트

현재 카드를 뽑는 메서드내에는 랜덤 로직이 존재하여 매번 반환하는 값이 달라지기 때문에 테스트가 어려웠습니다.
현재는 단순히 범위로 검증하고 있는데 리턴값이 매번 달라지기 때문에 언젠가 테스트가 깨질지도 모른다는 생각이 들었습니다.

실무에서는 이러한 통제가 어려운 로직은 어떻게 테스트하는지 궁금합니다.


3. 패키지 구조를 어떻게 정리하는 것이 좋을지

현재 패키지 구조는 컨트롤러/서비스/도메인/뷰와 같이 레이어 기준으로 나누었는데요.
다만 domain 패키지 내부에 Card, Participant 등 여러 객체가 있어 한눈에 들어오진 않는 것 같다고 느꼈습니다.
그 외에도 패키지 깊이 면에서 어떤 구조가 좋은지 의견을 구하고 싶습니다.

  • domain 내부에 Card 관련 객체 / Participant 관련 객체 등으로 한 번 더 패키지로 나누는 것이 좋은지
  • constant, exception 같은 패키지들은 common과 같은 상위 패키지로 묶어 관리하는 것이 더 나은지
  • 지금처럼 패키지 깊이를 최소화하는 구조가 더 가독성이 좋은지

4. Dealer와 Player의 관계에 대해

일단 딜러도 결국 블랙잭에 참여하는 한 명의 플레이어라고 생각했습니다.

그러나 상속을 적용하기 보다는 isDealer 불리언 필드로 구별하도록 구현했습니다.
공부를 하면서 상속을 통한 구조보다는 조합이나 단순한 모델을 우선 고려해야 한다고 듣기도 했고,
그렇다고 상속을 무조건 지양해야만 하는지도 고민됩니다.

그래서 만약 리뷰어님이 블랙잭을 구현했다면 DealerPlayer를 어떻게 모델링했을지 의견을 듣고 싶습니다!


5. 메서드 길이 10라인 이내 규칙

페어와 고민하면서 최대한 10라인 내외로 이뤄지게 메서드를 추출하며 노력했음에도 불구하고,
아직도 10라인을 넘는 메서드가 존재하는데요.

그러다보니 실무에서도 메서드 길이를 10라인 이내로 작성하는지도 궁금해졌습니다.
그리고 지나치게 메서드를 잘게 나눈 것은 아닌지, 오히려 흐름을 따라가기 어려워지진 않았는지 고민하게 됐습니다..

zzaekkii added 30 commits March 4, 2026 07:48
@zzaekkii
Copy link
Author

zzaekkii commented Mar 8, 2026

아, 그리고 제 노트북에서 페어 프로그래밍을 해서 participant에 페어가 표시되지 않는데,
제 페어는 @jhk01007 제이입니다!

Copy link

@Chocochip101 Chocochip101 left a comment

Choose a reason for hiding this comment

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

안녕하세요 재키 :) 만나서 반갑습니다, 리뷰어 초코칩입니다 🍪 :)
블랙잭 미션을 잘 구현해주셨네요!👍
코멘트 남겼으니 확인해보시고 재요청주세요!

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

public class BlackjackService {

Choose a reason for hiding this comment

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

Service를 사용해주셨네요. Domain, Service는 어떤 역할이고, 차이가 존재하나요? 현재 구조에서 Service가 꼭 필요한가요? 🤔 BlackjackService 내의 메서드는 꼭 BlackjackService에 존재해야하는건가요?

Choose a reason for hiding this comment

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

예를 들어 drawCard 함수는 Participant로 책임을 옮기면 더 자연스러울 수 있지 않을까요?

  • blackjackService.drawCard(): 서비스가 카드를 draw한다....?
  • participant.drawCard(): 참가자가 카드를 draw한다....!

Copy link

@jhk01007 jhk01007 Mar 10, 2026

Choose a reason for hiding this comment

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

안녕하세요. 재키의 페어인 제이입니다!
우선 말씀감사합니다! 저희가 이렇게 구현한 이유는 drawCard()는 랜덤로직을 필요로 하기때문에 도메인에 통제 불가능한 랜덤로직으로 오염되면 안좋다고 생각해 해당 랜덤 로직을 인프라 계층으로 분리하였습니다. 그러다보니 도메인과 랜덤로직을 묶어줄 클래스가 필요하다고 생각했고, 그래서 Service클래스를 만들었습니다.

여기서 제가 궁금한 부분은 크게 두가지입니다

  1. 제가 생각하는 서비스는 여러 도메인이 협력해야하는 기능이나 데이터베이스 같은 외부 계층이 필요한 기능인 경우 사용하는데, 초코칩이 생각하는 서비스클래스의 역할이나 사용할 때의 주의할 점이 있는지 궁금합니다!
  2. Participant 내부로 카드뽑는(랜덤) 로직을 넣으라고 하셨는데 도메인안에 랜덤 로직이 들어가도 괜찮을까요??

}

@Override
public boolean equals(Object o) {

Choose a reason for hiding this comment

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

equals & hashcode 구현 👍🏻 💯


import static exception.ErrorMessage.PLAYER_NAME_LENGTH_ERROR;

public class Name {

Choose a reason for hiding this comment

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

record가 아닌 class를 사용한 이유가 있나요?

private final String name;

public Name(String name) {
if (name.length() < 2 || name.length() > 10) {

Choose a reason for hiding this comment

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

2와 10이 의미하는게 무엇인가요?

@@ -0,0 +1,11 @@
package constant;

public class BlackjackConstant {

Choose a reason for hiding this comment

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

BlackjackConstant로 인해서 모든 클래스와 결합도가 높아지고 있네요. BlackjackConstant가 꼭 필요하나요?
예를 들어 MINIMUM_PLAYER_BOUND는 participants 내부로 옮겨 캡슐화를 지킬 수 있지 않을까요?

Copy link

@jhk01007 jhk01007 Mar 10, 2026

Choose a reason for hiding this comment

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

BlackjackConstant로 인해서 모든 클래스와 결합도가 높아지고 있네요. BlackjackConstant가 꼭 필요하나요? 예를 들어 MINIMUM_PLAYER_BOUND는 participants 내부로 옮겨 캡슐화를 지킬 수 있지 않을까요?

안녕하세요! 재키 페어 제이입니다.
BlackjackConstant와 같은 전역 상수 클래스를 둘 때 발생할 수 있는 문제점에 대해서는 이해했습니다.
다만 한 가지 궁금한 점이 있습니다.
하나의 클래스에서만 사용되는 상수가 아니라 여러 클래스에서 공통으로 사용되는 상수이거나, 테스트 코드에서도 해당 상수를 이용해 검증하는 경우에는 상황이 조금 다르지 않을까 하는 생각이 들었습니다.
이 경우 상수가 변경되면 여러 곳의 코드를 함께 수정해야 할 가능성이 있는데, 이런 상황이라면 오히려 공통 상수 클래스로 관리하는 것이 변경 대응 측면에서 더 합리적이지 않을까 하는 고민이 들었습니다.
이 부분에 대해 초코칩의 의견이 궁금합니다!

+) 아니면 각 상수에 어울리는 클래스에 넣되 public static final로 열어두는 방향으로 해야할까요?

}
}

throw new IllegalStateException(DEALER_NOT_FOUND_ERROR.getMessage());

Choose a reason for hiding this comment

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

Participants 생성자와 getDealer()에서 IllegalStateException을 던지고 있는데요, IllegalArgumentException을 사용하는 게 더 의미상 적절하지 않을까요?

IllegalStateException는 어떤 상황에서 사용하는게 적합할까요?

Copy link

@jhk01007 jhk01007 Mar 10, 2026

Choose a reason for hiding this comment

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

제가 공부한 바로는

  • IllegalArgumentExecption: 메서드에 잘못된 인자가 전달되었을 때 발생하는 예외
  • IllegalStateException: 객체의 현재 상태(state)가 메서드를 수행하기에 적절하지 않을 때 발생하는 예외

로 이해했는데 이를 기반한다면, getDealer() 메서드는 인자가 잘못됐다기 보다는 '딜러'가 없다는 객체의 현재 상태가 적절하지 않다고 생각해서 IllegalStateException이 더 맞다고 생각했습니다. 혹시 제 생각에 잘못된 부분이 있다면 알려주시면 감사하겠습니다!

Comment on lines +49 to +57
private List<Participant> addParticipants() {
List<Name> playerNames = inputView.readPlayers(); // 플레이어 입력받기
List<Participant> participantList = new ArrayList<>();
for (Name name : playerNames) {
participantList.add(
new Participant(name, new HandCards(new ArrayList<>()), false));
}
return participantList;
}

Choose a reason for hiding this comment

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

Participants 생성자에서 외부에서 받은 리스트에 딜러를 추가하고 있네요.
이렇게 하면 생성자 호출자가 전달한 컬렉션이 변경되는 사이드 이팩트가 발생할 수 있을 것 같아요.

List<Participant> myList = new ArrayList<>();
myList.add(new Participant(new Name("zzaekkii"), new HandCards(new ArrayList<>()), false));

Participants participants = new Participants(myList);
System.out.println(myList.size()); // 딜러까지 추가됨

또한 BlackjackController에서 많은 책임을 지고 있어요.
Participant의 생성의 책임은 Participants에서 지게 하는게 어떨가요? 그렇게 되면 Participants에서는 생성자의 입력으로 참가자들의 이름만 받으면 될 것 같네요!

return cardResults;
}

private static void validatePlayerCounts(List<Participant> participants) {

Choose a reason for hiding this comment

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

해당 부분 테스트가 없네요.

@@ -1,3 +1,105 @@
# java-blackjack

블랙잭 미션 저장소

Choose a reason for hiding this comment

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

TDD 개발 방식에 대해서

실제로 TDD를 많이 활용하고 있어요.
구현하려는 요구사항이 명확할 때는 given-when-then 구조로 테스트를 먼저 작성한 후 로직을 구현하는 식으로 진행합니다.
다만 엄격하게 모든 메서드에 대해 TDD를 적용하지는 않아요. 말씀하신 것처럼 시간이 오래 걸리기도 하고, 현업에서는 마감일이나 트레이드오프가 있기 때문이에요 😢
그래서 주로 핵심 로직이나 중요한 도메인 코드에 한해서 TDD를 먼저 적용하는 편입니다.

랜덤 같은 통제 어려운 값에 대한 테스트

랜덤 같은 통제가 어려운 값은 최대한 테스트 가능한 구조로 리팩터링합니다.
예를 들어 랜덤 로직을 인터페이스로 분리하면, 테스트에서는 Fake나 Stub으로 구현해서 다른 로직들을 안정적으로 검증할 수 있어요.
다만 이렇게 해도 실제 랜덤 기능이 항상 정상 동작할 거라는 보장은 없기 때문에, 통제 불가능한 기능에는 예외 처리나 fallback, 추가 테스트를 꼭 넣는 편입니다. 🙂

패키지 구조를 어떻게 정리하는 것이 좋을지

저는 주로 객체들이 어떻게 협력하는지를 먼저 보고 패키지 구조를 설계하는 편입니다.
예를 들어 Card, CardSuit, CardRank/domain/card 패키지로 묶으면 한눈에 카드 관련 도메인을 파악할 수 있더라고요.

또한 패키지가 적절한지 판단할 때는 실제 import를 확인해보는 것도 좋은 방법입니다.
저는 리뷰할 때 domain 전체 import를 살펴보듯, 셀프 체크 관점으로 패키지 구조를 점검하면 기준을 세우는 데 도움이 됩니다.

Ex) 도메인과 인프라와 연관을 맺고 있군요.

Image

상속을 무조건 지양해야하나요?

상속을 무조건 지양해야 하는 건 아니라고 생각해요. 상속과 조합 모두 장단점이 있고, 상황과 도메인에 따라 적절히 선택하는 것이 중요해요.

실제로 저도 우테코 때 블랙잭을 구현했어서, 부끄럽지만 그때 구현했고 답변했던 저의 생각을 링크로 남기겠습니다.

메서드 길이 10라인 이내 규칙

우테코에서 메서드 길 10줄 제약을 왜 두었을까요? (질문하셨는데 답변이 아닌 역질문해서 죄송해요 😅) 블랙잭 구현하면서 재키가 생각하는 메서드 10줄의 장단점이 뭐였을지 궁금해요!

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

class BlackjackServiceTest {

Choose a reason for hiding this comment

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

전체적으로 경계값 및 예외 테스트가 더 필요해 보입니다!

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.

3 participants