Deck of Cards / Blackjack

LLD

Understand it step by step (한국어로 이해 → 영어로 말하기)

카드 한 벌(52장)과 그 위에서 도는 블랙잭 게임을 객체로 설계하는 문제입니다. 카드를 섞고 한 장씩 뽑을 수 있어야 하고, 플레이어와 딜러가 손패를 들고 21을 넘지 않게 점수를 계산해야 합니다. 핵심 요구사항은 (1) 카드/덱 모델링, (2) 손패 점수 계산, (3) 에이스(1 또는 11) 처리, (4) 버스트(21 초과) 판정입니다.
  1. 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. 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. 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. 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. 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. 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. 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)" : ""));
    }
}
🎙 이 카드 AI랑 음성 대화 · 기록 저장