diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..3c8cb92f6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,28 @@
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+replay_pid*
+
+.idea
+.gradle
+build
\ No newline at end of file
diff --git a/README.md b/README.md
index e89d6ccc6..4748c93ae 100644
--- a/README.md
+++ b/README.md
@@ -1,36 +1,93 @@
-# java_calculator
-자바 계산기 구현 미션 Repository입니다.
+## 📌 계산기 TDD, OOP로 만들기
+### 다양한 표현 수식에 대한 사칙 연산을 수행하는 콘솔 기반의 계산기를 TDD와 OOP를 기반으로 구현해내는 프로젝트
+- 메뉴 입력(0. 종료, 1. 조회, 2. 계산)을 받는 기능
+- 이전 계산 이력에 대한 결과를 순서대로 조회하는 기능
+- 수식(중위 표현식)을 입력 받아 계산 결과 값을 출력하는 기능
+- 다양한 오류 처리 (메뉴 입력 오류, 표현 수식 오류)
-## 이곳은 공개 Repo입니다.
-1. 여러분의 포트폴리오로 사용하셔도 됩니다.
-2. 때문에 이 repo를 fork한 뒤
-3. 여러분의 개인 Repo에 작업하며
-4. 이 Repo에 PR을 보내어 멘토의 코드 리뷰와 피드백을 받으세요.
-## Branch 명명 규칙
-1. 여러분 repo는 알아서 해주시고 😀(본인 레포니 main으로 하셔두 되져)
-2. prgrms-be-devcourse/spring-board 레포로 PR시 branch는 gituser_id을 적어주세요 :)
-- base repo : `여기repo` base : `username` ← head repo : `여러분repo` compare : `main`또는 `github_id`
-- 이 규칙은 멘토+팀원들과 정하여 진행해주세요 :)
-- 참고 : [Github 위치 및 피드백 기준 가이드](https://www.notion.so/backend-devcourse/Github-e1a0908a6bbf4aeaa5a62981499bb215)
-### 과제를 통해 기대하는 역량
+
+
+## 👩💻 요구 사항과 구현 내용
+### 전체적인 클래스 관계 구조도
+
-- 깃허브를 통한 코드리뷰를 경험해보자
-- 기본적인 테스트 코드 작성 및 활용하는 능력해보자
-- 스스로 OOP를 생각하고 코드로 옮길 수 있는 능력해보자
-### 요구사항
+### 기능 구현
+
+- 콘솔로 구현입니다.(스윙으로 구현하시는 분들 계실까봐)
- 객체지향적인 코드로 계산기 구현하기
- - [ ] 더하기
- - [ ] 빼기
- - [ ] 곱하기
- - [ ] 나누기
- - [ ] 우선순위(사칙연산)
-- [ ] 테스트 코드 구현하기
-- [ ] 계산 이력을 맵으로 데이터 저장기능 만들기(인메모리 DB)
-- (선택) 정규식 사용
+ - [x] 더하기
+ - [x] 빼기
+ - [x] 곱하기
+ - [x] 나누기
+ - [x] 우선순위(사칙연산)
+- [x] 테스트 코드 구현하기
+- [x] 계산 이력을 맵으로 데이터 저장기능 만들기
+ - 애플리케이션이 동작하는 동안 데이터베이스 외에 데이터를 저장할 수 있는 방법을 고안해보세요.
+- [x] 정규식 사용
+
+
+
+
+## ✅ 피드백 반영사항
+
+
+ 첫번째 PR 반영사항
+
+### 1. CalculatorApp
+- 전략 패턴을 사용하여 CalculatorApp에서 구체적인 객체를 생성하여 `Calculator`에 주입합니다.
+```java
+ CalculatorConsole console = new CalculatorConsole();
+
+ new Calculator(
+ new PostfixCalculator(),
+ new InfixToPostfixConverter(),
+ console,
+ console,
+ new CalculationRepository()
+ ).run();
+```
+
+### 2. 다양한 형태의 Converter 구현
+- 다양한 수식 간의 변환을 가능하게 하는 `ExpressionConverter` 인터페이스로 추상화를 하고, 현재 계산기 프로그램에서는 중위 표현식을 후위 표현식으로 변환하는 `InfixToPosfixConverter` 클래스로 구현하여 동적으로 적절한 컨버터가 선택되도록 하였습니다.
+- `convert()` 메서드에서 String 형태의 expression을 변환하여 피연산자와 연산자의 리스트 형태의 ArrayList 타입으로 수식을 변환합니다.
+
+### 3. Calculator 추상화
+- 컨버터도 추상화할 수 있으면, 구체적인 표현식을 적절히 계산하여 계산 결과 값을 도출하는 계산기도 추상화할 수 있다고 생각하였습니다.
+- 따라서 `calculate()` 추상 메소드를 포함하는 `BasicCalculator` 인터페이스로 추상화하고, 구체적으로 후위 표현 수식을 계산하는 `PostfixCalculator` 클래스로 구체화하였습니다.
+- 마찬가지로 runnable한 Application에서 동작할 때, 계산 대상인 표현 수식에 따라 동적으로 적절한 Calculator가 선택됩니다.
+
+### 4. Validation
+- 기존에 Calculator 내에서 메뉴 선택, 표현 수식을 비롯한 입력에 대한 값 검증을 하던 `validateChoiceInput()`, `validateExpression()`의 메서드는 각각 Menu 클래스 내부, `CalculatorValidator` 클래스 내부로 이동하였습니다.
+- 메서드 명 또한 valid 여부에 따라 boolean 타입을 반환한다는 점에서, 이를 잘 드러낼 수 있느 `isValidInputMenu()`, `isValidExpression()` 으로 변경하였습니다.
+
+### 5. CalculationRepository - ConcurrentHashMap과 Atomic Variable을 통한 ID값 관리로 멀티 쓰레드 환경에서의 동시성 문제 고려
+- 계산 이력을 저장하기 위해서 Map을 사용한 이유는, 추후 데이터베이스로의 확장 가능성을 생각했을 때, 각 레코드 별로 고유 PK ID값을 통해 CRUD를 편리하게 하는 것을 고려하여 각 계산 결과 객체 값에 대하여 Key 값을 Integer타입으로, 계산 결과 (CalculationResult)를 Value로 저장하도록 구현하였습니다.
+- 기존에는 맵의 Key 값으로, 맵의 크기를 기반으로 id 값을 결정하였고 이는 데이터 삭제 로직으로 확장되는 경우를 고려하지 못했습니다.
+- 쓰기 작업(put)에서 Lock을 통해 멀티 쓰레드 환경의 동시성 문제를 해결할 수 있는 ConccurrentHashMap을 이용하였습니다.
+- 마찬가지로 유니크 아이디의 경우, 맵의 Key의 타입으로 AtomicInteger을 사용하였습니다.
+
+### 6. ParameterizedTest 기반 유닛 테스트
+- 단위 테스트 코드들을 작성하였고, ParameterizedTest를 통해 다양한 입력에 대한 테스트를 수행하였습니다.
+- [ ] InfixToPostfixConverter 테스트에서 Failed 1개 발생
+- [ ] 계산기 통합 테스트 Failed
+
+### 7. 상수 관리
+- 여러 클래스에서 사용되는 상수를 따로 하나의 클래스에서 관리하는 것은 객체 지향적이지 않다는 피드백을 바탕으로 각 클래스에서 사용하는 상수들은 클래스 내부로 옮겼습니다.
+
+### 8. toString의 쓰임
+- 디버그나 로깅 목적으로 toString이 사용된 다는 것을 새로 알게 되었고,
+- `CalculationRepository`에 객체 저장 시에 `CalculationResult` 자체를 매개 변수로 넣어주도록 변경하였습니다.
+
+### 9. Operation
+- 사칙 연산을 수행하는 `Operation` 객체를 싱글톤으로 생성하도록 LazyHolder의 방식으로 구현하였습니다.
+- [ ] Operation의 Converter, Calculator에서 모두 Operation을 필요로 한다는 점 -> static하게 사용할 수 있는 방법이 없을까? 고민하고 있습니다.. !
+- [ ] 사칙 연산을 수행해내기 위해서는, String(+,-,*,/)과 Operator Enum 객체들을 Map으로 저장하는 `OperatorMap` 을 초기화하는 `Operation` 클래스의 객체가 필수적으로 생성되어야 하는데, Converter과 Calculator 모두 Operation에 의존성을 띄고 있어 설계 리팩토링을 진행해야 할 것 같습니다..
+
+
### 실행결과(콘솔)
```
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 000000000..71e44b8b7
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,34 @@
+plugins {
+ id 'java'
+}
+
+group 'org.example'
+version '1.0-SNAPSHOT'
+sourceCompatibility = '17'
+
+
+repositories {
+ mavenCentral()
+}
+configurations {
+ compileOnly {
+ extendsFrom annotationProcessor
+ }
+}
+
+
+dependencies {
+
+ testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2'
+ testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2'
+ testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.2'
+
+ implementation 'org.assertj:assertj-core:3.24.2'
+
+ implementation group: 'org.projectlombok', name: 'lombok', version: '1.18.28'
+
+}
+
+tasks.test {
+ useJUnitPlatform()
+}
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..41dfb8790
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100644
index 000000000..1b6c78733
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,234 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+APP_NAME="Gradle"
+APP_BASE_NAME=${0##*/}
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 000000000..107acd32c
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 000000000..948943cf9
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,2 @@
+rootProject.name = 'javacalculator'
+
diff --git a/src/main/java/CalculatorApp.java b/src/main/java/CalculatorApp.java
new file mode 100644
index 000000000..7b20dbe6e
--- /dev/null
+++ b/src/main/java/CalculatorApp.java
@@ -0,0 +1,19 @@
+import calculator.Calculator;
+import calculator.CalculatorConsole;
+import calculator.model.calculator.PostfixCalculator;
+import calculator.model.converter.InfixToPostfixConverter;
+import calculator.repository.CalculationRepository;
+
+public class CalculatorApp {
+ public static void main(String[] args) {
+ CalculatorConsole console = new CalculatorConsole();
+
+ new Calculator(
+ new PostfixCalculator(),
+ new InfixToPostfixConverter(),
+ console,
+ console,
+ new CalculationRepository()
+ ).run();
+ }
+}
diff --git a/src/main/java/calculator/Calculator.java b/src/main/java/calculator/Calculator.java
new file mode 100644
index 000000000..b9dd25a42
--- /dev/null
+++ b/src/main/java/calculator/Calculator.java
@@ -0,0 +1,67 @@
+package calculator;
+
+import calculator.model.menu.Menu;
+import calculator.model.calculator.CalculationResult;
+import calculator.model.ExpressionConverter;
+import calculator.io.Input;
+import calculator.io.Output;
+import calculator.model.BasicCalculator;
+import calculator.repository.CalculationRepository;
+import calculator.model.validator.CalculatorValidator;
+
+import java.util.List;
+
+
+public class Calculator implements Runnable {
+ private final BasicCalculator calculator;
+ private final ExpressionConverter expressionConverter;
+ private final Input input;
+ private final Output output;
+ private final CalculationRepository calculationRepository;
+
+ private static final String MENU_INPUT_ERROR = "메뉴가 올바르게 입력되지 않았습니다.";
+ private static final String INVALID_INPUT_EXPRESSION = "올바른 수식 표현이 아닙니다.";
+ private static final char REQUEST_EXIT = '0';
+ private static final char REQUEST_VIEW_CALCULATION_RESULT = '1';
+ private static final char REQUEST_CALCULATION = '2';
+ private static final String CHOICE_PROMPT = "\n선택 : ";
+ private static final int FIRST_INDEX = 0;
+
+ public Calculator(BasicCalculator calculator, ExpressionConverter expressionConverter, Input input, Output output, CalculationRepository calculationRepository) {
+ this.calculator = calculator;
+ this.expressionConverter = expressionConverter;
+ this.input = input;
+ this.output = output;
+ this.calculationRepository = calculationRepository;
+ }
+
+
+ @Override
+ public void run() {
+ boolean isProgramRunnable = true;
+ while (isProgramRunnable) {
+ output.putMenu();
+ String inputMenu = input.getChoice(CHOICE_PROMPT);
+ if (!Menu.isValidMenuInput(inputMenu)) {
+ output.inputError(MENU_INPUT_ERROR);
+ continue;
+ }
+
+ switch (inputMenu.charAt(FIRST_INDEX)) {
+ case REQUEST_EXIT -> isProgramRunnable = false;
+ case REQUEST_VIEW_CALCULATION_RESULT -> output.showCalculationResult(calculationRepository.findAll());
+ case REQUEST_CALCULATION -> {
+ String expression = input.getExpression();
+ if (!CalculatorValidator.isValidExpression(expression)) {
+ output.inputError(INVALID_INPUT_EXPRESSION);
+ continue;
+ }
+ List convertedExpression = expressionConverter.convert(expression);
+ Integer result = calculator.calculate(convertedExpression);
+ output.showResult(result.toString());
+ calculationRepository.save(new CalculationResult(expression, result));
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/calculator/CalculatorConsole.java b/src/main/java/calculator/CalculatorConsole.java
new file mode 100644
index 000000000..3e3f28009
--- /dev/null
+++ b/src/main/java/calculator/CalculatorConsole.java
@@ -0,0 +1,49 @@
+package calculator;
+
+import calculator.model.menu.Menu;
+import calculator.io.Input;
+import calculator.io.Output;
+import calculator.model.calculator.CalculationResult;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Scanner;
+
+public class CalculatorConsole implements Input, Output {
+ private final static Scanner scanner = new Scanner(System.in);
+
+ @Override
+ public void putMenu() {
+ Arrays.stream(Menu.values())
+ .forEach(m -> System.out.println(m.toString()));
+ }
+
+ @Override
+ public void showCalculationResult(Map result) {
+ System.out.println();
+ result.forEach((key, value) -> System.out.println(value));
+ System.out.println();
+ }
+
+ @Override
+ public void inputError(String errorResponse) {
+ System.out.println(errorResponse + "\n");
+ }
+
+ @Override
+ public void showResult(String calculationResult) {
+ System.out.println(calculationResult + "\n");
+ }
+
+ @Override
+ public String getChoice(String prompt) {
+ System.out.print(prompt);
+ return scanner.nextLine();
+ }
+
+ @Override
+ public String getExpression() {
+ System.out.println();
+ return scanner.nextLine();
+ }
+}
diff --git a/src/main/java/calculator/io/Input.java b/src/main/java/calculator/io/Input.java
new file mode 100644
index 000000000..b5b284604
--- /dev/null
+++ b/src/main/java/calculator/io/Input.java
@@ -0,0 +1,6 @@
+package calculator.io;
+
+public interface Input {
+ String getChoice(String s);
+ String getExpression();
+}
diff --git a/src/main/java/calculator/io/Output.java b/src/main/java/calculator/io/Output.java
new file mode 100644
index 000000000..223ac8a70
--- /dev/null
+++ b/src/main/java/calculator/io/Output.java
@@ -0,0 +1,12 @@
+package calculator.io;
+
+import calculator.model.calculator.CalculationResult;
+
+import java.util.Map;
+
+public interface Output {
+ void putMenu();
+ void showCalculationResult(Map result);
+ void inputError(String errorResponse);
+ void showResult(String calculationResult);
+}
diff --git a/src/main/java/calculator/model/BasicCalculator.java b/src/main/java/calculator/model/BasicCalculator.java
new file mode 100644
index 000000000..96d20b3b6
--- /dev/null
+++ b/src/main/java/calculator/model/BasicCalculator.java
@@ -0,0 +1,7 @@
+package calculator.model;
+
+import java.util.List;
+
+public interface BasicCalculator {
+ Integer calculate(List expression);
+}
diff --git a/src/main/java/calculator/model/ExpressionConverter.java b/src/main/java/calculator/model/ExpressionConverter.java
new file mode 100644
index 000000000..1cf24d96d
--- /dev/null
+++ b/src/main/java/calculator/model/ExpressionConverter.java
@@ -0,0 +1,7 @@
+package calculator.model;
+
+import java.util.List;
+
+public interface ExpressionConverter {
+ List convert(String infixExpression);
+}
diff --git a/src/main/java/calculator/model/calculator/CalculationResult.java b/src/main/java/calculator/model/calculator/CalculationResult.java
new file mode 100644
index 000000000..3611db1a2
--- /dev/null
+++ b/src/main/java/calculator/model/calculator/CalculationResult.java
@@ -0,0 +1,17 @@
+package calculator.model.calculator;
+
+public class CalculationResult {
+ private final String expression;
+ private final Integer answer;
+ private static final String CALCULATION_EQUALS_SIGN = " = ";
+
+ public CalculationResult(String expression, Integer answer){
+ this.expression = expression;
+ this.answer = answer;
+ }
+
+ @Override
+ public String toString() {
+ return expression + CALCULATION_EQUALS_SIGN + answer;
+ }
+}
diff --git a/src/main/java/calculator/model/calculator/Operation.java b/src/main/java/calculator/model/calculator/Operation.java
new file mode 100644
index 000000000..154c54715
--- /dev/null
+++ b/src/main/java/calculator/model/calculator/Operation.java
@@ -0,0 +1,73 @@
+package calculator.model.calculator;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.BiFunction;
+
+
+public class Operation {
+ private static final Map operatorMap = new HashMap<>();
+
+ private static final Integer LOW = 0;
+ private static final Integer HIGH = 1;
+
+ private static final String OPERATOR_INPUT_ERROR = "연산자 입력에 오류가 발생했습니다.";
+ private static final String INVALID_OPERAND = "올바른 피연산자가 아닙니다.";
+
+ private Operation(){
+ Arrays.stream(Operator.values())
+ .forEach(op -> operatorMap.put(op.operator, op));
+ }
+
+ public static Operation getInstance(){
+ return LazyHolder.INSTANCE;
+ }
+
+ private static class LazyHolder{
+ private static final Operation INSTANCE = new Operation();
+ }
+
+ public Integer calculate(Integer a, String operator, Integer b){
+ return Optional.ofNullable(operatorMap.get(operator))
+ .orElseThrow(() -> new IllegalArgumentException(OPERATOR_INPUT_ERROR))
+ .mapCalculate(operator,a,b);
+
+ }
+ public static Operator getOperator(String operator){
+ return operatorMap.get(operator);
+ }
+
+ public enum Operator {
+
+ PLUS("+", (num1, num2) -> num1 + num2, LOW),
+ MINUS("-", (num1, num2) -> num1 - num2, LOW),
+ MULTIPLY("*", (num1, num2) -> num1 * num2, HIGH),
+ DIVIDE("/", (num1, num2) -> num1 / num2, HIGH);
+
+
+ private final String operator;
+ private final BiFunction expression;
+ private final Integer priority;
+
+ Operator(String operator, BiFunction expression, Integer priority) {
+ this.operator = operator;
+ this.expression = expression;
+ this.priority = priority;
+ }
+
+
+ public boolean isPrioritySameOrGreater(Operator operator) {
+ return priority >= operator.priority;
+ }
+
+ public Integer mapCalculate(String operator, Integer num1, Integer num2) {
+ if(Operation.getOperator(operator) == DIVIDE && num2 == 0){
+ throw new ArithmeticException(INVALID_OPERAND);
+ }
+ return getOperator(operator).expression.apply(num1, num2);
+ }
+ }
+
+}
diff --git a/src/main/java/calculator/model/calculator/PostfixCalculator.java b/src/main/java/calculator/model/calculator/PostfixCalculator.java
new file mode 100644
index 000000000..50b75b2af
--- /dev/null
+++ b/src/main/java/calculator/model/calculator/PostfixCalculator.java
@@ -0,0 +1,33 @@
+package calculator.model.calculator;
+
+import calculator.model.BasicCalculator;
+
+import java.util.List;
+import java.util.Stack;
+
+public class PostfixCalculator implements BasicCalculator {
+
+ private static final String OPERATOR_REGEX = "[-*/+]";
+
+ @Override
+ public Integer calculate(List expression) {
+ Stack calcStack = new Stack<>();
+ Operation operation = Operation.getInstance();
+
+ int op1, op2;
+
+ for(String s : expression){
+ if(s.matches(OPERATOR_REGEX)){
+ op2 = Integer.parseInt(calcStack.pop());
+ op1 = Integer.parseInt(calcStack.pop());
+
+ Integer result = operation.calculate(op1, s, op2);
+ calcStack.push(String.valueOf(result));
+ }
+ else{
+ calcStack.push(s);
+ }
+ }
+ return Integer.valueOf(calcStack.pop());
+ }
+}
diff --git a/src/main/java/calculator/model/converter/InfixToPostfixConverter.java b/src/main/java/calculator/model/converter/InfixToPostfixConverter.java
new file mode 100644
index 000000000..ecbb12062
--- /dev/null
+++ b/src/main/java/calculator/model/converter/InfixToPostfixConverter.java
@@ -0,0 +1,59 @@
+package calculator.model.converter;
+
+import calculator.model.ExpressionConverter;
+import calculator.model.calculator.Operation;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Stack;
+
+public class InfixToPostfixConverter implements ExpressionConverter {
+ private final ArrayList postfix;
+ private final Stack opStack;
+ private final Operation operation;
+
+ private static final String EXPRESSION_DELIMITER = " ";
+ private static final String OPERATOR_REGEX = "[-*/+]";
+ private static final String OPERAND_REGEX = "^\\d+$";
+
+
+ public InfixToPostfixConverter(){
+ postfix = new ArrayList<>();
+ opStack = new Stack<>();
+ operation = Operation.getInstance();
+ }
+
+ @Override
+ public List convert(String infixExpression) {
+ StringBuilder num = new StringBuilder();
+
+ // 중위 표기식을 리스트로 변환
+ String[] infix = infixExpression.split(EXPRESSION_DELIMITER);
+
+ // 후위 표기식으로 전환
+ for(String s : infix){
+ if(s.matches(OPERATOR_REGEX)){
+ if(!num.isEmpty()){
+ postfix.add(num.toString());
+ num = new StringBuilder();
+ }
+ if (opStack.isEmpty()) opStack.push(s);
+ else {
+ if (Operation.getOperator(opStack.peek()).isPrioritySameOrGreater(Operation.getOperator(s))) {
+ postfix.add(opStack.pop());
+ }
+ opStack.push(s);
+ }
+ } else if (s.matches(OPERAND_REGEX)) {
+ num.append(s);
+ }
+ }
+ if(!num.isEmpty()) {
+ postfix.add(num.toString());
+ }
+ while(!opStack.isEmpty()){
+ postfix.add(opStack.pop());
+ }
+ return postfix;
+ }
+}
diff --git a/src/main/java/calculator/model/menu/Menu.java b/src/main/java/calculator/model/menu/Menu.java
new file mode 100644
index 000000000..8ab2bb05c
--- /dev/null
+++ b/src/main/java/calculator/model/menu/Menu.java
@@ -0,0 +1,36 @@
+package calculator.model.menu;
+
+import java.util.Arrays;
+
+public enum Menu {
+ EXIT('0', "종료"),
+ CALCULATION_HISTORY('1', "조회"),
+ CALCULATE('2', "계산");
+
+ private final Character command;
+ private final String explanation;
+
+ private static final Integer MENU_INPUT_LENGTH = 1;
+ private static final Integer FIRST_INDEX = 0;
+
+ Menu(Character command, String explanation) {
+ this.command = command;
+ this.explanation = explanation;
+ }
+
+ public Character getCommand(){
+ return command;
+ }
+
+ public static boolean isValidMenuInput(String input){
+ if (input.length() != MENU_INPUT_LENGTH) return false;
+ Character firstChar = input.charAt(FIRST_INDEX);
+ return Arrays.stream(Menu.values()).filter(m -> m.getCommand()
+ .equals(firstChar)).count() == 1;
+ }
+
+ @Override
+ public String toString() {
+ return command + ". " + explanation;
+ }
+}
diff --git a/src/main/java/calculator/model/validator/CalculatorValidator.java b/src/main/java/calculator/model/validator/CalculatorValidator.java
new file mode 100644
index 000000000..5fbfe3d6f
--- /dev/null
+++ b/src/main/java/calculator/model/validator/CalculatorValidator.java
@@ -0,0 +1,28 @@
+package calculator.model.validator;
+
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicInteger;
+
+
+public class CalculatorValidator {
+ private static final String OPERATOR_REGEX = "[-*/+]";
+ private static final String OPERAND_REGEX = "^(0|[-]?[1-9]\\d*)$";
+
+ private static final int MINIMUM_OPS = 3;
+ private static final int FIRST_INDEX = 0;
+
+ private static final String EXPRESSION_DELIMITER = " ";
+
+
+ public static boolean isValidExpression(String expression){
+ AtomicInteger index = new AtomicInteger(FIRST_INDEX);
+ long countOfValidOps = Arrays.stream(expression.split(EXPRESSION_DELIMITER))
+ .filter(e -> isEvenNumber(index) ? e.matches(OPERAND_REGEX) : e.matches(OPERATOR_REGEX))
+ .count();
+ return countOfValidOps >= MINIMUM_OPS && Arrays.stream(expression.split(" ")).count() == countOfValidOps;
+ }
+
+ private static boolean isEvenNumber(AtomicInteger index) {
+ return index.getAndIncrement() % 2 == 0;
+ }
+}
diff --git a/src/main/java/calculator/repository/CalculationRepository.java b/src/main/java/calculator/repository/CalculationRepository.java
new file mode 100644
index 000000000..8fc77a669
--- /dev/null
+++ b/src/main/java/calculator/repository/CalculationRepository.java
@@ -0,0 +1,30 @@
+package calculator.repository;
+
+import calculator.model.calculator.CalculationResult;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class CalculationRepository {
+ private static Map results;
+ private static AtomicInteger idCounter;
+
+
+ public CalculationRepository() {
+ results = new ConcurrentHashMap<>();
+ idCounter = new AtomicInteger(0);
+ }
+
+ public void save(CalculationResult result) {
+ results.put(idCounter.getAndIncrement(), result);
+ }
+
+ public Map findAll() {
+ return results;
+ }
+
+ void clearStore(){
+ results.clear();
+ }
+}
diff --git a/src/test/java/calculator/CalculatorConsoleTest.java b/src/test/java/calculator/CalculatorConsoleTest.java
new file mode 100644
index 000000000..be6c01920
--- /dev/null
+++ b/src/test/java/calculator/CalculatorConsoleTest.java
@@ -0,0 +1,74 @@
+package calculator;
+
+import calculator.model.calculator.CalculationResult;
+import calculator.repository.CalculationRepository;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class CalculatorConsoleTest {
+
+ private static ByteArrayOutputStream outputMessage;
+ private static final CalculatorConsole console = new CalculatorConsole();
+ private static CalculationRepository repository;
+
+ private static final String NEW_LINE = "\r\n";
+
+
+ @BeforeEach
+ void setUpOutputStreams(){
+ repository = new CalculationRepository();
+ outputMessage = new ByteArrayOutputStream(); // OutputStream 생성
+ System.setOut(new PrintStream(outputMessage)); // 생성한 OutputStream 으로 설정
+ }
+
+ @AfterEach
+ void restoresStreams(){
+ System.setOut(System.out);
+ }
+
+ @Test
+ @DisplayName("콘솔에 메뉴 출력하기")
+ void putMenu() {
+ console.putMenu();
+ assertEquals("0. 종료" + NEW_LINE + "1. 조회" + NEW_LINE + "2. 계산", outputMessage.toString().strip());
+
+ }
+
+ @Test
+ @DisplayName("계산 이력 콘솔에 출력하기")
+ void showCalculationResult() {
+ String expression1 = "3 + 5";
+ Integer answer1 = 8;
+
+ String expression2 = "5 * 2";
+ Integer answer2 = 10;
+
+ repository.save(new CalculationResult(expression1, answer1));
+ repository.save(new CalculationResult(expression2, answer2));
+
+
+ console.showCalculationResult(repository.findAll());
+ assertEquals("3 + 5 = 8" + NEW_LINE + "5 * 2 = 10", outputMessage.toString().strip());
+ }
+
+ @Test
+ @DisplayName("계산의 수식과 답 콘솔에 출력하기")
+ void showResult() {
+ String expression = "3 + 5";
+ Integer answer = 8;
+
+ CalculationResult result = new CalculationResult(expression, answer);
+ repository.save(result);
+
+
+ console.showResult(result.toString());
+ assertEquals("3 + 5 = 8", outputMessage.toString().strip());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/calculator/CalculatorTest.java b/src/test/java/calculator/CalculatorTest.java
new file mode 100644
index 000000000..9312d78a7
--- /dev/null
+++ b/src/test/java/calculator/CalculatorTest.java
@@ -0,0 +1,53 @@
+package calculator;
+
+import calculator.model.calculator.PostfixCalculator;
+import calculator.repository.CalculationRepository;
+import calculator.model.converter.InfixToPostfixConverter;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.PrintStream;
+
+class CalculatorTest {
+
+ private final static ByteArrayOutputStream outputMessage = new ByteArrayOutputStream();
+ private final PrintStream systemOut = System.out;
+ private final InputStream systemIn = System.in;
+
+
+ @BeforeEach
+ public void setUpStreams(){
+ System.setOut(new PrintStream(outputMessage));
+ }
+
+ @AfterEach
+ void restoresStreams(){
+ System.setOut(System.out);
+ System.setIn(systemIn);
+ }
+
+ @Test
+ @DisplayName("계산기 통합 테스트")
+ void 계산기통합테스트_계산하기() { // fail
+ String[] inputs = { "2", "3 + 5 * -1", "2", "-1 * 2 + 5 - 1", "1", "0"};
+ InputStream in = new ByteArrayInputStream(String.join("\r\n", inputs).getBytes());
+ System.setIn(in);
+
+ CalculatorConsole console = new CalculatorConsole();
+ new Calculator(
+ new PostfixCalculator(),
+ new InfixToPostfixConverter(),
+ console,
+ console,
+ new CalculationRepository()
+ ).run();
+
+ String output = outputMessage.toString();
+ System.out.println("output = " + output);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/calculator/model/calculator/CalculationResultTest.java b/src/test/java/calculator/model/calculator/CalculationResultTest.java
new file mode 100644
index 000000000..f3ae5b0f3
--- /dev/null
+++ b/src/test/java/calculator/model/calculator/CalculationResultTest.java
@@ -0,0 +1,41 @@
+package calculator.model.calculator;
+
+import calculator.model.calculator.CalculationResult;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.stream.Stream;
+
+class CalculationResultTest {
+ @ParameterizedTest
+ @DisplayName("CalculationResult의 객체 생성 후 expression과 result를 올바르게 출력하는지 검증하는 테스트")
+ @MethodSource("testData")
+ void expression_and_answer(String expression, Integer answer, String result){
+ CalculationResult calculationResult = new CalculationResult(expression, answer);
+ Assertions.assertEquals(calculationResult.toString(), result);
+ }
+
+ private static Stream testData(){
+ return Stream.of(
+ Arguments.of("3 + 5", 8, "3 + 5 = 8"),
+ Arguments.of("3 + 8 * -1", -5, "3 + 8 * -1 = -5"),
+ Arguments.of("3 + 1 - 1", 3, "3 + 1 - 1 = 3")
+ );
+ }
+
+ @ParameterizedTest
+ @DisplayName("오버라이딩한 Calculation Result의 ToString 테스트")
+ @CsvSource({
+ "3 + 5, 8, 3 + 5 = 8",
+ "-2 * 1, -2, -2 * 1 = -2"
+ })
+ void testToString(String expression, Integer answer, String expectedResult) {
+ CalculationResult result = new CalculationResult(expression, answer);
+
+ Assertions.assertEquals(expectedResult, result.toString());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/calculator/model/calculator/InfixToPostfixConverterTest.java b/src/test/java/calculator/model/calculator/InfixToPostfixConverterTest.java
new file mode 100644
index 000000000..c702a4e46
--- /dev/null
+++ b/src/test/java/calculator/model/calculator/InfixToPostfixConverterTest.java
@@ -0,0 +1,43 @@
+package calculator.model.calculator;
+
+import calculator.model.calculator.Operation;
+import calculator.model.converter.InfixToPostfixConverter;
+import calculator.model.ExpressionConverter;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Stream;
+
+
+class InfixToPostfixConverterTest {
+
+ private static ExpressionConverter converter;
+ private static final Operation operation = Operation.getInstance();
+
+ @BeforeEach
+ void beforeEach(){
+ converter = new InfixToPostfixConverter();
+ }
+
+ @ParameterizedTest
+ @MethodSource("testExpressionData")
+ void convert(String infix, List expectedPostfix) {
+ List convertedToPostfix = converter.convert(infix);
+ Assertions.assertEquals(expectedPostfix, convertedToPostfix);
+ }
+
+ private static Stream testExpressionData(){
+ return Stream.of(
+ Arguments.of("3 + 5", Arrays.asList("3", "5", "+")),
+ Arguments.of("3 - 5 * -2", Arrays.asList("3", "5", "*", "-2", "-")), // Failed
+ Arguments.of("3 + 5 - 2 * 1", Arrays.asList("3", "5", "+", "2", "1", "*", "-")),
+ Arguments.of("1 + 5 * 2", Arrays.asList("1", "5", "2", "*", "+")),
+ Arguments.of("3 + 6 * 2 / 3", Arrays.asList("3", "6", "2", "*", "3", "/", "+"))
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/calculator/model/calculator/OperationTest.java b/src/test/java/calculator/model/calculator/OperationTest.java
new file mode 100644
index 000000000..91bac110c
--- /dev/null
+++ b/src/test/java/calculator/model/calculator/OperationTest.java
@@ -0,0 +1,87 @@
+package calculator.model.calculator;
+
+import calculator.model.calculator.Operation;
+import org.junit.jupiter.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.CsvSource;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.stream.Stream;
+
+
+class OperationTest {
+
+ private static final Operation operation = Operation.getInstance();
+
+
+ @ParameterizedTest
+ @DisplayName("Operator 사칙 연산을 검증 하는 테스트")
+ @CsvSource({
+ "1, +, 5, 6",
+ "2, -,1, 1",
+ "3, *, -5, -15",
+ "12, /, 4, 3"
+ })
+ void calculate(Integer a, String operator, Integer b, Integer result) {
+ Integer calculated = operation.calculate(a, operator, b);
+ Assertions.assertEquals(result, calculated);
+ }
+
+ @ParameterizedTest
+ @DisplayName("잘못된 표현 수식에 대한 연산을 검증 하는 테스트")
+ @CsvSource({
+ "2, /, 0",
+ "3, /, 0"
+ })
+ void calculateInvalidExpression(Integer a, String operator, Integer b) {
+ try {
+ Integer calculated = operation.calculate(a, operator, b);
+ } catch (RuntimeException e){
+ Assertions.assertEquals("올바른 피연산자가 아닙니다.", e.getMessage());
+ }
+ }
+
+
+ @ParameterizedTest
+ @DisplayName("Operator 사칙 연산을 검증 하는 테스트 - 잘못된 결과 값에 대한 테스트")
+ @CsvSource({
+ "1, +, 5, 5",
+ "2, -, 1, 4",
+ "3, *, -5, 15",
+ "12, /, 4, 1"
+ })
+ void validateWrongCalculation(Integer a, String operator, Integer b, Integer result) {
+ Integer calculated = operation.calculate(a, operator, b);
+ Assertions.assertNotEquals(result, calculated);
+
+ }
+
+ @ParameterizedTest
+ @DisplayName("String 형태의 연산자를 Operator 객체로 가져오는 것을 검증하는 테스트")
+ @MethodSource("testData")
+ void getOperator(Operation.Operator operator, String value) {
+ Assertions.assertEquals(operator, Operation.getOperator(value));
+
+ }
+
+ private static Stream testData(){
+ return Stream.of(
+ Arguments.of(Operation.Operator.PLUS, "+"),
+ Arguments.of(Operation.Operator.MINUS, "-"),
+ Arguments.of(Operation.Operator.DIVIDE, "/"),
+ Arguments.of(Operation.Operator.MULTIPLY, "*")
+ );
+ }
+
+ @Test
+ @DisplayName("Operation이 싱글톤으로 객체가 생성되는지 확인하는 테스트")
+ void getInstance() {
+ Operation operation = Operation.getInstance();
+ Operation operation2 = Operation.getInstance();
+
+ Assertions.assertEquals(operation, operation2);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/calculator/model/calculator/PostfixCalculatorTest.java b/src/test/java/calculator/model/calculator/PostfixCalculatorTest.java
new file mode 100644
index 000000000..a609da585
--- /dev/null
+++ b/src/test/java/calculator/model/calculator/PostfixCalculatorTest.java
@@ -0,0 +1,35 @@
+package calculator.model.calculator;
+
+import calculator.model.calculator.PostfixCalculator;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class PostfixCalculatorTest {
+
+ @ParameterizedTest
+ @MethodSource("testData")
+ void calculate(List postfix, Integer expectedResult) {
+ PostfixCalculator postfixCalculator = new PostfixCalculator();
+
+ Integer result = postfixCalculator.calculate(postfix);
+
+ assertEquals(expectedResult, result);
+ }
+
+ private static Stream testData() {
+ return Stream.of(
+ Arguments.of(Arrays.asList("3", "5", "+"), 8),
+ Arguments.of(Arrays.asList("3", "5", "*", "-2", "-"), 13), // failed
+ Arguments.of(Arrays.asList("3", "5", "+", "2", "1", "*", "-"), 6),
+ Arguments.of(Arrays.asList("1", "5", "2", "*", "+"), 11),
+ Arguments.of(Arrays.asList("3", "6", "2", "*", "3", "/", "+"), 7)
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/calculator/model/menu/MenuTest.java b/src/test/java/calculator/model/menu/MenuTest.java
new file mode 100644
index 000000000..8bbb8d9d7
--- /dev/null
+++ b/src/test/java/calculator/model/menu/MenuTest.java
@@ -0,0 +1,50 @@
+package calculator.model.menu;
+
+import calculator.model.menu.Menu;
+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.ValueSource;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class MenuTest {
+
+ @ParameterizedTest
+ @ValueSource(strings = {"0", "1", "2"})
+ @DisplayName("올바른 메뉴 선택을 하였을 때 성공하는 테스트")
+ void isValidMenu(String menuInput) {
+ assertTrue(Menu.isValidMenuInput(menuInput));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"3", "calculate", "no", " ", "\n"})
+ @DisplayName("올바르지 메뉴 선택을 하였을 때 ")
+ void isNotValidMenuInput(String wrongMenuInput) {
+ assertFalse(Menu.isValidMenuInput(wrongMenuInput));
+ }
+
+ @Test
+ @DisplayName("Menu의 오버라이딩한 toString이 잘 작동하는지 테스트")
+ void testToString() {
+ List menuStringExpected = List.of(
+ "0. 종료",
+ "1. 조회",
+ "2. 계산"
+ );
+
+
+ List menuList = Arrays.stream(Menu.values())
+ .map(Menu::toString).toList();
+ Assertions.assertThat(menuList)
+ .hasSize(3)
+ .contains("0. 종료")
+ .contains("1. 조회")
+ .contains("2. 계산")
+ .isEqualTo(menuStringExpected);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/calculator/model/validator/CalculatorValidatorTest.java b/src/test/java/calculator/model/validator/CalculatorValidatorTest.java
new file mode 100644
index 000000000..39e9154fa
--- /dev/null
+++ b/src/test/java/calculator/model/validator/CalculatorValidatorTest.java
@@ -0,0 +1,50 @@
+package calculator.model.validator;
+
+import calculator.model.validator.CalculatorValidator;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class CalculatorValidatorTest {
+
+ @ParameterizedTest
+ @MethodSource("testValidExpression")
+ @DisplayName("올바른 수식 표현을 검증하는 테스트")
+ void validateExpression(String expression) {
+ boolean isValidResult = CalculatorValidator.isValidExpression(expression);
+ assertTrue(isValidResult);
+ }
+
+ private static Stream testValidExpression(){
+ return Stream.of(
+ Arguments.of("3 + 6 / -2"),
+ Arguments.of("3 * 1 - 2"),
+ Arguments.of("-2 * -1 + 3")
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("testInvalidExpression")
+ @DisplayName("올바르지 않은 수식 표현을 검증하는 테스트")
+ void validateInvalidExpression(String expression) {
+ boolean isValidResult = CalculatorValidator.isValidExpression(expression);
+ assertFalse(isValidResult);
+ }
+
+ private static Stream testInvalidExpression() {
+ return Stream.of(
+ Arguments.of("3 + "),
+ Arguments.of("3"),
+ Arguments.of("+"),
+ Arguments.of("-3"),
+ Arguments.of("-3 + + "),
+ Arguments.of("-3 + 5 5 6"),
+ Arguments.of("+ 1 - 5")
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/calculator/model/validator/ExpressionValidationTest.java b/src/test/java/calculator/model/validator/ExpressionValidationTest.java
new file mode 100644
index 000000000..139c7abe9
--- /dev/null
+++ b/src/test/java/calculator/model/validator/ExpressionValidationTest.java
@@ -0,0 +1,35 @@
+package calculator.model.validator;
+
+import calculator.model.validator.CalculatorValidator;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+public class ExpressionValidationTest {
+ @Test
+ @DisplayName("올바른 연산 식을 입력한 경우")
+ void validExpression(){
+ Assertions.assertThat(CalculatorValidator.isValidExpression("3 + 5 * 2 - 1"))
+ .isEqualTo(true);
+ }
+ @Test
+ @DisplayName("올바른 연산자를 입력하지 않은 경우")
+ void invalidOperator(){
+ Assertions.assertThat(CalculatorValidator.isValidExpression("3 _ 5 * 2"))
+ .isEqualTo(false);
+ }
+
+ @Test
+ @DisplayName("올바른 연산 식을 입력하지 않은 경우")
+ void invalidExpression(){
+ Assertions.assertThat(CalculatorValidator.isValidExpression("- 5 / 5 + 1"))
+ .isEqualTo(false);
+ }
+
+ @Test
+ @DisplayName("분모가 0 또는 음수인 경우 에러 처리")
+ void invalidArithmeticOperation(){
+ Assertions.assertThat(CalculatorValidator.isValidExpression("3 + 5 / -1"))
+ .isEqualTo(false);
+ }
+}
diff --git a/src/test/java/calculator/repository/CalculationRepositoryTest.java b/src/test/java/calculator/repository/CalculationRepositoryTest.java
new file mode 100644
index 000000000..92dbbc47b
--- /dev/null
+++ b/src/test/java/calculator/repository/CalculationRepositoryTest.java
@@ -0,0 +1,63 @@
+package calculator.repository;
+
+import calculator.model.calculator.CalculationResult;
+import org.assertj.core.api.Assertions;
+import org.assertj.core.data.Index;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+
+import java.util.List;
+import static org.junit.jupiter.api.Assertions.*;
+
+class CalculationRepositoryTest {
+
+ private static CalculationRepository repository;
+
+ @BeforeEach
+ void beforeEach() {
+ repository = new CalculationRepository();
+ }
+
+ @AfterEach
+ void afterEach(){
+ repository.clearStore();
+ }
+
+ @Test
+ @DisplayName("CalculationRepository에 CaclulationResult가 잘 저장되었는지 확인하는 테스트")
+ void save() {
+ String expression = "3 + 5 * 2";
+ Integer answer = 13;
+
+ repository.save(new CalculationResult(expression, answer));
+
+ List results = repository.findAll().values().stream().map(CalculationResult::toString).toList();
+ Assertions.assertThat(results)
+ .hasSize(1)
+ .contains("3 + 5 * 2 = 13", Index.atIndex(0));
+ }
+
+ @Test
+ @DisplayName("Calculation Repository에서 CalculationResult 값들을 잘 가져오는지 확인하는 테스트")
+ void findAll() {
+ String expression1 = "3 + 5 * 2";
+ Integer answer1 = 13;
+
+ String expression2 = "5 - 1 * 2";
+ Integer answer2 = 3;
+
+ repository.save(new CalculationResult(expression1, answer1));
+ repository.save(new CalculationResult(expression2, answer2));
+
+ List results = repository.findAll().values().stream().map(CalculationResult::toString).toList();
+ assertEquals(2, results.size());
+ Assertions.assertThat(results)
+ .hasSize(2)
+ .contains("3 + 5 * 2 = 13")
+ .contains("5 - 1 * 2 = 3");
+
+ }
+}
\ No newline at end of file