Understand it step by step (한국어로 이해 → 영어로 말하기)
- 1
먼저 가장 작은 단위인 카드를 정합니다. 카드는 무늬(Suit)와 숫자(Rank)로만 이루어집니다. 무늬는 4종류로 고정이라 enum으로 만들고, Rank는 카드마다 점수 값을 가지므로 값을 들고 있는 enum으로 만듭니다.
⚖️ Trade-off: Rank를 단순 enum으로 둘지, 점수 값을 가진 enum으로 둘지가 갈립니다. 점수를 바깥에서 switch로 계산하면 로직이 흩어집니다.
✅ Fix: Rank enum 안에 value 필드를 넣어 각 카드가 자기 점수를 알게 합니다. JACK/QUEEN/KING은 10, ACE는 일단 11로 둡니다.
🔧 도구:enumSRPencapsulation
enum Suit { HEARTS, DIAMONDS, CLUBS, SPADES } enum Rank { TWO(2), THREE(3), FOUR(4), FIVE(5), SIX(6), SEVEN(7), EIGHT(8), NINE(9), TEN(10), JACK(10), QUEEN(10), KING(10), ACE(11); private final int value; Rank(int value) { this.value = value; } public int value() { return value; } }🗣 영어로 말해“Suit is a plain enum, and Rank is an enum that carries its own point value.”
checking microphone…
- 2
이제 카드 자체를 만듭니다. 카드는 무늬와 숫자를 합친 값일 뿐이고, 한 번 만들면 바뀌지 않습니다. 그래서 불변 데이터에 딱 맞는 record로 만듭니다.
⚖️ Trade-off: Card를 일반 class로 만들면 equals/hashCode/생성자를 직접 써야 하고 가변이 될 위험이 있습니다.
✅ Fix: Java 17 record를 써서 불변 + equals/hashCode 자동 생성을 얻습니다. toString만 보기 좋게 오버라이드합니다.
🔧 도구:recordimmutability
record Card(Rank rank, Suit suit) { @Override public String toString() { return rank + " of " + suit; } }🗣 영어로 말해“A card is just rank plus suit, so I model it as an immutable record.”
checking microphone…
- 3
다음은 덱(카드 한 벌)입니다. 덱은 52장을 모두 만들어 섞고, 위에서 한 장씩 뽑는 역할을 합니다. 뽑기/넣기가 한쪽 끝에서 일어나므로 스택처럼 동작하는 Deque를 씁니다.
⚖️ Trade-off: ArrayList로 두고 마지막 인덱스를 뽑을 수도 있지만, 의도(스택)가 코드에 드러나지 않습니다.
✅ Fix: ArrayDeque를 써서 pop()으로 위에서 한 장 뽑게 하고, 빈 덱은 예외로 막습니다.
🔧 도구:compositionDequefail-fast
class Deck { private final Deque<Card> cards = new ArrayDeque<>(); public Deck() { List<Card> list = new ArrayList<>(); for (Suit s : Suit.values()) for (Rank r : Rank.values()) list.add(new Card(r, s)); Collections.shuffle(list); cards.addAll(list); } public Card draw() { if (cards.isEmpty()) throw new IllegalStateException("Deck empty"); return cards.pop(); } public int size() { return cards.size(); } }🗣 영어로 말해“The deck builds all fifty-two cards, shuffles them, and draws from the top.”
checking microphone…
- 4
이제 핵심 동작인 손패(Hand)의 점수 계산입니다. 손패는 카드 리스트를 들고, 점수를 합산합니다. 여기서 에이스가 까다롭습니다. 에이스는 1 또는 11이 될 수 있기 때문입니다.
⚖️ Trade-off: 에이스를 어디서 1/11로 결정할지가 문제입니다. 카드에 박아두면 손패 전체 상황(다른 카드 합)을 알 수 없습니다.
✅ Fix: 에이스를 일단 11로 더한 뒤, 합이 21을 넘으면 에이스가 남아 있는 동안 10씩 깎습니다(11을 1로 강등). 이 로직은 손패 전체를 아는 Hand가 맡습니다.
🔧 도구:Information ExpertSRPencapsulation
class Hand { private final List<Card> cards = new ArrayList<>(); public void add(Card c) { cards.add(c); } public int score() { int total = 0, aces = 0; for (Card c : cards) { total += c.rank().value(); if (c.rank() == Rank.ACE) aces++; } while (total > 21 && aces > 0) { total -= 10; aces--; } return total; } public boolean isBust() { return score() > 21; } }🗣 영어로 말해“The hand counts each ace as eleven, then drops to one only while the total busts.”
checking microphone…
- 5
사람을 모델링합니다. 플레이어는 이름과 손패 하나를 가집니다. 딜러도 손패를 가지지만 규칙이 조금 달라서, 일단 Game이 딜러용 Hand를 직접 들고 있게 단순화합니다.
⚖️ Trade-off: 플레이어와 딜러를 상속(Dealer extends Player)으로 묶을지, 둘 다 그냥 Hand를 가진 참여자로 둘지 고민됩니다.
✅ Fix: 지금은 상속 대신 합성을 택합니다. 딜러의 특수 규칙(예: 17까지 무조건 hit)이 생기는 순간 별도 클래스로 분리하면 됩니다. 미리 상속 계층을 만들지 않습니다.
🔧 도구:composition-over-inheritanceYAGNI
class Player { private final String name; private final Hand hand = new Hand(); public Player(String name) { this.name = name; } public Hand hand() { return hand; } public String name() { return name; } }🗣 영어로 말해“A player simply owns one hand, and I prefer composition over a class hierarchy here.”
checking microphone…
- 6
마지막으로 게임 전체를 조율하는 Game을 만듭니다. Game은 덱 하나, 플레이어들, 딜러 손패를 들고, 초기 두 장 분배와 hit(한 장 더 받기)를 책임집니다.
⚖️ Trade-off: 점수 계산이나 버스트 판정을 Game에 넣으면 책임이 한 클래스에 몰립니다.
✅ Fix: Game은 흐름(분배/뽑기)만 맡고, 점수/버스트 판정은 Hand에 위임합니다. 각 클래스가 한 가지 책임만 갖게 유지합니다.
🔧 도구:SRPdelegationFacade-ish coordinator
class Game { private final Deck deck = new Deck(); private final List<Player> players = new ArrayList<>(); private final Hand dealer = new Hand(); public Game(int playerCount) { for (int i = 1; i <= playerCount; i++) players.add(new Player("P" + i)); } public void dealInitial() { for (int i = 0; i < 2; i++) { for (Player p : players) p.hand().add(deck.draw()); dealer.add(deck.draw()); } } public void hit(Player p) { p.hand().add(deck.draw()); } public int dealerScore() { return dealer.score(); } }🗣 영어로 말해“Game only coordinates dealing and hitting; scoring stays inside the hand.”
checking microphone…
- 7
엣지 케이스와 확장 포인트를 정리합니다. (1) 빈 덱에서 뽑기는 이미 예외로 막았습니다. (2) 에이스 여러 장(예: A+A=12)도 점수 루프가 처리합니다. (3) 여러 벌(슈)을 쓰려면 Deck 생성자만 바꾸면 됩니다. (4) 딜러 자동 규칙이나 셔플 전략은 나중에 인터페이스로 빼면 됩니다.
⚖️ Trade-off: 셔플 방식을 Collections.shuffle로 박아두면 시드 고정 테스트가 어렵습니다.
✅ Fix: 확장이 필요해지면 ShuffleStrategy 같은 인터페이스를 주입해 결정적 셔플을 끼웁니다. 지금은 과한 추상화를 피하고 필요할 때 분리합니다.
🔧 도구:Strategydependency injectionOCP
interface ShuffleStrategy { void shuffle(List<Card> cards); } // 추후: new Deck(new SeededShuffle(42)) 로 결정적 테스트 가능 // 딜러 규칙도 분리 예시: // while (dealer.score() < 17) dealer.add(deck.draw());🗣 영어로 말해“For extension I would inject a shuffle strategy and split out dealer rules later.”
checking microphone…
7단계 영어를 다 말하면 → 이 메커니즘 전체를 영어로 설명할 수 있게 된다.
Full Java (javac-verified)
import java.util.*;
public class Solution {
public static void main(String[] args) {
Game game = new Game(2);
game.dealInitial();
game.printState();
System.out.println("Dealer score: " + game.dealerScore());
}
}
enum Suit { HEARTS, DIAMONDS, CLUBS, SPADES }
enum Rank {
TWO(2), THREE(3), FOUR(4), FIVE(5), SIX(6), SEVEN(7), EIGHT(8),
NINE(9), TEN(10), JACK(10), QUEEN(10), KING(10), ACE(11);
private final int value;
Rank(int value) { this.value = value; }
public int value() { return value; }
}
record Card(Rank rank, Suit suit) {
@Override public String toString() { return rank + " of " + suit; }
}
class Deck {
private final Deque<Card> cards = new ArrayDeque<>();
public Deck() {
List<Card> list = new ArrayList<>();
for (Suit s : Suit.values())
for (Rank r : Rank.values())
list.add(new Card(r, s));
Collections.shuffle(list);
cards.addAll(list);
}
public Card draw() {
if (cards.isEmpty()) throw new IllegalStateException("Deck empty");
return cards.pop();
}
public int size() { return cards.size(); }
}
class Hand {
private final List<Card> cards = new ArrayList<>();
public void add(Card c) { cards.add(c); }
public int score() {
int total = 0, aces = 0;
for (Card c : cards) {
total += c.rank().value();
if (c.rank() == Rank.ACE) aces++;
}
while (total > 21 && aces > 0) { total -= 10; aces--; }
return total;
}
public boolean isBust() { return score() > 21; }
public List<Card> cards() { return List.copyOf(cards); }
}
class Player {
private final String name;
private final Hand hand = new Hand();
public Player(String name) { this.name = name; }
public Hand hand() { return hand; }
public String name() { return name; }
}
class Game {
private final Deck deck = new Deck();
private final List<Player> players = new ArrayList<>();
private final Hand dealer = new Hand();
public Game(int playerCount) {
for (int i = 1; i <= playerCount; i++) players.add(new Player("P" + i));
}
public void dealInitial() {
for (int i = 0; i < 2; i++) {
for (Player p : players) p.hand().add(deck.draw());
dealer.add(deck.draw());
}
}
public void hit(Player p) { p.hand().add(deck.draw()); }
public int dealerScore() { return dealer.score(); }
public void printState() {
for (Player p : players)
System.out.println(p.name() + ": " + p.hand().score()
+ (p.hand().isBust() ? " (BUST)" : ""));
}
}