Skip to content

[STEP 1] 블랙잭 구현하기#1

Open
oroi2009 wants to merge 16 commits into
mainfrom
step1
Open

[STEP 1] 블랙잭 구현하기#1
oroi2009 wants to merge 16 commits into
mainfrom
step1

Conversation

@oroi2009

@oroi2009 oroi2009 commented May 24, 2026

Copy link
Copy Markdown

체크 리스트

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

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

책임의 기준 정하기

'승패는 플레이어가 판단하는가?', '참가자들은 자신의 역할을 알고있어야 하는가? (dealer, player)' 등 해당 도메인의 책임 범위? 기준?을 잡는 것이 어려웠다.
이번 과제의 조건이었던 '인스턴스 변수를 2개 이하로만 가지도록' 내용을 지키는 과정을 통해서 책임의 기준에 대해 고민하고 생각하는 과정을 연습할 수 있었지만 마지막까지 어렵게만 느껴졌다.
플레이어가 승패의 조건을 알기 위해서는 본인의 점수와 딜러의 점수를 알고있어야 판단이 가능했다. 하지만 플레이어가 딜러의 점수까지 알아야 하는가?를 생각했을 때 딜러의 점수까지 알아야 한다는 것은 플레이어에게 과한 책임이라고 생각하여 심판을 도입하게 되었다.
이처럼 특정 기능을 수행하기 위해서 요구되는 상황, 조건들을 생각해봤을 때 이것이 결코 해당 도메인이 가질수 있는 적절한 책임인가?를 기준으로 잡았었는데 틀리거나 보완할 부분이 있는지, 혜정이의 기준은 또 무엇인지 궁금해.

테스트의 범위

TDD 방식을 처음 시도하여 굉장히 낯설었다. 처음 Card 부분에서도 조언을 듣기전가지 isACE를 확인하는 테스트는 생각도 못했다.
기능을 먼저 개발하고 테스트를 진행할 때에는 기능을 구현하던 과정에서 처리했던 예외를 확인해가며 테스트를 떠올리기 쉬웠는데,
반대로 테스트를 먼저 떠올리려고 하니 어떤 상황들이 발생할 수 있는지 그리기 어려웠다.
실제로 블랙잭을 참여하는 플레이어의 입장에서 전반적인 흐름을 생각해봤지만 그 안에 존재하는 단위테스트까지 완벽하게 캐치하지 못했다. 기능 구현을 생각하며 테스트를 작성하는 것은 뭔가 TDD 설계의 의도와는 맞지 않는 것 같은 느낌이었는데 혜정이는 TDD 설계를 어떻게 본인의 것으로 녹여내고 있는지 궁금해.

@oroi2009 oroi2009 requested a review from frombunny May 24, 2026 12:49
@oroi2009 oroi2009 self-assigned this May 24, 2026
* 플레이어에게 Hit/Stand 여부 확인
*/
private void askPlayerHit(Participant player, Deck deck){
while(inputView.askHit(player.getName())){

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

플레이어가 더 이상 카드를 받을 수 없는 상태가 되면 입력을 다시 묻지 않고 턴을 종료하는 흐름을 의도하신 걸까요?

제안: while (player.canReceiveCard() && inputView.askHit(player.getName()))처럼 턴 지속 조건에 점수 상태를 포함하거나, PlayerTurn 같은 도메인/서비스 객체가 한 턴의 종료 조건을 판단하도록 분리해보면 어떨까요? 또한 카드를 받은 직후 Bust 여부를 확인해 즉시 안내하고 반복을 종료하는 테스트를 추가하면 좋겠습니다.

}

public Card draw(){
if(cards.isEmpty()){

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

카드가 부족한 상황을 게임 진행 불가 상태로 다루려는 의도였을까요, 아니면 자동으로 새 덱을 준비하는 것을 룰로 보신 걸까요?

제안: draw()에서 카드가 없으면 예외를 던지거나, Deck.hasNext()/remainingCount()를 제공해 게임 진행 계층에서 명시적으로 처리해보면 어떨까요? 테스트도 '덱이 비면 새 덱 생성'보다 '덱이 비면 draw 불가' 또는 '카드 존재 여부를 확인한다'로 요구사항에 맞춰 조정하는 편이 안전해 보입니다.

public List<String> inputPlayers(){
System.out.println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)");
String input = scanner.nextLine();
return Arrays.stream(input.split(",")).map(String::trim).toList();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

이름 검증 책임을 InputView에 둘지, PlayerName 같은 도메인 값 객체에 둘지 고민해보셨을까요?

제안: 최소한 inputPlayers()에서 trim 후 빈 값 제거/거부, 중복 검사, 참가자 수 검사를 수행하거나, Player 생성 시 이름 검증을 강제해보면 어떨까요? 입력 실패 시 재입력을 받을지 예외를 던질지도 요구사항에 맞게 명시하면 테스트하기 쉬워집니다.

* 2. 딜러 draw 여부 확인 (16점 이하인지)
*/
private void askHitOrStand(List<Participant> participants, Deck deck){
Participant dealer = participants.getFirst();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

딜러와 플레이어 목록을 하나의 List<Participant>로 묶은 이유는 '동일하게 카드를 받는 참가자'라는 공통점을 활용하기 위한 선택이었을까요?

제안: Participants 또는 BlackjackGame 같은 객체가 Dealer dealer, List<Player> players를 명시적으로 보유하고, 공통 순회가 필요할 때만 allParticipants()를 제공하는 구조는 어떨까요? 그러면 딜러 위치 불변식을 한 곳에서 관리할 수 있습니다.

}
}

public void printGameResult(List<Participant> participants, ResultJudge judge){

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

심판을 도입해 승패 책임을 분리한 의도는 좋아 보이는데, 최종 결과 계산의 호출 위치가 View에 있는 것이 의도한 책임 분리와 잘 맞을까요?

제안: 컨트롤러나 도메인 게임 객체가 Map<Player, GameResult> 또는 GameResults를 만든 뒤, ResultView는 그 결과를 출력만 하도록 바꿔보면 어떨까요? 딜러 집계도 GameResults의 책임으로 두면 View의 역할이 더 명확해질 것 같습니다.

import domain.participant.Participant;

public class ResultJudge {
public GameResult checkPlayerGameResult(Participant dealer, Participant player){

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

TDD 관점에서 게임 결과 규칙을 먼저 예시로 나열한다면 어떤 케이스들이 가장 중요한 도메인 규칙이라고 생각하시나요?

제안: ResultJudgeTest를 추가해 player bust -> LOSE, dealer bust -> WIN, dealer score > player score -> LOSE, same score -> PUSH를 명시적으로 검증해보면 어떨까요? 참가자 생성과 카드 세팅이 번거롭다면 테스트 헬퍼를 두는 것도 좋습니다.

public class Deck {
private final List<Card> cards = new ArrayList<>();

public Deck() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

덱의 생성과 셔플을 한 번에 묶은 것은 '게임 시작 전 항상 섞인 덱'이라는 요구사항을 강제하기 위한 의도였을까요?

제안: 운영용으로는 Deck.shuffled() 같은 팩토리를 두고, 테스트용으로는 카드 목록을 주입할 수 있는 생성자를 제공하는 방식은 어떨까요? shuffle()이 외부에서 꼭 필요하지 않다면 접근 범위를 줄이는 것도 고려해볼 수 있습니다.

import domain.card.enums.Suit;

public class Card {
private Rank rank;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

카드를 불변 객체로 두려는 의도였다면 필드에도 그 의도를 표현해보는 것은 어떨까요?

제안: private final Rank rank;, private final Suit suit;로 선언해 카드가 생성 후 변하지 않음을 명시해보면 어떨까요? 같은 맥락에서 GameResult.labelfinal로 둘 수 있습니다.

do {
System.out.println(playerName + "는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)");
input = scanner.nextLine();
}while(!isValidHitResponse(input));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

입력 검증 실패를 조용히 재시도하는 방식과, 오류 메시지를 보여주는 방식 중 어떤 사용자 경험을 의도하셨나요?

제안: isValidHitResponse()가 false일 때 y 또는 n만 입력 가능하다는 메시지를 출력하거나, 입력 검증을 별도 메서드로 분리해 실패 케이스 테스트를 추가해보면 어떨까요?

@frombunny frombunny left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Q. 책임의 기준 정하기

나는 책임을 나눌 때 “이 객체가 그 역할을 수행하는 것이 자연스러운가?“를 가장 많이 고민하는 것 같네~

예를 들어 플레이어가 승패를 판단하려면 딜러의 점수도 알아야 하는데, 나는 플레이어가 자신의 카드뿐 아니라 상대방의 상태까지 알아야 하는 구조가 자연스럽지 않다고 느껴서 승패 판정은 별도의 객체가 더 적절하다고 생각했어.

또한 객체가 자신의 상태만으로 수행할 수 있는 행동인지도 중요하게 보는 것 같아.
(자기가 가진 정보로만 그 일을 할 수 있는지?)


Q. 테스트의 범위

테스트... 나도 테스트를 많이 짜보지 않아서 처음에 되게 막막했는데, 이 당시에는 요구사항을 기준으로 판단했어.

예를 들어 딜러는 처음에 받은 2장의 합계가 16이하이면 반드시 1장의 카드를 추가로 받아야 하고, 17점 이상이면 추가로 받을 수 없다.라는 요구사항에 대해서 세 개의 테스트케이스를 작성했어.

a) 딜러가 처음 받은 2장의 합계가 16 이하일 경우 카드 1장을 추가로 받는다.
b) 딜러가 처음 받은 2장의 합계가 16일 경우 카드 1장을 추가로 받는다.
c) 딜러가 처음 받은 2장의 합계가 16을 초과할 경우 카드를 추가로 받을 수 없다.

이것처럼 요구사항에서 경계가 되는 값과 분기를 먼저 찾아 테스트로 옮겼어.
그 후에는 테스트 커버리지를 높이려고 하다 보니 자연스럽게 테스트를 더 많이 작성하게 되더라.
물론 지금은 단순히 커버리지를 높이는 것 자체가 목표는 아니라고 생각하지만, 처음 테스트를 익히는 과정에서는 꽤 좋은 장치였던 것 같아.

Image

요즘은 테스트를 작성할 때 “이 코드가 동작하는가?“보다 “이 테스트를 통해 무엇을 검증하려고 하는가?“를 더 많이 생각하는 것 같아.

예전에는 테스트 개수나 커버리지에 집중했다면, 지금은 이 테스트가 비즈니스 규칙을 검증하는지, 중요한 요구사항을 보호하고 있는지를 먼저 보려고 해.

그래서 TDD를 완전히 내 것으로 만들었다고 말하기는 어렵지만, 예전보다 요구사항과 규칙을 테스트로 표현하려는 방향으로 조금씩 바뀌고 있는 것 같아.

성진이는 TDD가 처음이니, 일단 요구사항에서 검증해야 할 테스트케이스가 무엇인지 생각해보고 커버리지를 확인해보면서 연습해나가는 게 좋을 것 같네~

Comment on lines +23 to +25
public String displayName(){
return rank.getName() + suit.getName();
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Card가 자신의 표현 방식까지 책임지는 것이 자연스러울까요?

}

public boolean isBust() {
return score() > 21;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

만약 블랙잭이 21이 아니라 22가 된다면, 몇 군데를 수정해야 할까요?
의미있는 값으로 나타낼 수는 없을까요?

@@ -0,0 +1,12 @@
package domain.participant;

public class Dealer extends Participant{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

추상 클래스는 어떤 단점이 있을까요?

Comment on lines +35 to +45
public int getScore(){
return hand.score();
}

public boolean isBust(){
return hand.isBust();
}

protected int checkScore(){
return hand.score();
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

checkScore와 getScore의 차이는 무엇인가요?

import domain.participant.Participant;

public class ResultJudge {
public GameResult checkPlayerGameResult(Participant dealer, Participant player){

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

블랙잭 점수 계산 테스트와 별개로, 승패 판정의 각 분기를 독립적으로 테스트해보면 어떨까요?

제안: player bust -> LOSE, dealer bust -> WIN, dealer higher -> LOSE, player higher -> WIN, same score -> PUSH를 각각 또는 파라미터화 테스트로 추가해보면 좋겠습니다.

public class ParticipantTest {

@Test
void 딜러는_현재_들고있는_카드의_합계가_16이하이면_canReceiveCard가_true이다(){

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

역할별 카드 수령 가능 조건의 경계값을 테스트 이름에 드러내서 추가해보면 어떨까요?

제안: 딜러는 16점 true, 17점 false; 플레이어는 20점 true, 21점 false, 22점 false 같은 케이스를 파라미터화 테스트로 추가해볼 수 있습니다.

@frombunny frombunny changed the title 블랙잭 구현하기 [STEP 1] 블랙잭 구현하기 Jun 16, 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.

2 participants