diff --git a/README.md b/README.md
index 8d7e8aee..06ca8b53 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,62 @@
-# java-baseball-precourse
\ No newline at end of file
+# java-baseball-precourse
+
+## ⚾ 숫자 야구 게임 (Number Baseball)
+JUnit5와 MVC 패턴을 활용하여 구현한 숫자 야구 게임 프로젝트입니다.
+도메인 로직의 완성도를 높이기 위해 단위 테스트를 포함합니다.
+
+## 🏗️ 아키텍처 설계 (MVC Pattern)
+본 프로젝트는 관심사 분리를 위해 MVC(Model-View-Controller) 패턴을 기반으로 설계되었습니다.
+
+Model: 게임의 핵심 데이터와 비즈니스 로직을 담당합니다. (컴퓨터 숫자 생성, 스트라이크/볼 판정 등)
+
+View: 사용자 입력을 받고(System.in), 결과를 출력하는(System.out) UI 영역을 담당합니다.
+
+Controller: Model과 View 사이에서 흐름을 제어합니다.
+
+## 📌 주요 기능 목록
+### 1. 게임 시스템 (Model)
+상대 플레이어 숫자 생성: 1부터 9까지의 서로 다른 임의의 수 3개를 생성합니다.
+힌트 계산 로직: 사용자가 입력한 숫자와 컴퓨터의 숫자를 비교하여 판정 결과를 도출합니다.
+- 스트라이크: 숫자와 위치가 모두 일치하는 경우
+- 볼: 숫자는 일치하지만 위치가 다른 경우
+- 낫싱: 일치하는 숫자가 하나도 없는 경우
+- 게임 상태 관리: 3스트라이크 달성 시 라운드 종료 및 재시작/종료 상태를 관리합니다.
+
+### 2. 입출력 UI (View)
+입력 기능: 숫자 입력 및 게임 재시작 여부(1 또는 2)를 입력받습니다.
+
+출력 기능:
+- 게임 시작 문구 출력
+- 판정 결과(힌트) 출력
+- 게임 종료 및 에러 메시지 출력
+
+### 3. 게임 흐름 제어 (Controller)
+전반적인 게임 진행 루프를 관리합니다.
+
+사용자의 입력값에 따라 Model을 업데이트하고 View를 통해 결과를 전달합니다.
+
+### 4. 예외 처리
+사용자가 잘못된 값을 입력할 경우 [ERROR] 문구를 출력하고 게임을 지속합니다.
+
+중복된 숫자, 3자리가 아닌 입력, 1~9 범위를 벗어난 값 등
+
+## 🧪 테스트 계획 (Unit Test)
+JUnit5와 AssertJ를 사용하여 UI 로직을 제외한 도메인(Model) 로직에 대한 단위 테스트를 수행합니다.
+
+컴퓨터 숫자 생성 테스트: 매번 1~9 사이의, 서로 다른 임의의 숫자가 세개가 생성되는지 검증
+
+판정 로직 테스트: 입력값에 따라 정해진 힌트(스트라이크, 볼, 낫싱)가 정확히 반환되는지 검증
+
+예외 상황 테스트: 잘못된 입력값 입력 시 의도한 대로 에러 처리가 되는지 검증
+
+## 🕹️ 실행 방법 및 예시
+```
+숫자를 입력해주세요 : 123
+1스트라이크
+숫자를 입력해주세요 : 456
+1스트라이크 1볼
+숫자를 입력해주세요 : 425
+3스트라이크
+3개의 숫자를 모두 맞히셨습니다! 게임 종료
+게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.
+```
\ No newline at end of file
diff --git a/src/main/java/baseball/Application.java b/src/main/java/baseball/Application.java
new file mode 100644
index 00000000..849df127
--- /dev/null
+++ b/src/main/java/baseball/Application.java
@@ -0,0 +1,10 @@
+package baseball;
+
+import baseball.controller.BaseballController;
+
+public class Application {
+ public static void main(String[] args) {
+ BaseballController baseballController = new BaseballController();
+ baseballController.run();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/baseball/controller/BaseballController.java b/src/main/java/baseball/controller/BaseballController.java
new file mode 100644
index 00000000..932a4591
--- /dev/null
+++ b/src/main/java/baseball/controller/BaseballController.java
@@ -0,0 +1,83 @@
+package baseball.controller;
+
+import baseball.model.*;
+import baseball.view.InputView;
+import baseball.view.OutputView;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class BaseballController {
+ private final InputView inputView;
+ private final OutputView outputView;
+ private final NumberGenerator numberGenerator;
+
+ public BaseballController() {
+ this.inputView = new InputView();
+ this.outputView = new OutputView();
+ this.numberGenerator = new NumberGenerator();
+ }
+
+ public void run() {
+ outputView.printStartMessage();
+ do {
+ playGame();
+ } while (shouldRestart());
+ }
+
+ private void playGame() {
+ BaseballNumbers computerNumbers = new BaseballNumbers(numberGenerator.createRandomNumbers());
+ boolean isGameWon = false;
+
+ while (!isGameWon) {
+ isGameWon = playTurn(computerNumbers);
+ }
+ }
+
+ private boolean playTurn(BaseballNumbers computerNumbers) {
+ try {
+ BaseballNumbers playerNumbers = parseInput(inputView.readNumbers());
+ GameResult result = computerNumbers.compare(playerNumbers);
+
+ outputView.printResult(result);
+ return checkWin(result);
+ } catch (IllegalArgumentException e) {
+ outputView.printErrorMessage(e.getMessage());
+ return false;
+ }
+ }
+
+ private boolean checkWin(GameResult result) {
+ if (result.getStrikes() == 3) {
+ outputView.printGameEnd();
+ return true;
+ }
+ return false;
+ }
+
+ private BaseballNumbers parseInput(String input) {
+ if (input.length() != 3 || !input.matches("^[1-9]+$")) {
+ throw new IllegalArgumentException("1~9 사이의 숫자 3개를 입력해야 합니다.");
+ }
+ List numbers = Arrays.stream(input.split(""))
+ .map(Integer::parseInt)
+ .collect(Collectors.toList());
+ if (numbers.stream().distinct().count() != 3) {
+ throw new IllegalArgumentException("중복된 숫자는 입력할 수 없습니다.");
+ }
+ return new BaseballNumbers(numbers);
+ }
+
+ private boolean shouldRestart() {
+ try {
+ String command = inputView.readRestartCommand();
+ if (command.equals("1")) return true;
+ if (command.equals("2")) return false;
+ throw new IllegalArgumentException("1 또는 2만 입력 가능합니다.");
+ } catch (IllegalArgumentException e) {
+ outputView.printErrorMessage(e.getMessage());
+ return shouldRestart(); // 올바른 입력이 올 때까지 재귀 호출
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/baseball/model/BaseballNumbers.java b/src/main/java/baseball/model/BaseballNumbers.java
new file mode 100644
index 00000000..ffa8ea24
--- /dev/null
+++ b/src/main/java/baseball/model/BaseballNumbers.java
@@ -0,0 +1,33 @@
+package baseball.model;
+import java.util.List;
+
+public class BaseballNumbers {
+ private final List numbers;
+
+ public BaseballNumbers(List numbers) {
+ this.numbers = numbers;
+ }
+
+ public GameResult compare(BaseballNumbers other) {
+ int strikes = 0;
+ int balls = 0;
+
+ for (int i = 0; i < numbers.size(); i++) {
+ if (isStrike(other, i)) {
+ strikes++;
+ continue;
+ }
+ if (isBall(other, i)) balls++;
+ }
+
+ return new GameResult(strikes, balls);
+ }
+
+ private boolean isStrike(BaseballNumbers other, int index) {
+ return this.numbers.get(index).equals(other.numbers.get(index));
+ }
+
+ private boolean isBall(BaseballNumbers other, int index) {
+ return other.numbers.contains(this.numbers.get(index));
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/baseball/model/GameResult.java b/src/main/java/baseball/model/GameResult.java
new file mode 100644
index 00000000..d875fa10
--- /dev/null
+++ b/src/main/java/baseball/model/GameResult.java
@@ -0,0 +1,19 @@
+package baseball.model;
+
+public class GameResult {
+ private final int strikes;
+ private final int balls;
+
+ public GameResult(int strikes, int balls) {
+ this.strikes = strikes;
+ this.balls = balls;
+ }
+
+ public int getStrikes() {
+ return strikes;
+ }
+
+ public int getBalls() {
+ return balls;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/baseball/model/NumberGenerator.java b/src/main/java/baseball/model/NumberGenerator.java
new file mode 100644
index 00000000..c9411c8d
--- /dev/null
+++ b/src/main/java/baseball/model/NumberGenerator.java
@@ -0,0 +1,30 @@
+package baseball.model;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+public class NumberGenerator {
+ private static final int TARGET_SIZE = 3;
+ private final Random random = new Random();
+
+ public List createRandomNumbers() {
+ List numbers = new ArrayList<>();
+ while (numbers.size() < TARGET_SIZE) {
+ int randomNumber = generateRandomNumber();
+ addIfUnique(numbers, randomNumber);
+ }
+ return numbers;
+ }
+
+ private int generateRandomNumber() {
+ // nextInt(9)는 0~8을 반환하므로 +1을 해서 1~9로 만듭니다.
+ return random.nextInt(9) + 1;
+ }
+
+ private void addIfUnique(List numbers, int number) {
+ if (!numbers.contains(number)) {
+ numbers.add(number);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/baseball/view/InputView.java b/src/main/java/baseball/view/InputView.java
new file mode 100644
index 00000000..d4697301
--- /dev/null
+++ b/src/main/java/baseball/view/InputView.java
@@ -0,0 +1,17 @@
+package baseball.view;
+
+import java.util.Scanner;
+
+public class InputView {
+ private static final Scanner scanner = new Scanner(System.in);
+
+ public String readNumbers() {
+ System.out.print("숫자를 입력해주세요 : ");
+ return scanner.nextLine();
+ }
+
+ public String readRestartCommand() {
+ System.out.println("게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.");
+ return scanner.nextLine();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/baseball/view/OutputView.java b/src/main/java/baseball/view/OutputView.java
new file mode 100644
index 00000000..bf535ec7
--- /dev/null
+++ b/src/main/java/baseball/view/OutputView.java
@@ -0,0 +1,33 @@
+package baseball.view;
+
+import baseball.model.GameResult;
+
+public class OutputView {
+ public void printStartMessage() {
+ System.out.println("숫자 야구 게임을 시작합니다.");
+ }
+
+ public void printResult(GameResult result) {
+ if (result.getStrikes() == 0 && result.getBalls() == 0) {
+ System.out.println("낫싱");
+ return;
+ }
+
+ StringBuilder sb = new StringBuilder();
+ if (result.getBalls() > 0) {
+ sb.append(result.getBalls()).append("볼 ");
+ }
+ if (result.getStrikes() > 0) {
+ sb.append(result.getStrikes()).append("스트라이크");
+ }
+ System.out.println(sb.toString().trim());
+ }
+
+ public void printGameEnd() {
+ System.out.println("3개의 숫자를 모두 맞히셨습니다! 게임 종료");
+ }
+
+ public void printErrorMessage(String message) {
+ System.out.println("[ERROR] " + message);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/baseball/BaseballModelTest.java b/src/test/java/baseball/BaseballModelTest.java
new file mode 100644
index 00000000..af141732
--- /dev/null
+++ b/src/test/java/baseball/BaseballModelTest.java
@@ -0,0 +1,74 @@
+package baseball;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import baseball.model.BaseballNumbers;
+import baseball.model.GameResult;
+import baseball.model.NumberGenerator;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class BaseballModelTest {
+
+ @Test
+ @DisplayName("생성된 숫자는 반드시 3자리여야 한다")
+ void generate_SizeTest() {
+ NumberGenerator generator = new NumberGenerator();
+ List numbers = generator.createRandomNumbers();
+
+ assertThat(numbers).hasSize(3);
+ }
+
+ @Test
+ @DisplayName("생성된 숫자는 모두 1에서 9 사이의 값이어야 한다")
+ void generate_RangeTest() {
+ NumberGenerator generator = new NumberGenerator();
+ List numbers = generator.createRandomNumbers();
+
+ // 모든 숫자가 1 이상 9 이하인지 검증
+ for (int number : numbers) {
+ assertThat(number).isBetween(1, 9);
+ }
+ }
+
+ @Test
+ @DisplayName("생성된 3개의 숫자는 서로 중복되지 않아야 한다")
+ void generate_DuplicateTest() {
+ NumberGenerator generator = new NumberGenerator();
+ List numbers = generator.createRandomNumbers();
+
+ // 중복을 허용하지 않는 Set에 넣었을 때도 크기가 3이어야 함
+ Set uniqueNumbers = new HashSet(numbers);
+
+ assertThat(uniqueNumbers).hasSize(3);
+ }
+
+ @Test
+ @DisplayName("숫자와 위치가 같으면 '스트라이크'로 판정된다")
+ void compare_StrikeTest() {
+ BaseballNumbers computer = new BaseballNumbers(List.of(1, 2, 3));
+ BaseballNumbers player = new BaseballNumbers(List.of(1, 2, 5));
+
+ // When: 비교 로직을 실행하면
+ GameResult result = computer.compare(player);
+
+ // Then: 2스트라이크여야 한다
+ assertThat(result.getStrikes()).isEqualTo(2);
+ }
+
+ @Test
+ @DisplayName("숫자는 같지만 위치가 다르면 '볼'로 판정된다")
+ void compare_BallTest() {
+ BaseballNumbers computer = new BaseballNumbers(List.of(1, 2, 3));
+ BaseballNumbers player = new BaseballNumbers(List.of(3, 1, 2));
+
+ // When: 비교 로직을 실행하면
+ GameResult result = computer.compare(player);
+
+ // Then: 3볼 0스트라이크여야 한다
+ assertThat(result.getBalls()).isEqualTo(3);
+ }
+}