Skip to content

[🚀 사이클2 - 미션 (블랙잭 베팅 기능)] 제제 미션 제출합니다.#1093

Open
alstj2384 wants to merge 58 commits intowoowacourse:alstj2384from
alstj2384:step2
Open

[🚀 사이클2 - 미션 (블랙잭 베팅 기능)] 제제 미션 제출합니다.#1093
alstj2384 wants to merge 58 commits intowoowacourse:alstj2384from
alstj2384:step2

Conversation

@alstj2384
Copy link

@alstj2384 alstj2384 commented Mar 12, 2026

사이클 2 블랙잭 미션 제출합니다! 🙋🏻‍♂️

저번 미션에서는 제출이 늦었지만, 이번에는 가이드에 따라 기능 구현 완료 시점에 맞춰 바로 제출합니다!

사이클 1에서 주신 피드백을 모두 반영하고 싶었으나, 우선순위를 신규 기능 구현(제출 기간 고려)에 두어 아직 반영하지 못한 부분이 많습니다. 😢
리뷰어님이 많은 리뷰이를 관리하시느라 이전 피드백을 기억하시기 어려울 수 있으니, 지난 피드백 중북 여부와 상관없이 편하게 피드백 남겨주세요! 😃
지난 피드백과 종합하여 반영하겠습니다!

체크 리스트

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

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

금액 처리에 대하여

[현재 상황]
사용자의 자산과 관련된 금액 데이터는 엄격해야 한다고 들어서, 이번 미션에서는 이 부분을 신경써보고 싶었습니다. 현재는 단판이지만, 여러 라운드가 진행되는 상황과 금액 제한 없는 배팅이 가능하도록 혼자만의 규칙을 세워 고민했습니다.

[고민 지점]
블랙잭 승리 시 배당률(1.5배)로 인해 소수점이 발생할 수 있는 상황에서, 두 가지 방향을 검토했습니다.

  1. 도메인 정책으로 제약(짝수만 배팅): 배팅 금액을 짝수로 제한하면 소수점 계산 문제를 원천 봉쇄할 수 있었습니다. 하지만 "내부 구현의 편의를 위해 도메인 로직(배팅 자유도)를 제한하는 것은 옳지 않다"고 생각이 들어 철회했습니다.
  2. 자료형 선택: 제한 없는 금액과 정밀한 계산을 위해 BigInteger와 BigDecimal을 고려했습니다:
  • BigInteger: 소수점 자리만큼 미리 곱하여 정수 연산으로 처리
  • BigDecimal: API에서 제공하는 소수점 연산 기능을 직접 활용

[최종 결정: BigDecimal]
두 방식 모두 구현해 본 결과, BigDecimal이 더 명확하게 관리할 수 있다고 판단하여 선택했습니다.

Player와 BetMoney 설계

[현재 상황]
현재 BlackjackGame이 List<String>으로 이름을 한 번에 받아 플레이어를 생성하는 구조다 보니, 이름 입력 후 배팅 금액을 추가로 받는 과정에서 설계를 세 가지 방향으로 고민했습니다.

1번: Player에서 BetMoney 필드를 final로 관리 & BetMoney 가변

Class Player{
    private final BetMoney betMoney // betMoney 가변
    public void setBetMoney(int value){
        betMoney.setValue(value);
    }
}

장점 : 기존 생성 방식 유지 가능
단점: BetMoney가 가변으로, 방어적 복사 필요 및 잠재적 버그 위험 존재, 결국 setter가 필요함.

2번: Player에서 BetMoney final없이 관리 & BetMoney 불변

Class Player{
    private BetMoney betMoney // betMoney 불변
    public void setBetMoney(int value){
        betMoney = BetMoney.of(value);
    }
}

장점 : 기존 생성 방식 유지 가능, BetMoney 불변으로 관리 가능
단점: Setter 사용 필요, 객체 생성 후 상태가 변하므로 잠재적 버그 위험 존재

3번: Player에서 BetMoney 필드를 final로 관리 & BetMoney 불변

Class Player{
    private final BetMoney betMoney // betMoney 불변
    public Player(... , int value){
        betMoney = BetMoney.of(value);
    }
    // setter 없음
}

장점 : 모두 불변으로, 잠재적 버그 위험에서 상대적으로 안전함.
단점: 현재 생성 로직 전면 수정 필요 및 View에서 이름/금액을 모두 한 번에 넘겨야 함

저는 우선 빠른 구현을 위해 2번을 선택했습니다.
금액 데이터(BetMoney) 자체는 불변하게 보호할 수 있었으나, 여전히 Player의 Setter를 통해 상태가 변경될 수 있다는 점이 마음에 걸렸습니다.

그래서 구조적 안전성을 위해 3번으로 리팩토링을 수행할까 합니다. (아직 못 했습니다!)
다만 3번 도입 시 발생하는 중복 검증에 대해 고민해 보았고, 다음과 같은 결론을 내렸습니다.

  • [상황] View에서 이름과 금액을 모아 도메인으로 넘겨야 하므로 View 단의 1차 검증(이름 중복, 길이, 음수 여부 등)과 도메인의 2차 검증이 겹치게 됩니다.
  • [사고] 처음에는 정책 노출과 검증 중복이 비효율적이라고 생각했으나, 웹 사이트의 회원가입 절차를 떠올리자 생각이 바뀌었습니다. 회원가입 시 아이디 길이, 중복, 비밀번호 길이 등의 정책이 노출되고 1차적으로 검증하는 것 처럼, View에서의 정책 노출과 검증도 비슷하다는 생각이 들었습니다.
  • [결론] 이름 중복이나 배팅 금액의 음수 제한같은 정책은 사용자도 알아야 할 규칙이므로, View에서 이를 표현하고 검증하는 것은 자연스러운 구조라고 판단했습니다!

결국 다음과 같은 구조를 갖게됩니다:

  1. 플레이어 이름 입력 & View에서 중복 및 길이 검증 (아직 Players 생성 x)
  2. 플레이어 배팅 금액 입력 & View에서 금액 검증
  3. 배팅 모두 입력 시 Players를 일괄적으로 생성(도메인 검증 수행)

[질문]
위와 같은 상황(입력 시기가 다른 데이터를 처리할 때)에 더 적절한 방법이 있을까요?
3번으로 리팩토링을 수행하기로 결정한 이유와 사고 과정은 타당하다고 생각하시나요?

도메인과 입출력의 분리에 대해서 (03.13 20:15 추가)

[질문]
"입력 방식의 추가로 도메인이 수정되는 상황"에 대해 크루와 토론을 통해 "입력 방식이 도메인에 영향을 주는 구조는 적절하지 않다" 라는 결론에 도달했습니다.
도메인은 외부의 입력 방식과 무관하게 본질적인 형태를 유지하고, Controller나 View 같은 외부 계층에서 도메인이 원하는 형태로 데이터를 가공해서 넘겨줘야 한다는 의견인데요!
이런 방향성은 적절할까요? 현업에서도 이런 구조로 개발이 진행되는지 궁금합니다!

Copy link

@Gomding Gomding left a comment

Choose a reason for hiding this comment

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

안녕하세요 제제!
사이클2 빠르게 진행해주셨네요 🙇
코멘트 남겼으니 확인부탁드려요~
이전 PR 에 머지하면서 남겼던 코멘트는 이번에 고려하지 않으신것 같은데
함께 반영해주세요

추가로 궁금한 점 있으면 DM 이나 코멘트 남겨주세요!

Copy link

Choose a reason for hiding this comment

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

사이클1에서 제가 리뷰를 못드렸네요 😢

Controller 가 BlackjackGame 을 상태로 가지고 있어도 괜찮을지 고민해보면 좋겠어요~

Copy link
Author

Choose a reason for hiding this comment

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

적절하지 않을 것 같습니다.
run() 메서드 안에서만 BlackjackGame이 사용되고 있기 때문에 run()의 지역 변수로 사용하도록 수정했습니다!

Copy link

Choose a reason for hiding this comment

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

어떤 이유로 적절하지 않다고 생각하셨는지도 궁금하네요!
Controller 가 상태를 가지면 어떤 단점이 있을까요?

Copy link
Author

Choose a reason for hiding this comment

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

우선 컨트롤러 1개당 상태 1개로 매핑되어 여러 개의 상태가 필요한 경우 컨트롤러를 여러 개 만들어야 할 수도 있겠네요!
또한 동시성 이슈도 떠올랐습니다. 단일 스레드 환경에서는 상태를 가지는 게 잠재적 위험이 없을 수도 있지만, 다중 스레드 환경에서는 동시성으로 인한 잠재적 위험이 생길 수 있다는 생각이 드네요!

Copy link

Choose a reason for hiding this comment

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

깊게 고민해주셨네요!! 💯 💯 💯
제제의 의견에 동의합니다 👍

public abstract class Participant {
protected final Name name;
protected final Hand hand;
protected BetMoney betMoney;
Copy link

Choose a reason for hiding this comment

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

딜러도 betMoney 를 사용하나요?

Copy link
Author

Choose a reason for hiding this comment

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

딜러에게 필요없는 부분까지 Participant가 포함하고 있던 것 같습니다!
Participant에는 플레이어와 딜러 모두에게 필요한 hand만 남기고, 나머지는 Player가 갖도록 수정했습니다!

부모 클래스가 정말 자식 클래스에게 공통적으로 필요한 정보만 담고있는지 잘 고민해야겠네요 😢
감사합니다!

Comment on lines 50 to 58
@@ -52,7 +58,7 @@ public void dealerHitStand(Consumer<Boolean> printDecisionOutput) {
}
Copy link

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
Author

@alstj2384 alstj2384 left a comment

Choose a reason for hiding this comment

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

안녕하세요 찰리!
피드백 반영하여 제출합니다.

피드백을 통해 성장감을 많이 느끼고 있습니다. 정말 감사합니다! 😊

public abstract class Participant {
protected final Name name;
protected final Hand hand;
protected BetMoney betMoney;
Copy link
Author

Choose a reason for hiding this comment

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

딜러에게 필요없는 부분까지 Participant가 포함하고 있던 것 같습니다!
Participant에는 플레이어와 딜러 모두에게 필요한 hand만 남기고, 나머지는 Player가 갖도록 수정했습니다!

부모 클래스가 정말 자식 클래스에게 공통적으로 필요한 정보만 담고있는지 잘 고민해야겠네요 😢
감사합니다!

Copy link
Author

Choose a reason for hiding this comment

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

적절하지 않을 것 같습니다.
run() 메서드 안에서만 BlackjackGame이 사용되고 있기 때문에 run()의 지역 변수로 사용하도록 수정했습니다!

Copy link

@Gomding Gomding left a comment

Choose a reason for hiding this comment

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

안녕하세요 제제!
전체적으로 잘 수정해주셨네요 :)
추가로 코멘트 남겼으니 확인부탁드려요!

궁금한 점 있으면 언제든 DM 이나 코멘트 남겨주세요~

Copy link

Choose a reason for hiding this comment

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

어떤 이유로 적절하지 않다고 생각하셨는지도 궁금하네요!
Controller 가 상태를 가지면 어떤 단점이 있을까요?

public static Score totalSum(int aceAmount, Score sum) {
for (int i = 0; i < aceAmount; i++) {
sum = sum.add(decideAceValue(sum, aceAmount - i - 1));
public static Score sumWithAce(int aceAmount, Score sumWithOutAce) {
Copy link

Choose a reason for hiding this comment

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

Ace가 여럿있을 떄 점수 합상에 버그가 있는것 같네요 🤔
수정하시고 테스트 코드도 함께 보강해주세요!

Copy link
Author

Choose a reason for hiding this comment

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

변수명 리팩토링 과정에서 sumWithOutAce를 수정하지 않아 발생했던 거 같습니다 😭

변수명 바꿀 때도 제대로 바꿨는지 주의해야겠습니다..
반영하여 수정 및 테스트 추가했습니다!

Copy link

Choose a reason for hiding this comment

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

리팩토링 과정에서 휴먼에러가 많이 발생할 수 있죠 :)
그렇기 때문에 리팩토링에는 테스트 코드가 존재해야하는게 전재조건 입니다 😄
그래야 리팩토링 후에도 정상 동작하는지 검증이 가능하니까요!

경험해보면서 테스트 케이스를 놓치지 않기위해 고민을 계속 해보시면 됩니다!!
테스트 코드는 잘 작성하고 계시니 걱정없겠네요 👍

}

public static Hand copyOf(Hand hand) {
return new Hand(hand.getCards());
Copy link

Choose a reason for hiding this comment

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

copy 라는 메서드 이름은 깊은 복사만을 생각할것 같은데
불변으로 반환할거라고는 예측하지 못할 수 있겠네요 🤔

다른 개발자가 복사후에 카드를 추가하고싶어서 add 를 호출한다면 에러가 발생할 수 있곘네요!
제제는 어떻게 생각하시나요?

Copy link
Author

Choose a reason for hiding this comment

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

동의합니다.. 수정을 허용하기 위해 getCards가 아닌, new ArrayList<>(...) 형태로 내려주는 게 적절할 것 같습니다!

깊은 복사와 불변 컬렉션을 나누어 생각하지 못했던 거 같네요.
감사합니다!

Copy link

Choose a reason for hiding this comment

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

만약 복사하며 불변 반환을 원했다면
메서드 이름에 불변을 반환하는것이 드러나게 변경하는 방법도 있습니다 :)

Comment on lines +15 to +17
private BetMoney(BigDecimal value) {
this.value = value;
}
Copy link

Choose a reason for hiding this comment

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

입력에서 어느정도 방어가 될 것 같지만
BetMoney 자체는 음수로 객체 생성이 가능하겠네요 🤔

Copy link
Author

@alstj2384 alstj2384 Mar 16, 2026

Choose a reason for hiding this comment

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

플레이어 패배 시 BetMoney가 음수가 되는 경우도 있어 검증을 추가하지 않았습니다!
입력은 음수로 받는 것이 잘못되었기에 검증하고, 내부적으로는 음수를 생성할 수 있게 의도했습니다.

Copy link

Choose a reason for hiding this comment

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

BetMoney 를 결과에서 사용하고 있는것은 몰랐네요 😅
동의합니다!

Copy link
Author

@alstj2384 alstj2384 left a comment

Choose a reason for hiding this comment

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

안녕하세요 찰리!

오늘 "상태 패턴"에 알게 되어 학습 후 적용하여 리팩토링 해보았습니다.(아직 많이 어렵습니다....)
그 외에도 흐름 제어를 담당하던 BlackjackGame을 삭제한 후, Controller에서 도메인을 활용하여 흐름을 제어하도록 수정했습니다!

Comment on lines +15 to +17
private BetMoney(BigDecimal value) {
this.value = value;
}
Copy link
Author

@alstj2384 alstj2384 Mar 16, 2026

Choose a reason for hiding this comment

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

플레이어 패배 시 BetMoney가 음수가 되는 경우도 있어 검증을 추가하지 않았습니다!
입력은 음수로 받는 것이 잘못되었기에 검증하고, 내부적으로는 음수를 생성할 수 있게 의도했습니다.

}

public static Hand copyOf(Hand hand) {
return new Hand(hand.getCards());
Copy link
Author

Choose a reason for hiding this comment

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

동의합니다.. 수정을 허용하기 위해 getCards가 아닌, new ArrayList<>(...) 형태로 내려주는 게 적절할 것 같습니다!

깊은 복사와 불변 컬렉션을 나누어 생각하지 못했던 거 같네요.
감사합니다!

public static Score totalSum(int aceAmount, Score sum) {
for (int i = 0; i < aceAmount; i++) {
sum = sum.add(decideAceValue(sum, aceAmount - i - 1));
public static Score sumWithAce(int aceAmount, Score sumWithOutAce) {
Copy link
Author

Choose a reason for hiding this comment

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

변수명 리팩토링 과정에서 sumWithOutAce를 수정하지 않아 발생했던 거 같습니다 😭

변수명 바꿀 때도 제대로 바꿨는지 주의해야겠습니다..
반영하여 수정 및 테스트 추가했습니다!

Comment on lines +21 to +29
public Result judge(State state) {
if (state instanceof Blackjack) {
return Result.LOSE;
}
if (state instanceof Bust) {
return Result.WIN;
}
return judgeByScore(state);
}
Copy link
Author

Choose a reason for hiding this comment

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

Hit와 Stay는 점수 계산 로직이 같아서 완전히 같은 코드가 Hit와 Stay에 중복으로 오버라이딩 되어야 했습니다.
이 중복을 줄이고자 State에서 기본 점수 계산 로직으로 구현 후,
특수한 계산이 있는 블랙잭, 버스트 클래스에서 오버라이딩하는 식으로 구성해 봤는데요!

Hit만의 계산 로직이 추가된다면 수정되어야 할 거 같고, Hit와 Stay의 judge 방식을 한 눈에 확인하기 어려운 구조라고도 생각이 들었습니다.
중복이 있더라도 각 클래스에 같은 로직을 작성하는 게 적절할까요?

Copy link

Choose a reason for hiding this comment

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

상속을 플랫한 구조로 생각하기 쉬운데
상속에도 계층 구조를 만들 수 있다는것을 힌트로 드려볼게요~ 😄

동물(부모 클래스) - 뱅갈 고양이, 시베리안 고양이, 리트리버, 말티즈, 진돗개 <-- 플랫한 구조

interface 동물 { sound() }

class 뱅갈고양이 extends 동물 { sound() { "냥냥" }  }
class 시베리안 고양이 extends 동물 { sound() { "냥냥" } }

class 리트리버 extends 동물 { sound() { "멍멍" } }
class 말티즈 extends 동물 { sound() { "멍멍" } }
class 진돗개 extends 동물 { sound() { "멍멍" } }

중복이 있는데 동물을 상속받은 클래스는 2개의 그룹으로 묶어볼 수 있곘죠? 😉

Copy link
Author

@alstj2384 alstj2384 Mar 18, 2026

Choose a reason for hiding this comment

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

좋은 예시와 의견 감사합니다!

상태 패턴을 적용하게 된 계기는 LMS의 강의 글을 보고 나서였는데요!

해당 강의에서는 계층 구조가 적용된 블랙잭의 상태 패턴을 예시로 보여주고 있었습니다.
처음에는 그 구조가 전혀 이해가 되지 않았기에, 부딪히면서 배워보자는 생각으로 리팩토링을 시도했던 거 같아요.

다만 "강의 글에서 이 구조로 했으니까 이 구조부터 만들고 시작하자" 라는 접근이 아니라, "내가 이 추상 클래스 계층의 필요성을 느끼면 넣겠다" 라고 나름 홍대병(?)걸린 마음가짐으로 하다보니 PR을 제출할 당시에는 그 이유를 찾지 못했습니다.

리팩토링 과정에서 남겨주신 피드백을 반영하기 위해 "논리적으로 맞지 않는 메서드(bust에서 draw가 가능한 것)의 예외를 추가"한 후, 각 구현체를 보니 공통된 로직을 발견했는데요.

바로 "끝난 상태의 구현체들은 draw와 stay에 모두 예외를 발생시키고 있다" 였습니다.
이에 이 구현체들의 공통점을 추상 클래스(Finished)에 정의하여 상속 계층을 추가했습니다.

현재는 이런식으로 점진적으로 상속 계층을 추가하여 반영한 상황입니다!

Copy link

@Gomding Gomding left a comment

Choose a reason for hiding this comment

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

안녕하세요 제제!
상태 패턴 도입해주셨네요 😄
몇가지 의견 남겼으니 확인해주세요~

궁금한 점 있으면 언제든 DM 이나 코멘트 남겨주세요!

Copy link

Choose a reason for hiding this comment

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

깊게 고민해주셨네요!! 💯 💯 💯
제제의 의견에 동의합니다 👍

public static Score totalSum(int aceAmount, Score sum) {
for (int i = 0; i < aceAmount; i++) {
sum = sum.add(decideAceValue(sum, aceAmount - i - 1));
public static Score sumWithAce(int aceAmount, Score sumWithOutAce) {
Copy link

Choose a reason for hiding this comment

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

리팩토링 과정에서 휴먼에러가 많이 발생할 수 있죠 :)
그렇기 때문에 리팩토링에는 테스트 코드가 존재해야하는게 전재조건 입니다 😄
그래야 리팩토링 후에도 정상 동작하는지 검증이 가능하니까요!

경험해보면서 테스트 케이스를 놓치지 않기위해 고민을 계속 해보시면 됩니다!!
테스트 코드는 잘 작성하고 계시니 걱정없겠네요 👍

}

public static Hand copyOf(Hand hand) {
return new Hand(hand.getCards());
Copy link

Choose a reason for hiding this comment

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

만약 복사하며 불변 반환을 원했다면
메서드 이름에 불변을 반환하는것이 드러나게 변경하는 방법도 있습니다 :)

Comment on lines +15 to +17
private BetMoney(BigDecimal value) {
this.value = value;
}
Copy link

Choose a reason for hiding this comment

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

BetMoney 를 결과에서 사용하고 있는것은 몰랐네요 😅
동의합니다!

Comment on lines +21 to +29
public Result judge(State state) {
if (state instanceof Blackjack) {
return Result.LOSE;
}
if (state instanceof Bust) {
return Result.WIN;
}
return judgeByScore(state);
}
Copy link

Choose a reason for hiding this comment

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

상속을 플랫한 구조로 생각하기 쉬운데
상속에도 계층 구조를 만들 수 있다는것을 힌트로 드려볼게요~ 😄

동물(부모 클래스) - 뱅갈 고양이, 시베리안 고양이, 리트리버, 말티즈, 진돗개 <-- 플랫한 구조

interface 동물 { sound() }

class 뱅갈고양이 extends 동물 { sound() { "냥냥" }  }
class 시베리안 고양이 extends 동물 { sound() { "냥냥" } }

class 리트리버 extends 동물 { sound() { "멍멍" } }
class 말티즈 extends 동물 { sound() { "멍멍" } }
class 진돗개 extends 동물 { sound() { "멍멍" } }

중복이 있는데 동물을 상속받은 클래스는 2개의 그룹으로 묶어볼 수 있곘죠? 😉

Comment on lines +12 to +16
@Override
public State draw(Card card) {
hand.add(card);
return new Bust(hand);
}
Copy link

Choose a reason for hiding this comment

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

draw 도 마찬가지 입니다 :)

현실세계에서 bust 된 플레이어가 카드를 뽑으려하면 당장 경고를 하고 멈추라 하겠죠?
프로그램에서는 어떻게 해야할까요~

+) 다른 상태 클래스들도 점검해주세요!

Copy link
Author

@alstj2384 alstj2384 Mar 18, 2026

Choose a reason for hiding this comment

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

이 부분도 고민이 많았습니다 😭

처음 생각은 Bust된 플레이어는 draw를 할 수 없으므로 예외를 발생시키려고 했습니다.
하지만 협업 관점에서 다른 개발자가 "얘는 draw 구현되어 있네? 써도 되겠다"라고 착각할 수도 있다는 생각도 들었습니다.
이에 "필요하지 않은 인터페이스는 갖지 않게 해야한다" 라는 생각으로 구조를 고민했으나, 답을 찾기 어려웠습니다 😭
따라서 일단 다른 개발자가 사용해도 혼동이 없도록 모든 인터페이스를 그럴듯하게 구현했습니다.

Blackjack에서 카드를 뽑을 수 없는 게 맞지만, 뽑게 된다면 Bust가 된다 라는 느낌입니다.

다만 다시 생각해 보면, 오히려 예외를 적절히 발생시키는 게 객체의 책임이라는 생각도 들었네요.
리팩토링 과정에서 논리적으로 불가능한 동작들은 모두 예외를 발생하도록 수정했습니다!

감사합니다!

Copy link

Choose a reason for hiding this comment

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

BlackjackGame 클래스가 없어진 이유가 있을까요? 😢

Copy link
Author

Choose a reason for hiding this comment

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

Controller에서 도메인을 활용해 흐름을 제어하자 라는 생각에 "게임의 흐름"을 담당하는 클래스의 필요 유무를 느끼지 못해서 삭제했습니다.

현재 Controller의 collectPlayerResults, calculatePlayerProfits, calculateDealerResult 메서드들은 그대로 BlackjackGame에 옮겨도 위화감이 없다고 느껴져서, Controller에서 수행하지 않아도 될 거 같다는 생각이 드네요.

너무 도메인을 직접 활용하려고 하니 Controller가 비대해진 느낌도 듭니다.

리팩토링을 통해 BlackjackGame을 다시 만들지 않은 이유는 HitStand 로직이 결국 getPlayers를 통해 컨트롤러에서 수행되어야 하기 때문인 거 같습니다.
약간 반쪽짜리 흐름 제어 객체다 라는 느낌이 들어 선뜻 다시 만들고 싶진 않았던 거 같아요 😭

더 깔끔한 (상태를 보관할 수 있는) 도메인을 만들어 냈다면, BlackjackGame의 역할도 분명하게 나뉠 수 있었을 거 같은데, 도메인 설계 역량이 부족한 탓이라는 생각도 드네요 😭

Copy link

Choose a reason for hiding this comment

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

상태 패턴 도입 배경은 학습을 위해서 일 것 같은데 맞을까요? 😄
상태 패턴을 도입하면서 어떤 장점이 느껴졌나요?

Copy link
Author

@alstj2384 alstj2384 Mar 18, 2026

Choose a reason for hiding this comment

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

LMS의 강의 글을 통해 상태 패턴을 적용할 수 있음을 깨달아 학습을 위해서 적용해 봤습니다.

상태 패턴을 적용하기 전에는 플레이어가 어떤 상태인지 cards에 계속해서 질의를 날려 계산해야 했습니다.
또한, 상태별 행위(draw, stay)가 구분되지 않는 하나의 메서드에서 뭉쳐져 있어 두 행위를 구분하기 어려웠습니다.

상태 패턴을 도입하고 상태별 행위가 메서드로 분명히 구분될 수 있었고, 메서드의 결과에 따라 상태 구현체가 변경되면서 자동으로 동작이 변경되어 "상태가 변경되었는가?"에 대한 확인 수요가 없어졌습니다.
결과적으로 유저가 Hit인지, Bust인지 확인하지 않고, 그저 클라이언트는 draw()를 호출하기만 하면 될 뿐이었습니다.

이런 부분이 정말 좋았던 것 같습니다!
다음 미션때도 적용할 수 있는 부분이 있다면 꼭 고려해 적용해 보고 싶네요!

Comment on lines +19 to +24
public State draw(Card card) {
hand.add(card);
if (hand.isBust()) {
return new Bust(hand);
}
return new Hit(hand);
Copy link

Choose a reason for hiding this comment

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

상태 전이를 잘 구현해주셨네요 👍

Comment on lines +20 to +24
hand.add(card);
if (hand.isBust()) {
return new Bust(hand);
}
return new Hit(hand);
Copy link

Choose a reason for hiding this comment

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

상태 전이 시 hand 를 그대로 넘겨주고 있는데
가능하면 새로 만들거나 불변으로 넘겨주는것은 어떻게 생각하시나요?
어떤 장점이 있을까요~

Copy link
Author

@alstj2384 alstj2384 Mar 18, 2026

Choose a reason for hiding this comment

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

가능하면 새로 만들거나 불변으로 넘겨주는것은 어떻게 생각하시나요?

저는 이 부분이 "기존 객체(컬렉션)가, 전이된 상태에 영향을 줄 수 없는 구조로 바꾸는 것은 어떤가?" 라고 해석됐습니다.

이를 바탕으로 생각해 보니, 현재 코드는 state의 객체는 계속해서 바뀌는데, hand 자체는 같은 객체로 유지되고 있었습니다.
만약 외부에서 hand를 변경하면, 이를 가지고 있는 상태는 책임과 맞지 않는 값을 지니게 됩니다.
예를 들면, 블랙잭 상태인 상태의 hand에 클로버 7을 추가하면, 손패 상태는 버스트인데 상태 자체는 블랙잭인 미스매치가 발생하게 됩니다.
이로 인해 의도치 않은 동작이 수행될 수 있다고 판단이 드네요!

현재 구조상 불변으로 넘겨주는 것은 불가능할 거 같아서 깊은 복사(기존 컬렉션과 연결을 끊어서 제공)하는 방법으로 수정했습니다!

감사합니다 ☺️

Copy link
Author

@alstj2384 alstj2384 left a comment

Choose a reason for hiding this comment

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

안녕하세요 찰리!
피드백 반영하여 제출합니다 😊

Copy link
Author

@alstj2384 alstj2384 Mar 18, 2026

Choose a reason for hiding this comment

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

LMS의 강의 글을 통해 상태 패턴을 적용할 수 있음을 깨달아 학습을 위해서 적용해 봤습니다.

상태 패턴을 적용하기 전에는 플레이어가 어떤 상태인지 cards에 계속해서 질의를 날려 계산해야 했습니다.
또한, 상태별 행위(draw, stay)가 구분되지 않는 하나의 메서드에서 뭉쳐져 있어 두 행위를 구분하기 어려웠습니다.

상태 패턴을 도입하고 상태별 행위가 메서드로 분명히 구분될 수 있었고, 메서드의 결과에 따라 상태 구현체가 변경되면서 자동으로 동작이 변경되어 "상태가 변경되었는가?"에 대한 확인 수요가 없어졌습니다.
결과적으로 유저가 Hit인지, Bust인지 확인하지 않고, 그저 클라이언트는 draw()를 호출하기만 하면 될 뿐이었습니다.

이런 부분이 정말 좋았던 것 같습니다!
다음 미션때도 적용할 수 있는 부분이 있다면 꼭 고려해 적용해 보고 싶네요!

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