diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000000..b819b8e049
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,90 @@
+# 블랙잭 규칙
+
+## 참여자
+
+**승**
+
+* (딜러 카드 합 < 참여자 카드 합) && 참여자가 버스트가 아닌 경우
+* (딜러 카드 합 == 참여자 카드 합) && (딜러 카드 수 > 참여자 카드 수) && 참여자가 버스트가 아닌 경우
+* 딜러가 버스트인 경우 && 참여자가 버스트가 아닌 경우
+
+**무**
+
+* (딜러 카드 합 == 참여자 카드 합) && (딜러 카드 수 == 참여자 카드 수) && 참여자가 버스트가 아닌 경우
+
+**패**
+
+* 참여자가 버스트인 경우
+* (딜러 카드 합 > 참여자 카드 합)
+
+
+
+# 기능 요구 사항
+
+* [x] 참여자 이름 입력 받는다.
+ * [x] 양 끝 공백을 제거한다.
+ * [x] 참여자는 쉼표(,)로 구분한다.
+ * [x] null 이나 공백인 경우 예외가 발생한다.
+ * [x] 쉼표로 시작시 예외가 발생한다. ex) ,pobi,jason
+ * [x] 쉼표로 끝날시 예외가 발생한다. ex) pobi,jason,
+ * [x] 쉼표가 연속으로 올시 예외가 발생한다. ex) pobi,,jason
+ * [x] 참여자 이름이 중복시 예외가 발생한다.
+* [x] 총 참여자의 수는 2이상 8이하여야 한다.
+
+
+
+* [x] 딜러가 카드 2장을 분배하다.
+ * [x] 카드는 총 6벌을 둔다. (52 * 6)
+* [x] 딜러의 카드를 출력한다. (1장)
+* [x] 참여자의 카드를 출력한다. (2장)
+
+
+
+* [x] 참여자는 hit(y) / stay(n)를 선택한다.
+ * [x] y, n 가 아닐시 예외가 발생한다.
+* [x] 참여자가 hit을 하는 경우 현재 가지고 있는 카드를 출력한다.
+* [x] 참여자가 hit을 한 적 없이 stay를 하는 경우 현재 가지고 있는 카드를 출력한다.
+* [x] 카드 합이 블랙잭인 경우 블랙잭 메시지를 출력한다.
+* [x] 카드 합이 21 초과시 버스트 메시지를 출력한다.
+
+
+
+* [x] 딜러의 카드의 합을 계산한다.
+* [x] 카드 내의 ACE 가 포함된 경우
+ * ACE: 11
+ * 11로 했을 때 카드의 합이 21을 초과한 경우 1로 계산
+* [x] 17 이상이 될때까지 카드를 받는다.
+
+
+
+* [x] 모든 참여자의 카드의 합을 계산한다.
+* [x] 딜러의 승패무를 확인한다.
+* [x] 참여자의 승패무를 확인한다.
+* [x] 게임 결과를 출력한다.
+
+---
+
+## 리팩터링 목록
+
+~~추상 클래스 활용~~
+
+* [x] 카드덱이 비었을때 꺼낸 경우 예외 발생
+* [x] createEmptyPacket 메서드명 수정
+* [x] 참가자의 이름 딜러 불가
+* [x] Name 객체 포장하기
+* [x] Result 함수형 프로그래밍 사용
+* [x] TestFixture 사용
+* [x] 패키지 정리
+* [x] 메서드 컨벤션 작성 및 확인
+* [x] final 컨벤션 통일
+* [x] CardDeck 생성자 닫기
+
+
+
+* [x] 예외 시 재입력 기능
+* [x] 예외 메시지 상수화
+ * 커스텀 예외 구현
+* [x] CardDeck 웨어하우스로 바라보기
+* [ ] ~~컨트롤러 메서드 수정하기~~
+* [x] ~~Participant 추상 클래스 대신 클래스로 변경하기~~
+ * 추상 클래스를 유지하고 추상 메서드 추가
diff --git a/src/main/java/application/Application.java b/src/main/java/application/Application.java
new file mode 100644
index 0000000000..2d98d55bfc
--- /dev/null
+++ b/src/main/java/application/Application.java
@@ -0,0 +1,18 @@
+package application;
+
+import controller.BlackJackController;
+import controller.InputController;
+import java.util.Scanner;
+import view.InputView;
+import view.OutputView;
+
+public class Application {
+ public static void main(String[] args) {
+ final Scanner scanner = new Scanner(System.in);
+ final InputView inputView = new InputView(scanner);
+ final OutputView outputView = new OutputView();
+ final InputController inputController = new InputController(inputView, outputView);
+ final BlackJackController blackJackController = new BlackJackController(inputController, outputView);
+ blackJackController.run();
+ }
+}
diff --git a/src/main/java/constants/ErrorCode.java b/src/main/java/constants/ErrorCode.java
new file mode 100644
index 0000000000..d6278bf1c5
--- /dev/null
+++ b/src/main/java/constants/ErrorCode.java
@@ -0,0 +1,15 @@
+package constants;
+
+public enum ErrorCode {
+
+ NOT_EXIST_MESSAGE,
+ INVALID_SEPARATOR,
+ INVALID_INPUT,
+ INVALID_SIZE,
+ DUPLICATE_NAME,
+ RESERVED_NAME,
+ BLANK_VALUE,
+ EMPTY_CARD,
+ INVALID_COMMAND,
+ ;
+}
diff --git a/src/main/java/controller/BlackJackController.java b/src/main/java/controller/BlackJackController.java
new file mode 100644
index 0000000000..e44b17ed96
--- /dev/null
+++ b/src/main/java/controller/BlackJackController.java
@@ -0,0 +1,92 @@
+package controller;
+
+import domain.Answer;
+import domain.participant.Dealer;
+import domain.participant.Player;
+import domain.participant.Players;
+import dto.DealerHandsDto;
+import dto.ParticipantDto;
+import dto.ParticipantsDto;
+import view.OutputView;
+
+public class BlackJackController {
+
+ private final InputController inputController;
+ private final OutputView outputView;
+
+
+ public BlackJackController(final InputController inputController, final OutputView outputView) {
+ this.inputController = inputController;
+ this.outputView = outputView;
+ }
+
+ public void run() {
+ final Players players = inputController.getPlayers();
+ final Dealer dealer = new Dealer();
+
+ initHands(players, dealer);
+ dealWithPlayers(players, dealer);
+
+ if (!players.isAllBust()) {
+ dealer.deal();
+ printDealerTurnMessage(dealer.countAddedHands());
+ }
+
+ printFinalResult(players, dealer);
+ }
+
+ private void printDealerTurnMessage(final int turn) {
+ for (int i = 0; i < turn; i++) {
+ outputView.printDealerTurnMessage();
+ }
+ }
+
+ private void dealWithPlayers(final Players players, final Dealer dealer) {
+ for (Player player : players.getPlayers()) {
+ deal(player, dealer);
+ }
+ }
+
+ private void initHands(final Players players, final Dealer dealer) {
+ dealer.initHands(players);
+ outputView.printStartDeal(DealerHandsDto.from(dealer), ParticipantsDto.of(players));
+ }
+
+ private void printFinalResult(final Players players, final Dealer dealer) {
+ outputView.printHandsResult(ParticipantsDto.of(dealer, players));
+ outputView.printGameResult(dealer.getDealerResult(players), players.getPlayersResult(dealer));
+ }
+
+ private void deal(final Player player, final Dealer dealer) {
+ boolean handsChanged = false;
+ boolean turnEnded = false;
+
+ while (!turnEnded) {
+ final Answer answer = inputController.getAnswer(player.getName());
+ dealer.deal(player, answer);
+
+ printHandsIfRequired(player, handsChanged, answer);
+
+ handsChanged = true;
+ turnEnded = isTurnEnded(player, answer);
+ }
+ }
+
+ private void printHandsIfRequired(final Player player, final boolean handsChanged, final Answer answer) {
+ if (shouldShowHands(handsChanged, answer)) {
+ outputView.printHands(ParticipantDto.from(player));
+ }
+ }
+
+ private boolean isTurnEnded(final Player player, final Answer answer) {
+ if (player.canDeal()) {
+ return !answer.isHit();
+ }
+ outputView.printDealEndMessage(player.isBust());
+ return true;
+ }
+
+ private boolean shouldShowHands(final boolean handsChanged, final Answer answer) {
+ return answer.isHit() || !handsChanged;
+ }
+}
diff --git a/src/main/java/controller/InputController.java b/src/main/java/controller/InputController.java
new file mode 100644
index 0000000000..4f9bb5df96
--- /dev/null
+++ b/src/main/java/controller/InputController.java
@@ -0,0 +1,55 @@
+package controller;
+
+import domain.Answer;
+import domain.participant.Players;
+import exception.CustomException;
+import java.util.List;
+import view.InputView;
+import view.OutputView;
+
+public class InputController {
+
+ private final InputView inputView;
+ private final OutputView outputView;
+
+ public InputController(final InputView inputView, final OutputView outputView) {
+ this.inputView = inputView;
+ this.outputView = outputView;
+ }
+
+ public Players getPlayers() {
+ Players players;
+ do {
+ players = readPlayers();
+ } while (players == null);
+ return players;
+ }
+
+ public Answer getAnswer(String name) {
+ Answer answer;
+ do {
+ answer = readAnswer(name);
+ } while (answer == null);
+ return answer;
+ }
+
+ private Players readPlayers() {
+ try {
+ List rawNames = inputView.readNames();
+ return Players.from(rawNames);
+ } catch (CustomException exception) {
+ outputView.printException(exception.getErrorCode());
+ return null;
+ }
+ }
+
+ private Answer readAnswer(String name) {
+ try {
+ String value = inputView.readAnswer(name);
+ return Answer.from(value);
+ } catch (CustomException exception) {
+ outputView.printException(exception.getErrorCode());
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/domain/Answer.java b/src/main/java/domain/Answer.java
new file mode 100644
index 0000000000..dc53bfc33a
--- /dev/null
+++ b/src/main/java/domain/Answer.java
@@ -0,0 +1,28 @@
+package domain;
+
+import constants.ErrorCode;
+import exception.InvalidCommandException;
+import java.util.Arrays;
+
+public enum Answer {
+
+ HIT("y"),
+ STAY("n");
+
+ private final String value;
+
+ Answer(final String value) {
+ this.value = value;
+ }
+
+ public static Answer from(final String value) {
+ return Arrays.stream(Answer.values())
+ .filter(answer -> answer.value.equals(value))
+ .findFirst()
+ .orElseThrow(() -> new InvalidCommandException(ErrorCode.INVALID_COMMAND));
+ }
+
+ public boolean isHit() {
+ return HIT.equals(this);
+ }
+}
diff --git a/src/main/java/domain/Result.java b/src/main/java/domain/Result.java
new file mode 100644
index 0000000000..15cdb94994
--- /dev/null
+++ b/src/main/java/domain/Result.java
@@ -0,0 +1,55 @@
+package domain;
+
+import domain.participant.Hands;
+import java.util.Arrays;
+import java.util.function.BiPredicate;
+
+public enum Result {
+
+ WIN("승", Result::winningCondition),
+ TIE("무", Result::tieCondition),
+ LOSE("패", Result::loseCondition);
+
+ private final String value;
+ private final BiPredicate condition;
+
+ Result(final String value, final BiPredicate condition) {
+ this.value = value;
+ this.condition = condition;
+ }
+
+ public Result reverse() {
+ if (Result.WIN.equals(this)) {
+ return LOSE;
+ }
+ if (Result.LOSE.equals(this)) {
+ return WIN;
+ }
+ return TIE;
+ }
+
+ public static Result calculateOf(final Hands hands, final Hands target) {
+ return Arrays.stream(Result.values())
+ .filter(result -> result.condition.test(hands, target))
+ .findFirst()
+ .orElseThrow();
+ }
+
+ private static boolean winningCondition(final Hands hands, final Hands target) {
+ return (!hands.isBust() && target.isBust())
+ || (hands.sum() > target.sum() && !hands.isBust())
+ || (hands.sum() == target.sum() && hands.size() < target.size() && !hands.isBust());
+ }
+
+ private static boolean tieCondition(final Hands hands, final Hands target) {
+ return hands.sum() == target.sum() && hands.size() == target.size() && !hands.isBust();
+ }
+
+ private static boolean loseCondition(final Hands hands, final Hands target) {
+ return hands.isBust() || hands.sum() < target.sum() || !target.isBust();
+ }
+
+ public String getValue() {
+ return value;
+ }
+}
diff --git a/src/main/java/domain/card/Card.java b/src/main/java/domain/card/Card.java
new file mode 100644
index 0000000000..f32cdd138f
--- /dev/null
+++ b/src/main/java/domain/card/Card.java
@@ -0,0 +1,63 @@
+package domain.card;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+public class Card {
+
+ private static final List CACHE = new ArrayList<>();
+
+ static {
+ for (Shape shape : Shape.values()) {
+ generateCard(shape);
+ }
+ }
+
+ private final Rank rank;
+ private final Shape shape;
+
+ public Card(final Rank rank, final Shape shape) {
+ this.rank = rank;
+ this.shape = shape;
+ }
+
+ private static void generateCard(final Shape shape) {
+ for (Rank rank : Rank.values()) {
+ CACHE.add(new Card(rank, shape));
+ }
+ }
+
+ public static List values() {
+ return List.copyOf(CACHE);
+ }
+
+ public boolean isAce() {
+ return rank.isAce();
+ }
+
+ public int getCardNumber() {
+ return rank.getValue();
+ }
+
+ @Override
+ public boolean equals(final Object target) {
+ if (this == target) {
+ return true;
+ }
+ if (!(target instanceof Card card)) {
+ return false;
+ }
+ return Objects.equals(rank, card.rank) && Objects.equals(shape, card.shape);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(rank, shape);
+ }
+
+ @Override
+ public String toString() {
+ return rank.getName() + shape.getName();
+ }
+}
diff --git a/src/main/java/domain/card/CardDeck.java b/src/main/java/domain/card/CardDeck.java
new file mode 100644
index 0000000000..90432e4fa5
--- /dev/null
+++ b/src/main/java/domain/card/CardDeck.java
@@ -0,0 +1,39 @@
+package domain.card;
+
+import constants.ErrorCode;
+import exception.NoMoreCardException;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.List;
+
+public class CardDeck {
+
+ private final Deque cards;
+
+ public CardDeck(final Deque cards) {
+ this.cards = cards;
+ }
+
+ protected static CardDeck generate(int size) {
+ final List deck = new ArrayList<>();
+
+ for (int i = 0; i < size; i++) {
+ deck.addAll(Card.values());
+ }
+ Collections.shuffle(deck);
+ return new CardDeck(new ArrayDeque<>(deck));
+ }
+
+ public Card pop() {
+ if (cards.isEmpty()) {
+ throw new NoMoreCardException(ErrorCode.EMPTY_CARD);
+ }
+ return cards.pop();
+ }
+
+ public int size() {
+ return cards.size();
+ }
+}
diff --git a/src/main/java/domain/card/Rank.java b/src/main/java/domain/card/Rank.java
new file mode 100644
index 0000000000..aa92ef3465
--- /dev/null
+++ b/src/main/java/domain/card/Rank.java
@@ -0,0 +1,38 @@
+package domain.card;
+
+public enum Rank {
+
+ ACE("A", 1),
+ TWO("2", 2),
+ THREE("3", 3),
+ FOUR("4", 4),
+ FIVE("5", 5),
+ SIX("6", 6),
+ SEVEN("7", 7),
+ EIGHT("8", 8),
+ NINE("9", 9),
+ TEN("10", 10),
+ JACK("J", 10),
+ QUEEN("Q", 10),
+ KING("K", 10);
+
+ private final String name;
+ private final int value;
+
+ Rank(final String name, final int value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ public boolean isAce() {
+ return this == ACE;
+ }
+
+ public int getValue() {
+ return value;
+ }
+
+ public String getName() {
+ return name;
+ }
+}
diff --git a/src/main/java/domain/card/Shape.java b/src/main/java/domain/card/Shape.java
new file mode 100644
index 0000000000..093d73cfa8
--- /dev/null
+++ b/src/main/java/domain/card/Shape.java
@@ -0,0 +1,19 @@
+package domain.card;
+
+public enum Shape {
+
+ SPADE("스페이드"),
+ HEART("하트"),
+ CLOVER("클로버"),
+ DIAMOND("다이아몬드");
+
+ private final String name;
+
+ Shape(final String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+}
diff --git a/src/main/java/domain/participant/Dealer.java b/src/main/java/domain/participant/Dealer.java
new file mode 100644
index 0000000000..08cc130792
--- /dev/null
+++ b/src/main/java/domain/participant/Dealer.java
@@ -0,0 +1,79 @@
+package domain.participant;
+
+import domain.Answer;
+import domain.Result;
+import domain.card.Card;
+import domain.card.CardDeck;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+
+public class Dealer extends Participant {
+
+ public static final int INIT_HANDS_SIZE = 2;
+ public static final int THRESHOLD = 16;
+ public static final int DECK_SIZE = 6;
+ public static final Name DEALER_NAME = new Name("딜러");
+
+ private final CardDeck cardDeck;
+
+ public Dealer() {
+ super(DEALER_NAME, Hands.createEmptyHands());
+ this.cardDeck = new CardDeck(generate());
+ }
+
+ public Dealer(final Hands hands) {
+ super(DEALER_NAME, hands);
+ this.cardDeck = new CardDeck(generate());
+ }
+
+ private static ArrayDeque generate() {
+ final List deck = new ArrayList<>();
+ for (int i = 0; i < DECK_SIZE; i++) {
+ deck.addAll(Card.values());
+ }
+ Collections.shuffle(deck);
+ return new ArrayDeque<>(deck);
+ }
+
+ public void initHands(final Players players) {
+ for (int i = 0; i < INIT_HANDS_SIZE; i++) {
+ players.forEach(player -> player.add(cardDeck.pop()));
+ super.add(cardDeck.pop());
+ }
+ }
+
+ public void deal(final Player player, final Answer answer) {
+ if (answer.isHit()) {
+ player.add(cardDeck.pop());
+ }
+ }
+
+ public void deal() {
+ while (canDeal()) {
+ super.add(cardDeck.pop());
+ }
+ }
+
+ public int countAddedHands() {
+ return handsSize() - INIT_HANDS_SIZE;
+ }
+
+ public Map getDealerResult(final Players players) {
+ Map dealerResult = new EnumMap<>(Result.class);
+
+ for (Result value : players.getPlayersResult(this).values()) {
+ Result reversed = value.reverse();
+ dealerResult.put(reversed, dealerResult.getOrDefault(reversed, 0) + 1);
+ }
+ return dealerResult;
+ }
+
+ @Override
+ public boolean canDeal() {
+ return handsSum() <= THRESHOLD;
+ }
+}
diff --git a/src/main/java/domain/participant/Hands.java b/src/main/java/domain/participant/Hands.java
new file mode 100644
index 0000000000..102d2966e8
--- /dev/null
+++ b/src/main/java/domain/participant/Hands.java
@@ -0,0 +1,88 @@
+package domain.participant;
+
+import domain.card.Card;
+import domain.Result;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+public class Hands {
+
+ public static final int BLACK_JACK = 21;
+ private static final int EXTRA_ACE_VALUE = 10;
+
+ private final List cards;
+
+ public Hands(final List cards) {
+ this.cards = new ArrayList<>(cards);
+ }
+
+ public static Hands createEmptyHands() {
+ return new Hands(new ArrayList<>());
+ }
+
+ public int sum() {
+ int total = cards.stream()
+ .mapToInt(Card::getCardNumber)
+ .sum();
+
+ return calculateTotalByAce(total);
+ }
+
+ public void add(final Card card) {
+ cards.add(card);
+ }
+
+ public boolean isBust() {
+ return sum() > BLACK_JACK;
+ }
+
+ public boolean isBlackJack() {
+ return sum() == BLACK_JACK;
+ }
+
+ private boolean hasAce() {
+ return cards.stream()
+ .anyMatch(Card::isAce);
+ }
+
+ public int size() {
+ return cards.size();
+ }
+
+ public List getCards() {
+ return cards.stream()
+ .map(Card::toString)
+ .toList();
+ }
+
+ public Result calculateResult(final Hands target) {
+ return Result.calculateOf(this, target);
+ }
+
+ private int calculateTotalByAce(final int total) {
+ if (hasAce() && total + EXTRA_ACE_VALUE <= BLACK_JACK) {
+ return total + EXTRA_ACE_VALUE;
+ }
+
+ return total;
+ }
+
+ @Override
+ public boolean equals(final Object target) {
+ if (this == target) {
+ return true;
+ }
+
+ if (!(target instanceof Hands hands)) {
+ return false;
+ }
+
+ return Objects.equals(cards, hands.cards);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(cards);
+ }
+}
diff --git a/src/main/java/domain/participant/Name.java b/src/main/java/domain/participant/Name.java
new file mode 100644
index 0000000000..ffb95b64db
--- /dev/null
+++ b/src/main/java/domain/participant/Name.java
@@ -0,0 +1,52 @@
+package domain.participant;
+
+import constants.ErrorCode;
+import exception.InvalidPlayerNameException;
+import java.util.Objects;
+
+public class Name {
+
+ private final String value;
+
+ public Name(final String value) {
+ validate(value);
+ this.value = value;
+ }
+
+ private void validate(final String name) {
+ validateNull(name);
+ validateBlank(name);
+ }
+
+ private void validateNull(final String name) {
+ if (name == null) {
+ throw new InvalidPlayerNameException(ErrorCode.BLANK_VALUE);
+ }
+ }
+
+ private void validateBlank(final String name) {
+ if (name.isBlank()) {
+ throw new InvalidPlayerNameException(ErrorCode.BLANK_VALUE);
+ }
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public boolean equals(final Object target) {
+ if (this == target) {
+ return true;
+ }
+ if (!(target instanceof Name name)) {
+ return false;
+ }
+ return Objects.equals(value, name.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(value);
+ }
+}
diff --git a/src/main/java/domain/participant/Participant.java b/src/main/java/domain/participant/Participant.java
new file mode 100644
index 0000000000..0bfa97b988
--- /dev/null
+++ b/src/main/java/domain/participant/Participant.java
@@ -0,0 +1,72 @@
+package domain.participant;
+
+import domain.Result;
+import domain.card.Card;
+import java.util.List;
+import java.util.Objects;
+
+public abstract class Participant {
+
+ private final Name name;
+ private final Hands hands;
+
+ protected Participant(final Name name, final Hands hands) {
+ this.name = name;
+ this.hands = hands;
+ }
+
+ public abstract boolean canDeal();
+
+ public void add(final Card card) {
+ hands.add(card);
+ }
+
+ public boolean isBust() {
+ return hands.isBust();
+ }
+
+ //TODO 시작시에 블랙잭일 때 쓸거에용
+ public boolean isBlackJack() {
+ return hands.isBlackJack();
+ }
+
+ public int handsSum() {
+ return hands.sum();
+ }
+
+ public int handsSize() {
+ return hands.size();
+ }
+
+ public Result calculateResult(final Participant participant) {
+ return hands.calculateResult(participant.getHands());
+ }
+
+ public List getCardNames() {
+ return hands.getCards();
+ }
+
+ public String getName() {
+ return name.getValue();
+ }
+
+ public Hands getHands() {
+ return hands;
+ }
+
+ @Override
+ public boolean equals(final Object target) {
+ if (this == target) {
+ return true;
+ }
+ if (!(target instanceof Participant participant)) {
+ return false;
+ }
+ return Objects.equals(name, participant.name) && Objects.equals(hands, participant.hands);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, hands);
+ }
+}
diff --git a/src/main/java/domain/participant/Player.java b/src/main/java/domain/participant/Player.java
new file mode 100644
index 0000000000..4674d6e99e
--- /dev/null
+++ b/src/main/java/domain/participant/Player.java
@@ -0,0 +1,25 @@
+package domain.participant;
+
+import static domain.participant.Dealer.DEALER_NAME;
+
+import constants.ErrorCode;
+import exception.ReservedPlayerNameException;
+
+public class Player extends Participant {
+
+ public Player(final Name name, final Hands hands) {
+ super(name, hands);
+ validate(name);
+ }
+
+ private void validate(final Name name) {
+ if (DEALER_NAME.equals(name)) {
+ throw new ReservedPlayerNameException(ErrorCode.RESERVED_NAME);
+ }
+ }
+
+ @Override
+ public boolean canDeal() {
+ return handsSum() <= Hands.BLACK_JACK;
+ }
+}
diff --git a/src/main/java/domain/participant/Players.java b/src/main/java/domain/participant/Players.java
new file mode 100644
index 0000000000..b007d830b9
--- /dev/null
+++ b/src/main/java/domain/participant/Players.java
@@ -0,0 +1,73 @@
+package domain.participant;
+
+import constants.ErrorCode;
+import domain.Result;
+import exception.DuplicatePlayerNameException;
+import exception.InvalidPlayersSizeException;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+
+public class Players {
+
+ private static final int MIN_SIZE = 2;
+ private static final int MAX_SIZE = 8;
+
+ private final List names;
+
+ public Players(final List names) {
+ this.names = names;
+ }
+
+ public static Players from(final List names) {
+ validate(names);
+ return new Players(mapToPlayers(names));
+ }
+
+ public void forEach(Consumer super Player> action) {
+ names.forEach(action);
+ }
+
+ public boolean isAllBust() {
+ return names.stream()
+ .allMatch(Player::isBust);
+ }
+
+ public Map getPlayersResult(final Dealer dealer) {
+ final Map result = new LinkedHashMap<>();
+ for (Player name : names) {
+ result.put(name, name.calculateResult(dealer));
+ }
+ return result;
+ }
+
+ private static List mapToPlayers(final List names) {
+ return names.stream()
+ .map(String::trim)
+ .map(name -> new Player(new Name(name), Hands.createEmptyHands()))
+ .toList();
+ }
+
+ private static void validate(final List names) {
+ validateSize(names);
+ validateDuplicate(names);
+ }
+
+ private static void validateSize(final List names) {
+ if (names.size() < MIN_SIZE || MAX_SIZE < names.size()) {
+ throw new InvalidPlayersSizeException(ErrorCode.INVALID_SIZE);
+ }
+ }
+
+ private static void validateDuplicate(final List names) {
+ if (names.size() != Set.copyOf(names).size()) {
+ throw new DuplicatePlayerNameException(ErrorCode.DUPLICATE_NAME);
+ }
+ }
+
+ public List getPlayers() {
+ return names;
+ }
+}
diff --git a/src/main/java/dto/DealerHandsDto.java b/src/main/java/dto/DealerHandsDto.java
new file mode 100644
index 0000000000..dae3867715
--- /dev/null
+++ b/src/main/java/dto/DealerHandsDto.java
@@ -0,0 +1,22 @@
+package dto;
+
+import domain.participant.Participant;
+import java.util.List;
+
+public class DealerHandsDto {
+
+ private final String displayedCard;
+
+ private DealerHandsDto(final String displayedCard) {
+ this.displayedCard = displayedCard;
+ }
+
+ public static DealerHandsDto from(final Participant dealer) {
+ List cards = dealer.getCardNames();
+ return new DealerHandsDto(cards.get(0));
+ }
+
+ public String getDisplayedCard() {
+ return displayedCard;
+ }
+}
diff --git a/src/main/java/dto/ParticipantDto.java b/src/main/java/dto/ParticipantDto.java
new file mode 100644
index 0000000000..795bae6b57
--- /dev/null
+++ b/src/main/java/dto/ParticipantDto.java
@@ -0,0 +1,11 @@
+package dto;
+
+import domain.participant.Participant;
+import java.util.List;
+
+public record ParticipantDto(String name, List cards, int totalSum) {
+
+ public static ParticipantDto from(final Participant player) {
+ return new ParticipantDto(player.getName(), player.getCardNames(), player.handsSum());
+ }
+}
diff --git a/src/main/java/dto/ParticipantsDto.java b/src/main/java/dto/ParticipantsDto.java
new file mode 100644
index 0000000000..c7a2984c4a
--- /dev/null
+++ b/src/main/java/dto/ParticipantsDto.java
@@ -0,0 +1,42 @@
+package dto;
+
+import domain.participant.Participant;
+import domain.participant.Players;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ParticipantsDto {
+
+ private final List players;
+
+ private ParticipantsDto(final List players) {
+ this.players = players;
+ }
+
+ public static ParticipantsDto of(final Participant dealer, final Players players) {
+ List result = new ArrayList<>();
+ result.add(ParticipantDto.from(dealer));
+ for (Participant player : players.getPlayers()) {
+ result.add(ParticipantDto.from(player));
+ }
+ return new ParticipantsDto(result);
+ }
+
+ public static ParticipantsDto of(final Players players) {
+ List result = new ArrayList<>();
+ for (Participant player : players.getPlayers()) {
+ result.add(ParticipantDto.from(player));
+ }
+ return new ParticipantsDto(result);
+ }
+
+ public List getNames() {
+ return players.stream()
+ .map(ParticipantDto::name)
+ .toList();
+ }
+
+ public List getPlayers() {
+ return players;
+ }
+}
diff --git a/src/main/java/exception/CustomException.java b/src/main/java/exception/CustomException.java
new file mode 100644
index 0000000000..fa189d08d1
--- /dev/null
+++ b/src/main/java/exception/CustomException.java
@@ -0,0 +1,16 @@
+package exception;
+
+import constants.ErrorCode;
+
+public class CustomException extends RuntimeException {
+
+ private final ErrorCode errorCode;
+
+ public CustomException(final ErrorCode errorCode) {
+ this.errorCode = errorCode;
+ }
+
+ public ErrorCode getErrorCode() {
+ return errorCode;
+ }
+}
diff --git a/src/main/java/exception/DuplicatePlayerNameException.java b/src/main/java/exception/DuplicatePlayerNameException.java
new file mode 100644
index 0000000000..8eb7bb2b5a
--- /dev/null
+++ b/src/main/java/exception/DuplicatePlayerNameException.java
@@ -0,0 +1,10 @@
+package exception;
+
+import constants.ErrorCode;
+
+public class DuplicatePlayerNameException extends CustomException {
+
+ public DuplicatePlayerNameException(final ErrorCode errorCode) {
+ super(errorCode);
+ }
+}
diff --git a/src/main/java/exception/InvalidCommandException.java b/src/main/java/exception/InvalidCommandException.java
new file mode 100644
index 0000000000..ea970cc741
--- /dev/null
+++ b/src/main/java/exception/InvalidCommandException.java
@@ -0,0 +1,10 @@
+package exception;
+
+import constants.ErrorCode;
+
+public class InvalidCommandException extends CustomException {
+
+ public InvalidCommandException(final ErrorCode errorCode) {
+ super(errorCode);
+ }
+}
diff --git a/src/main/java/exception/InvalidInputException.java b/src/main/java/exception/InvalidInputException.java
new file mode 100644
index 0000000000..1ade7a5b23
--- /dev/null
+++ b/src/main/java/exception/InvalidInputException.java
@@ -0,0 +1,10 @@
+package exception;
+
+import constants.ErrorCode;
+
+public class InvalidInputException extends CustomException {
+
+ public InvalidInputException(final ErrorCode errorCode) {
+ super(errorCode);
+ }
+}
diff --git a/src/main/java/exception/InvalidPlayerNameException.java b/src/main/java/exception/InvalidPlayerNameException.java
new file mode 100644
index 0000000000..2bf217ea37
--- /dev/null
+++ b/src/main/java/exception/InvalidPlayerNameException.java
@@ -0,0 +1,10 @@
+package exception;
+
+import constants.ErrorCode;
+
+public class InvalidPlayerNameException extends CustomException{
+
+ public InvalidPlayerNameException(final ErrorCode errorCode) {
+ super(errorCode);
+ }
+}
diff --git a/src/main/java/exception/InvalidPlayersSizeException.java b/src/main/java/exception/InvalidPlayersSizeException.java
new file mode 100644
index 0000000000..7242843584
--- /dev/null
+++ b/src/main/java/exception/InvalidPlayersSizeException.java
@@ -0,0 +1,10 @@
+package exception;
+
+import constants.ErrorCode;
+
+public class InvalidPlayersSizeException extends CustomException{
+
+ public InvalidPlayersSizeException(final ErrorCode errorCode) {
+ super(errorCode);
+ }
+}
diff --git a/src/main/java/exception/InvalidSeparatorException.java b/src/main/java/exception/InvalidSeparatorException.java
new file mode 100644
index 0000000000..76538c156d
--- /dev/null
+++ b/src/main/java/exception/InvalidSeparatorException.java
@@ -0,0 +1,10 @@
+package exception;
+
+import constants.ErrorCode;
+
+public class InvalidSeparatorException extends CustomException{
+
+ public InvalidSeparatorException(final ErrorCode errorCode) {
+ super(errorCode);
+ }
+}
diff --git a/src/main/java/exception/MessageDoesNotExistException.java b/src/main/java/exception/MessageDoesNotExistException.java
new file mode 100644
index 0000000000..74251064ca
--- /dev/null
+++ b/src/main/java/exception/MessageDoesNotExistException.java
@@ -0,0 +1,10 @@
+package exception;
+
+import constants.ErrorCode;
+
+public class MessageDoesNotExistException extends CustomException{
+
+ public MessageDoesNotExistException(final ErrorCode errorCode) {
+ super(errorCode);
+ }
+}
diff --git a/src/main/java/exception/NoMoreCardException.java b/src/main/java/exception/NoMoreCardException.java
new file mode 100644
index 0000000000..25551e0a6b
--- /dev/null
+++ b/src/main/java/exception/NoMoreCardException.java
@@ -0,0 +1,10 @@
+package exception;
+
+import constants.ErrorCode;
+
+public class NoMoreCardException extends CustomException{
+
+ public NoMoreCardException(final ErrorCode errorCode) {
+ super(errorCode);
+ }
+}
diff --git a/src/main/java/exception/ReservedPlayerNameException.java b/src/main/java/exception/ReservedPlayerNameException.java
new file mode 100644
index 0000000000..836ba97541
--- /dev/null
+++ b/src/main/java/exception/ReservedPlayerNameException.java
@@ -0,0 +1,10 @@
+package exception;
+
+import constants.ErrorCode;
+
+public class ReservedPlayerNameException extends CustomException {
+
+ public ReservedPlayerNameException(final ErrorCode errorCode) {
+ super(errorCode);
+ }
+}
diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java
new file mode 100644
index 0000000000..c432099732
--- /dev/null
+++ b/src/main/java/view/InputView.java
@@ -0,0 +1,58 @@
+package view;
+
+import constants.ErrorCode;
+import exception.InvalidInputException;
+import exception.InvalidSeparatorException;
+import java.util.List;
+import java.util.Scanner;
+
+public class InputView {
+
+ private static final String NAME_SEPARATOR = ",";
+
+ private final Scanner scanner;
+
+ public InputView(final Scanner scanner) {
+ this.scanner = scanner;
+ }
+
+ public List readNames() {
+ System.out.println("게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)");
+ String rawNames = scanner.nextLine().trim();
+ validateBlank(rawNames);
+ validateSeparators(rawNames);
+ List names = List.of(rawNames.split(NAME_SEPARATOR));
+ System.out.println();
+ return names;
+ }
+
+ public String readAnswer(String name) {
+ System.out.printf("%s는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)", name);
+ System.out.println();
+ String rawAnswer = scanner.nextLine().trim();
+ validateBlank(rawAnswer);
+ return rawAnswer;
+ }
+
+ private void validateBlank(final String rawNames) {
+ if (rawNames == null || rawNames.isBlank()) {
+ throw new InvalidInputException(ErrorCode.INVALID_INPUT);
+ }
+ }
+
+ private void validateSeparators(final String rawNames) {
+ if (isInvalidSeparator(rawNames)) {
+ throw new InvalidSeparatorException(ErrorCode.INVALID_SEPARATOR);
+ }
+ }
+
+ private boolean isInvalidSeparator(final String rawNames) {
+ if (rawNames.startsWith(NAME_SEPARATOR)) {
+ return true;
+ }
+ if (rawNames.endsWith(NAME_SEPARATOR)) {
+ return true;
+ }
+ return rawNames.contains(NAME_SEPARATOR.repeat(2));
+ }
+}
diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java
new file mode 100644
index 0000000000..87c607de6b
--- /dev/null
+++ b/src/main/java/view/OutputView.java
@@ -0,0 +1,94 @@
+package view;
+
+import static domain.participant.Dealer.INIT_HANDS_SIZE;
+import static domain.participant.Dealer.THRESHOLD;
+
+import constants.ErrorCode;
+import domain.Result;
+import domain.participant.Player;
+import dto.DealerHandsDto;
+import dto.ParticipantDto;
+import dto.ParticipantsDto;
+import java.util.List;
+import java.util.Map;
+import view.message.ErrorCodeMessage;
+
+public class OutputView {
+
+ private static final String LINE = System.lineSeparator();
+ private static final String FORM = "%s카드: %s%n";
+ private static final String TOTAL_SUM_FORM = "%s 카드: %s - 결과: %d%n";
+ private static final String RESULT_FORM = "%s: %s%n";
+ private static final String ERROR_FORM = "[ERROR] %s%n";
+
+
+ public void printStartDeal(final DealerHandsDto dealerHandsDto, final ParticipantsDto participantsDto) {
+ final String dealerCard = dealerHandsDto.getDisplayedCard();
+
+ final List playerNames = participantsDto.getNames();
+ System.out.printf("딜러와 %s 에게 %d장을 나누었습니다.%n", format(playerNames), INIT_HANDS_SIZE);
+ System.out.printf("딜러: %s%n", dealerCard);
+
+ for (ParticipantDto participantDto : participantsDto.getPlayers()) {
+ System.out.printf(FORM, participantDto.name(), format(participantDto.cards()));
+ }
+ System.out.print(LINE);
+ }
+
+ public void printHands(final ParticipantDto participantDto) {
+ System.out.printf(FORM, participantDto.name(), format(participantDto.cards()));
+ }
+
+ public void printDealerTurnMessage() {
+ System.out.printf("딜러는 %d이하라 한장의 카드를 더 받았습니다.%n%n", THRESHOLD);
+ }
+
+ public void printHandsResult(final ParticipantsDto participantsDto) {
+ for (ParticipantDto participantDto : participantsDto.getPlayers()) {
+ System.out.printf(TOTAL_SUM_FORM, participantDto.name(), format(participantDto.cards()),
+ participantDto.totalSum());
+ }
+ System.out.print(LINE);
+ }
+
+ public void printGameResult(final Map dealerResult, final Map playerResult) {
+ System.out.println("## 최종결과");
+ System.out.printf(RESULT_FORM, "딜러", format(dealerResult));
+ for (Map.Entry entry : playerResult.entrySet()) {
+ System.out.printf(RESULT_FORM, entry.getKey().getName(), entry.getValue().getValue());
+ }
+ }
+
+ private String format(final Map dealerResult) {
+ StringBuilder stringBuilder = new StringBuilder();
+ for (Map.Entry entry : dealerResult.entrySet()) {
+ stringBuilder.append(entry.getValue()).append(entry.getKey().getValue()).append(" ");
+ }
+
+ return stringBuilder.toString();
+ }
+
+ private String format(final List playerNames) {
+ return String.join(", ", playerNames);
+ }
+
+ public void printBust() {
+ System.out.printf("BUST%n");
+ }
+
+ public void printBlackJack() {
+ System.out.printf("BLACK JACK!!!%n");
+ }
+
+ public void printException(final ErrorCode errorCode) {
+ System.out.printf(ERROR_FORM, ErrorCodeMessage.from(errorCode).getMessage());
+ }
+
+ public void printDealEndMessage(final boolean isBust) {
+ if (isBust) {
+ printBust();
+ return;
+ }
+ printBlackJack();
+ }
+}
diff --git a/src/main/java/view/message/ErrorCodeMessage.java b/src/main/java/view/message/ErrorCodeMessage.java
new file mode 100644
index 0000000000..507dc908f7
--- /dev/null
+++ b/src/main/java/view/message/ErrorCodeMessage.java
@@ -0,0 +1,49 @@
+package view.message;
+
+import constants.ErrorCode;
+import exception.MessageDoesNotExistException;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+public enum ErrorCodeMessage {
+
+ NOT_EXIST_MESSAGE(ErrorCode.NOT_EXIST_MESSAGE, "해당 메시지가 없습니다."),
+ INVALID_SEPARATOR(ErrorCode.INVALID_SEPARATOR, "유효하지 않은 구분자입니다."),
+ INVALID_INPUT(ErrorCode.INVALID_INPUT, "유효하지 않은 입력입니다."),
+ INVALID_SIZE(ErrorCode.INVALID_SIZE, "유효하지 않은 참여자 수입니다."),
+ DUPLICATE_NAME(ErrorCode.DUPLICATE_NAME, "중복된 이름은 사용할 수 없습니다."),
+ RESERVED_NAME(ErrorCode.RESERVED_NAME, "이름은 딜러일 수 없습니다."),
+ BLANK_VALUE(ErrorCode.BLANK_VALUE, "이름은 공백일 수 없습니다."),
+ EMPTY_CARD(ErrorCode.EMPTY_CARD, "뽑을 수 있는 카드가 없습니다."),
+ INVALID_COMMAND(ErrorCode.INVALID_COMMAND, "y 또는 n을 입력해주세요"),
+ ;
+
+ private static final Map SUIT_MESSAGE = Arrays.stream(values())
+ .collect(Collectors.toMap(ErrorCodeMessage::getCode, Function.identity()));
+
+
+ private final ErrorCode errorCode;
+ private final String message;
+
+ ErrorCodeMessage(final ErrorCode errorCode, final String message) {
+ this.errorCode = errorCode;
+ this.message = message;
+ }
+
+ public static ErrorCodeMessage from(ErrorCode errorCode) {
+ if (SUIT_MESSAGE.containsKey(errorCode)) {
+ return SUIT_MESSAGE.get(errorCode);
+ }
+ throw new MessageDoesNotExistException(ErrorCode.NOT_EXIST_MESSAGE);
+ }
+
+ private ErrorCode getCode() {
+ return errorCode;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+}
diff --git a/src/test/java/domain/AnswerTest.java b/src/test/java/domain/AnswerTest.java
new file mode 100644
index 0000000000..ea61387a17
--- /dev/null
+++ b/src/test/java/domain/AnswerTest.java
@@ -0,0 +1,18 @@
+package domain;
+
+import exception.InvalidCommandException;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class AnswerTest {
+
+ @ParameterizedTest
+ @DisplayName("y혹은 n이 아닐시 예외가 발생한다.")
+ @ValueSource(strings = {"Y", "nn", "aa"})
+ void invalidAnswer(String value) {
+ Assertions.assertThatThrownBy(() -> Answer.from(value))
+ .isInstanceOf(InvalidCommandException.class);
+ }
+}
diff --git a/src/test/java/domain/CardTest.java b/src/test/java/domain/CardTest.java
new file mode 100644
index 0000000000..39c5667e1d
--- /dev/null
+++ b/src/test/java/domain/CardTest.java
@@ -0,0 +1,20 @@
+package domain;
+
+import domain.card.Card;
+import java.util.List;
+import java.util.Set;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class CardTest {
+
+ @Test
+ @DisplayName("중복없는 52장의 카드를 생성한다.")
+ void generate() {
+ List values = Card.values();
+
+ Assertions.assertThat(values).hasSize(52);
+ Assertions.assertThat(Set.copyOf(values)).hasSize(52);
+ }
+}
diff --git a/src/test/java/domain/DealerTest.java b/src/test/java/domain/DealerTest.java
new file mode 100644
index 0000000000..fcec8af224
--- /dev/null
+++ b/src/test/java/domain/DealerTest.java
@@ -0,0 +1,105 @@
+package domain;
+
+import static domain.HandsTestFixture.sum10Size2;
+import static domain.HandsTestFixture.sum18Size2;
+import static domain.HandsTestFixture.sum20Size3;
+import static domain.HandsTestFixture.sum21Size2;
+import static domain.Result.LOSE;
+import static domain.Result.TIE;
+import static domain.Result.WIN;
+
+import domain.participant.Dealer;
+import domain.participant.Hands;
+import domain.participant.Name;
+import domain.participant.Player;
+import domain.participant.Players;
+import java.util.List;
+import java.util.Map;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class DealerTest {
+
+ @Test
+ @DisplayName("참여자에게 카드 2장을 나눠준다.")
+ void dealCards() {
+ //given
+ final Players players = Players.from(List.of("레디", "제제"));
+ final Dealer dealer = new Dealer();
+
+ //when
+ dealer.initHands(players);
+
+ //then
+ Assertions.assertThat(players.getPlayers()).allMatch(player -> player.handsSize() == 2);
+ }
+
+ @Test
+ @DisplayName("참여자의 답변이 y라면 카드를 한장 추가한다.")
+ void addOneCard() {
+ //given
+ final Player hitPlayer = new Player(new Name("레디"), Hands.createEmptyHands());
+ final Player stayPlayer = new Player(new Name("제제"), Hands.createEmptyHands());
+
+ final Players players = new Players(List.of(hitPlayer, stayPlayer));
+
+ final Dealer dealer = new Dealer();
+ dealer.initHands(players);
+
+ //when
+ dealer.deal(hitPlayer, Answer.HIT);
+ dealer.deal(stayPlayer, Answer.STAY);
+
+ //then
+ Assertions.assertThat(hitPlayer.handsSize()).isEqualTo(3);
+ Assertions.assertThat(stayPlayer.handsSize()).isEqualTo(2);
+ }
+
+ @Test
+ @DisplayName("딜러의 카드의 합이 17이상이 될때까지 카드를 추가한다.")
+ void dealerDeal() {
+ //given
+ final Dealer dealer = new Dealer(sum10Size2);
+
+ //when
+ dealer.deal();
+
+ //then
+ Assertions.assertThat(dealer.countAddedHands()).isPositive();
+ Assertions.assertThat(dealer.handsSum()).isGreaterThanOrEqualTo(17);
+ }
+
+ @DisplayName("딜러의 카드의 합이 17이상이라면 카드를 추가하지 않는다")
+ @Test
+ void dealerNoDeal() {
+ //given
+ final Dealer dealer = new Dealer(sum18Size2);
+
+ //when
+ dealer.deal();
+
+ //then
+ Assertions.assertThat(dealer.countAddedHands()).isZero();
+ Assertions.assertThat(dealer.handsSum()).isGreaterThanOrEqualTo(17);
+ }
+
+ @DisplayName("딜러의 승패무를 판단한다.")
+ @Test
+ void dealerResult() {
+ // given
+ Player loser1 = new Player(new Name("레디"), sum18Size2);
+ Player loser2 = new Player(new Name("피케이"), sum18Size2);
+ Player winner = new Player(new Name("제제"), sum21Size2);
+ Player tier = new Player(new Name("브라운"), sum20Size3);
+
+ Players players = new Players(List.of(loser1, loser2, winner, tier));
+ Dealer dealer = new Dealer(sum20Size3);
+
+ // when
+ Map expected = Map.of(WIN, 2, LOSE, 1, TIE, 1);
+
+ // then
+ Assertions.assertThat(dealer.getDealerResult(players)).isEqualTo(expected);
+ }
+}
diff --git a/src/test/java/domain/HandsTest.java b/src/test/java/domain/HandsTest.java
new file mode 100644
index 0000000000..8c5bb396d8
--- /dev/null
+++ b/src/test/java/domain/HandsTest.java
@@ -0,0 +1,84 @@
+package domain;
+
+import static domain.HandsTestFixture.sum19Size3Ace1;
+import static domain.HandsTestFixture.sum19Size4Ace11;
+import static domain.HandsTestFixture.sum20Size2;
+import static domain.HandsTestFixture.sum20Size3Ace1;
+import static domain.HandsTestFixture.sum21Size3Ace11;
+import static domain.card.Rank.EIGHT;
+import static domain.card.Shape.CLOVER;
+
+import domain.card.Card;
+import domain.participant.Hands;
+import java.util.stream.Stream;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class HandsTest {
+
+ @Test
+ @DisplayName("카드를 가지고 있는 객체를 생성한다.")
+ void createPacket() {
+ Assertions.assertThatCode(Hands::createEmptyHands)
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ @DisplayName("카드를 추가한다.")
+ void addCard() {
+ //given
+ final Hands hands = Hands.createEmptyHands();
+
+ //when
+ hands.add(new Card(EIGHT, CLOVER));
+
+ //then
+ Assertions.assertThat(hands.size()).isEqualTo(1);
+ }
+
+ @DisplayName("카드의 합을 구한다.")
+ @Test
+ void sum() {
+ Assertions.assertThat(sum20Size2.sum()).isEqualTo(20);
+ }
+
+ @DisplayName("에이스를 11로 계산한다.")
+ @ParameterizedTest
+ @MethodSource("sumAce11ParameterProvider")
+ void sumAce11(final Hands hands, final int expected) {
+ // given & when
+ final int result = hands.sum();
+
+ // then
+ Assertions.assertThat(result).isEqualTo(expected);
+ }
+
+ @DisplayName("에이스를 1로 계산한다.")
+ @ParameterizedTest
+ @MethodSource("sumAce1ParameterProvider")
+ void sumAce1(final Hands hands, final int expected) {
+ // given & when
+ final int result = hands.sum();
+
+ // then
+ Assertions.assertThat(result).isEqualTo(expected);
+ }
+
+ static Stream sumAce11ParameterProvider() {
+ return Stream.of(
+ Arguments.of(sum21Size3Ace11, 21),
+ Arguments.of(sum19Size4Ace11, 19)
+ );
+ }
+
+ static Stream sumAce1ParameterProvider() {
+ return Stream.of(
+ Arguments.of(sum19Size3Ace1, 19),
+ Arguments.of(sum20Size3Ace1, 20)
+ );
+ }
+}
diff --git a/src/test/java/domain/HandsTestFixture.java b/src/test/java/domain/HandsTestFixture.java
new file mode 100644
index 0000000000..bb1adb416d
--- /dev/null
+++ b/src/test/java/domain/HandsTestFixture.java
@@ -0,0 +1,49 @@
+package domain;
+
+import static domain.card.Rank.ACE;
+import static domain.card.Rank.EIGHT;
+import static domain.card.Rank.FIVE;
+import static domain.card.Rank.FOUR;
+import static domain.card.Rank.JACK;
+import static domain.card.Rank.KING;
+import static domain.card.Rank.NINE;
+import static domain.card.Rank.QUEEN;
+import static domain.card.Rank.SEVEN;
+import static domain.card.Rank.SIX;
+import static domain.card.Rank.TEN;
+import static domain.card.Rank.THREE;
+import static domain.card.Rank.TWO;
+import static domain.card.Shape.CLOVER;
+import static domain.card.Shape.DIAMOND;
+import static domain.card.Shape.HEART;
+import static domain.card.Shape.SPADE;
+
+import domain.card.Card;
+import domain.participant.Hands;
+import java.util.List;
+
+class HandsTestFixture {
+
+ static final Hands sum10Size2 = new Hands(List.of(new Card(FIVE, SPADE), new Card(FIVE, HEART)));
+ static final Hands sum17Size3One = new Hands(
+ List.of(new Card(SEVEN, SPADE), new Card(FOUR, SPADE), new Card(SIX, SPADE)));
+ static final Hands sum17Size3Two = new Hands(
+ List.of(new Card(TEN, SPADE), new Card(THREE, SPADE), new Card(FOUR, SPADE)));
+ static final Hands sum18Size2 = new Hands(List.of(new Card(EIGHT, CLOVER), new Card(TEN, DIAMOND)));
+ static final Hands sum19Size4Ace11 = new Hands(
+ List.of(new Card(ACE, DIAMOND), new Card(TWO, CLOVER), new Card(FOUR, CLOVER), new Card(TWO, CLOVER)));
+ static final Hands sum19Size3Ace1 = new Hands(
+ List.of(new Card(ACE, HEART), new Card(NINE, SPADE), new Card(NINE, CLOVER)));
+ static final Hands sum20Size2 = new Hands(List.of(new Card(NINE, SPADE), new Card(ACE, SPADE)));
+ static final Hands sum20Size3 = new Hands(
+ List.of(new Card(SEVEN, SPADE), new Card(TWO, SPADE), new Card(ACE, SPADE)));
+ static final Hands sum20Size3Ace1 = new Hands(
+ List.of(new Card(ACE, DIAMOND), new Card(EIGHT, CLOVER), new Card(FIVE, CLOVER), new Card(SIX, CLOVER)));
+ static final Hands sum21Size2 = new Hands(List.of(new Card(QUEEN, HEART), new Card(ACE, SPADE)));
+ static final Hands sum21Size3Ace11 = new Hands(
+ List.of(new Card(ACE, HEART), new Card(EIGHT, SPADE), new Card(TWO, CLOVER)));
+ static final Hands bustHands = new Hands(
+ List.of(new Card(EIGHT, DIAMOND), new Card(TWO, DIAMOND), new Card(TWO, DIAMOND), new Card(KING, CLOVER)));
+ static final Hands noBustHands = new Hands(List.of(new Card(JACK, HEART), new Card(TEN, SPADE)));
+ static final Hands blackJack = new Hands(List.of(new Card(JACK, HEART), new Card(ACE, SPADE)));
+}
diff --git a/src/test/java/domain/NameTest.java b/src/test/java/domain/NameTest.java
new file mode 100644
index 0000000000..401a38f783
--- /dev/null
+++ b/src/test/java/domain/NameTest.java
@@ -0,0 +1,28 @@
+package domain;
+
+import domain.participant.Name;
+import exception.InvalidPlayerNameException;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.NullSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class NameTest {
+
+ @DisplayName("공백을 입력하면 예외를 발생시킨다.")
+ @ParameterizedTest
+ @ValueSource(strings = {"", " ", " "})
+ void BlankInputThrowException(String value) {
+ Assertions.assertThatThrownBy(() -> new Name(value))
+ .isInstanceOf(InvalidPlayerNameException.class);
+ }
+
+ @DisplayName("null을 입력하면 예외를 발생시킨다.")
+ @ParameterizedTest
+ @NullSource
+ void nullInputThrowException(String value) {
+ Assertions.assertThatThrownBy(() -> new Name(value))
+ .isInstanceOf(InvalidPlayerNameException.class);
+ }
+}
diff --git a/src/test/java/domain/PlayerTest.java b/src/test/java/domain/PlayerTest.java
new file mode 100644
index 0000000000..850c1599ec
--- /dev/null
+++ b/src/test/java/domain/PlayerTest.java
@@ -0,0 +1,28 @@
+package domain;
+
+import domain.participant.Hands;
+import domain.participant.Name;
+import domain.participant.Player;
+import exception.ReservedPlayerNameException;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class PlayerTest {
+ @DisplayName("이름으로 참여자를 생성한다.")
+ @Test
+ void createPlayerWithName() {
+ Assertions.assertThatCode(() -> new Player(new Name("pobi"), Hands.createEmptyHands()))
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ @DisplayName("참여자 이름이 딜러이면 예외가 발생한다.")
+ void validateName() {
+ final Name name = new Name("딜러");
+ final Hands hands = Hands.createEmptyHands();
+
+ Assertions.assertThatThrownBy(() -> new Player(name, hands))
+ .isInstanceOf(ReservedPlayerNameException.class);
+ }
+}
diff --git a/src/test/java/domain/PlayersTest.java b/src/test/java/domain/PlayersTest.java
new file mode 100644
index 0000000000..f038cb7a75
--- /dev/null
+++ b/src/test/java/domain/PlayersTest.java
@@ -0,0 +1,142 @@
+package domain;
+
+import static domain.HandsTestFixture.bustHands;
+import static domain.HandsTestFixture.noBustHands;
+import static domain.HandsTestFixture.sum18Size2;
+import static domain.HandsTestFixture.sum20Size2;
+import static domain.HandsTestFixture.sum20Size3;
+import static domain.HandsTestFixture.sum21Size2;
+import static domain.Result.LOSE;
+import static domain.Result.TIE;
+import static domain.Result.WIN;
+
+import domain.participant.Dealer;
+import domain.participant.Name;
+import domain.participant.Player;
+import domain.participant.Players;
+import exception.DuplicatePlayerNameException;
+import exception.InvalidPlayersSizeException;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class PlayersTest {
+
+ @Test
+ @DisplayName("참여자 이름 중복시 예외가 발생한다.")
+ void duplicatePlayerName() {
+ //given
+ final List names = List.of("redy", "redy");
+
+ //when & then
+ Assertions.assertThatThrownBy(() -> Players.from(names))
+ .isInstanceOf(DuplicatePlayerNameException.class);
+ }
+
+ @DisplayName("총 참여자 수가 2이상 8이하이면 참여자를 생성한다.")
+ @ParameterizedTest
+ @MethodSource("validPlayersSizeParameterProvider")
+ void validPlayersSize(final List names) {
+ Assertions.assertThatCode(() -> Players.from(names))
+ .doesNotThrowAnyException();
+ }
+
+ @DisplayName("총 참여자 수는 2이상 8이하가 아니면 예외가 발생한다.")
+ @ParameterizedTest
+ @MethodSource("invalidPlayersSizeParameterProvider")
+ void invalidPlayersSize(final List names) {
+ Assertions.assertThatThrownBy(() -> Players.from(names))
+ .isInstanceOf(InvalidPlayersSizeException.class);
+ }
+
+ @Test
+ @DisplayName("참가자 중 버스트 되지 않은 참가자가 있다면 isAllBust가 False를 반환한다.")
+ void isAllBustFalse() {
+ //given
+ final Player bustPlayer = new Player(new Name("레디"), bustHands);
+ final Player noBustPlayer = new Player(new Name("제제"), noBustHands);
+ final Players players = new Players(List.of(bustPlayer, noBustPlayer));
+
+ //when && then
+ Assertions.assertThat(players.isAllBust()).isFalse();
+ }
+
+ @Test
+ @DisplayName("모든 참가자가 버스트되면 isAllBust가 True를 반환한다.")
+ void isAllBustTrue() {
+ //given
+ Player player1 = new Player(new Name("레디"), bustHands);
+ Player player2 = new Player(new Name("제제"), bustHands);
+ Player player3 = new Player(new Name("수달"), bustHands);
+ Player player4 = new Player(new Name("피케이"), bustHands);
+
+ Players players = new Players(List.of(player1, player2, player3, player4));
+
+ //when && then
+ Assertions.assertThat(players.isAllBust()).isTrue();
+ }
+
+ @Test
+ @DisplayName("참여자의 승패무를 판단한다.")
+ void playerResult() {
+ //given
+ Player loser = new Player(new Name("레디"), sum18Size2);
+ Player winner = new Player(new Name("제제"), sum21Size2);
+ Player tier = new Player(new Name("수달"), sum20Size3);
+
+ Players players = new Players(List.of(loser, winner, tier));
+ Dealer dealer = new Dealer(sum20Size3);
+
+ //when & then
+ Map expected = Map.of(loser, LOSE, winner, WIN, tier, TIE);
+ Assertions.assertThat(players.getPlayersResult(dealer)).isEqualTo(expected);
+ }
+
+ @Test
+ @DisplayName("딜러가 버스트일때 참여자가 버스트가 아니면 WIN")
+ void all() {
+ //given
+ Dealer bustDealer = new Dealer(bustHands);
+ Player winner1 = new Player(new Name("레디"), sum18Size2);
+ Player winner2 = new Player(new Name("브라운"), sum20Size2);
+ Player loser = new Player(new Name("제제"), bustHands);
+
+ Players players = new Players(List.of(winner1, winner2, loser));
+
+ //when
+ Map expectedPlayerResult = Map.of(winner1, WIN, winner2, WIN, loser, LOSE);
+ Map expectedDealerResult = Map.of(WIN, 1, LOSE, 2);
+
+ //then
+ Assertions.assertThat(players.getPlayersResult(bustDealer)).isEqualTo(expectedPlayerResult);
+ Assertions.assertThat(bustDealer.getDealerResult(players)).isEqualTo(expectedDealerResult);
+ }
+
+ static Stream validPlayersSizeParameterProvider() {
+ return Stream.of(
+ Arguments.of(
+ List.of("pobi", "jason")
+ ),
+ Arguments.of(
+ List.of("1", "2", "3", "4", "5", "6", "7", "8")
+ )
+ );
+ }
+
+ static Stream invalidPlayersSizeParameterProvider() {
+ return Stream.of(
+ Arguments.of(
+ List.of("pobi")
+ ),
+ Arguments.of(
+ List.of("1", "2", "3", "4", "5", "6", "7", "8", "9")
+ )
+ );
+ }
+}
diff --git a/src/test/java/domain/ResultTest.java b/src/test/java/domain/ResultTest.java
new file mode 100644
index 0000000000..01ad8e26a1
--- /dev/null
+++ b/src/test/java/domain/ResultTest.java
@@ -0,0 +1,48 @@
+package domain;
+
+import static domain.HandsTestFixture.blackJack;
+import static domain.HandsTestFixture.bustHands;
+import static domain.HandsTestFixture.sum17Size3One;
+import static domain.HandsTestFixture.sum17Size3Two;
+import static domain.HandsTestFixture.sum20Size2;
+import static domain.HandsTestFixture.sum20Size3;
+import static domain.HandsTestFixture.sum21Size2;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class ResultTest {
+
+ @DisplayName("카드 합이 같고 카드 갯수가 같으면 무승부이다.")
+ @Test
+ void isTie() {
+ Assertions.assertThat(sum17Size3One.calculateResult(sum17Size3Two)).isEqualTo(Result.TIE);
+ }
+
+ @DisplayName("카드 합이 같은데 카드 갯수가 더 적으면 승리이다.")
+ @Test
+ void isWinBySize() {
+ Assertions.assertThat(sum20Size2.calculateResult(sum20Size3)).isEqualTo(Result.WIN);
+ }
+
+ @Test
+ @DisplayName("카드 합이 21이하이면서 21에 가까운 카드가 승리한다.")
+ void isWin() {
+ Assertions.assertThat(sum21Size2.calculateResult(sum20Size2)).isEqualTo(Result.WIN);
+ Assertions.assertThat(sum20Size2.calculateResult(sum21Size2)).isEqualTo(Result.LOSE);
+ }
+
+ @Test
+ @DisplayName("카드 합이 21초과이면 패배한다.")
+ void isLoseWhenCardSumGreater21() {
+ Assertions.assertThat(bustHands.calculateResult(sum20Size2)).isEqualTo(Result.LOSE);
+ }
+
+ @Test
+ @DisplayName("blackjack이 이긴다.")
+ void isWinBlackJack() {
+ Assertions.assertThat(blackJack.calculateResult(sum20Size2)).isEqualTo(Result.WIN);
+ Assertions.assertThat(sum20Size2.calculateResult(blackJack)).isEqualTo(Result.LOSE);
+ }
+}
diff --git a/src/test/java/domain/card/CardDeckTest.java b/src/test/java/domain/card/CardDeckTest.java
new file mode 100644
index 0000000000..a7bf0c60bd
--- /dev/null
+++ b/src/test/java/domain/card/CardDeckTest.java
@@ -0,0 +1,39 @@
+package domain.card;
+
+import static domain.participant.Dealer.DECK_SIZE;
+
+import exception.NoMoreCardException;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+class CardDeckTest {
+
+ @DisplayName("카드를 52 * 6 만큼 생성한다.")
+ @Test
+ void generate() {
+ // given
+ CardDeck cardDeck = CardDeck.generate(DECK_SIZE);
+
+ // when && then
+ Assertions.assertThat(cardDeck.size()).isEqualTo(52 * 6);
+ }
+
+ @Test
+ @DisplayName("카드가 없는데 카드를 뽑을 경우 예외가 발생한다.")
+ void pop() {
+ //given
+ CardDeck cardDeck = CardDeck.generate(1);
+
+ //when
+ int cardSize = 52;
+ while (cardSize > 0) {
+ cardDeck.pop();
+ cardSize--;
+ }
+
+ //then
+ Assertions.assertThatThrownBy(cardDeck::pop)
+ .isInstanceOf(NoMoreCardException.class);
+ }
+}