From 5aee2d21810105f4f5b25978b62021cac4c49494 Mon Sep 17 00:00:00 2001 From: LimGyeongHan <91595140+conycomy@users.noreply.github.com> Date: Mon, 20 Oct 2025 16:32:22 +0900 Subject: [PATCH 01/13] docs: Update README.md --- README.md | 123 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 122 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bd90ef0247..d70a978705 100644 --- a/README.md +++ b/README.md @@ -1 +1,122 @@ -# java-calculator-precourse \ No newline at end of file +# java-calculator-precourse +# 문자열 덧셈 계산기 + +입력된 문자열을 구분자를 기준으로 분리하여 합계를 계산하는 프로그램입니다. +절차지향적으로 기능을 완성한 뒤, 객체지향 원칙에 따라 리팩토링하며 구조를 개선했습니다. + +--- + +## 미션 개요 + +- 우아한테크코스 프리코스 1주차 과제 +- 문자열 입력을 받아 구분자 기준으로 숫자를 분리하고, 합을 계산하는 프로그램 +- 기본 구분자( `,` , `:` )와 커스텀 구분자(`//;\n`) 모두 지원 +- 잘못된 입력(음수, 문자 등)에 대해 예외 발생 + +--- + +## 기능 요구사항 + +- 입력된 문자열에서 숫자를 추출하여 덧셈 결과를 반환한다. +- 기본 구분자(쉼표 `,` , 콜론 `:`)를 사용한다. +- 커스텀 구분자를 지정할 수 있다. (예: `//;\n1;2;3`) +- 잘못된 입력(문자, 음수, 공백 등)은 `IllegalArgumentException`을 발생시킨다. + +--- + +## 구현 단계별 정리 + +이 프로젝트는 **절차지향적으로 구현한 뒤**, +**리팩토링을 통해 객체지향적으로 개선한 과정**을 담고 있습니다. +핵심 로직을 세분화하고, 역할에 따라 클래스를 분리하며 점진적으로 구조를 발전시켰습니다. + +--- + +### 1단계: 핵심 로직 구현 (절차지향 → 기능 분리) + +- 문자열을 입력받아 구분자 기준으로 분리하고, 숫자를 더하는 기본 기능 구현 +- 커스텀 구분자(`//;\n`) 처리 및 음수 검증 등 비즈니스 규칙을 절차적으로 구현 +- 기능 단위로 메서드를 분리하여 테스트 용이성 확보 + +--- + +### 2단계: 구조화 및 객체지향 리팩토링 + +- **DelimiterInfo**: 구분자 정규식과 숫자 문자열을 담는 DTO +- **DelimiterParser / DefaultDelimiterParser**: 입력 문자열 분석 및 `DelimiterInfo` 반환 +- **NumberParser**: 문자열을 정수로 변환하며 예외 처리 +- **Validator**: 음수 값 검증 후 `IllegalArgumentException` 발생 +- **StringCalculator** + - 구성 요소를 주입받아 순차적으로 호출 + - 문자열 분리 → 숫자 변환 → 검증 → 합산 로직 수행 +- **Calculator 인터페이스** + - `calculate(String input)` 메서드로 계산 행위의 규약 정의 + +--- + +### 3단계: 입출력 연결 및 실행 환경 구성 + +- **InputHandler** + - `Console.readLine()`으로 사용자 입력 처리 및 자원 정리 +- **Application (main)** + - 입력 수집 → `StringCalculator` 실행 → 결과 출력 + - `try-catch-finally` 구문을 통해 예외 처리 및 리소스 정리 + +--- + +## 예외 상황 + +- 문자가 포함된 입력값 (예: `"a,2,3"`, `"1b:4"`) +- 음수 입력값 (예: `"-1,2,3"`) +- 구분자가 연속된 경우 (예: `"1,,2"`) +- 소수 입력 (예: `"1.5,2"`) +- 정수 범위 초과 입력 +- 빈 문자열 또는 공백 입력 (예: `""`, `" "`) +- `null` 또는 입력이 존재하지 않는 경우 +- 숫자만 단독 입력된 경우 + +--- + +## 미션 진행 방향 + +이 미션은 **절차지향적으로 작동하는 코드를 먼저 작성한 뒤**, +객체지향 원칙(OOP)에 따라 **역할과 책임 중심으로 구조를 개선**하는 것을 목표로 합니다. + +1. 절차지향적으로 기능을 완성한다. +2. 역할과 책임을 분리하여 구조를 개선한다. +3. 테스트 가능한 구조로 리팩토링한다. + +--- + +## 커밋 컨벤션 + +AngularJS Commit Message 규칙을 참고했습니다. +커밋은 **기능 단위**로 나누어 작성합니다. (예: 기본 구분자 처리, 커스텀 구분자 추가, 음수 예외 처리 등) + +**Allowed ``** + +- feat: 새로운 기능 추가 +- fix: 버그 수정 +- docs: 문서 수정 +- style: 코드 포맷팅 +- refactor: 코드 리팩토링 +- test: 테스트 코드 추가 +- chore: 빌드, 설정 등 유지보수 작업 + +> 콜론(`:`) 뒤에는 반드시 공백 한 칸을 둡니다. + +--- + +## 어떤 점에 집중했는가 + +- 절차지향적 설계로 기본 동작 완성 능력 향상 +- 예외 처리와 입력 검증을 체계적으로 구현 +- 객체지향적 리팩토링을 통한 책임 분리 연습 +- 클린 코드 원칙을 적용하고 유지보수성을 고려한 설계 + +--- + +## 요약 + +이 프로젝트는 문자열 계산기를 절차지향적으로 구현한 뒤, +객체지향 설계 원칙에 따라 구조를 개선하며 **깨끗하고 확장 가능한 코드**로 발전시키는 과정을 담고 있습니다. From 7fc586f43ce49fad3ee1a6e2dfde4cb6f7f15f85 Mon Sep 17 00:00:00 2001 From: conycomy Date: Mon, 20 Oct 2025 16:37:40 +0900 Subject: [PATCH 02/13] =?UTF-8?q?feat:=20=EB=AC=B8=EC=9E=90=EC=97=B4=20?= =?UTF-8?q?=EB=8D=A7=EC=85=88=20=EA=B3=84=EC=82=B0=EA=B8=B0=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20=EA=B8=B0=EB=8A=A5(=EC=A0=88=EC=B0=A8=EC=A7=80?= =?UTF-8?q?=ED=96=A5=20=EB=B0=A9=EC=8B=9D)=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/calculator/Application.java | 78 ++++++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/src/main/java/calculator/Application.java b/src/main/java/calculator/Application.java index 573580fb40..159e2ca121 100644 --- a/src/main/java/calculator/Application.java +++ b/src/main/java/calculator/Application.java @@ -1,7 +1,81 @@ package calculator; +import java.util.regex.Pattern; +import camp.nextstep.edu.missionutils.Console; + public class Application { + public static void main(String[] args) { - // TODO: 프로그램 구현 + System.out.println("===덧셈할 문자열을 입력해주세요==="); + String input = Console.readLine(); // Console.readLine() 사용 + + try { + if (input == null || input.trim().isEmpty()) { + System.out.println("결과: 0"); + return; + } + + String basicDelimiterRegex = "[,:]"; + String finalDelimiterRegex = basicDelimiterRegex; + String numbersToSplit = input; + + // 2. 커스텀 구분자 + if (input.startsWith("//")) { + int delimiterEndIndex = input.indexOf("\n"); + + if (delimiterEndIndex == -1) { + throw new IllegalArgumentException("커스텀 구분자 선언 후 반드시 줄 바꿈(\\n)을 해야합니다."); + } + String customDelimiter = input.substring(2, delimiterEndIndex); + + finalDelimiterRegex += "|" + Pattern.quote(customDelimiter); + + numbersToSplit = input.substring(delimiterEndIndex + 1); + } + + + String[] parts = numbersToSplit.split(finalDelimiterRegex); + + int sum = 0; + StringBuilder negativeNumbers = new StringBuilder(); + + for (String part : parts) { + String trimmedS = part.trim(); + + if (trimmedS.isEmpty()) { + continue; + } + + if (!trimmedS.matches("^-?\\d+$")) { + throw new IllegalArgumentException("잘못된 입력 형식입니다. 숫자만 입력이 가능합니다: " + trimmedS); + } + + int num = Integer.parseInt(trimmedS); + + if (num < 0) { + if(negativeNumbers.length() > 0) { + negativeNumbers.append(", "); + } + negativeNumbers.append(num); + continue; + } + + sum += num; + } + + if (negativeNumbers.length() > 0) { + throw new IllegalArgumentException("음수는 입력이 불가능합니다: " + negativeNumbers.toString()); + } + + System.out.println("결과: " + sum); + + } catch (IllegalArgumentException e) { + System.out.println("오류: " + e.getMessage()); + } catch (Exception e) { + System.out.println("예상치 못한 오류가 발생했습니다."); + // e.printStackTrace(); + } finally { + Console.close(); + } } -} +} \ No newline at end of file From c420bfe075c30fdb4e111de889db5572099228e6 Mon Sep 17 00:00:00 2001 From: conycomy Date: Mon, 20 Oct 2025 17:59:17 +0900 Subject: [PATCH 03/13] =?UTF-8?q?refactor:=20DelimiterParser=20=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=EB=A1=9C=20=EB=AC=B8=EC=9E=90=EC=97=B4=20=EB=B6=84?= =?UTF-8?q?=EC=84=9D=20=EC=B1=85=EC=9E=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../calculator/DefaultDelimiterParser.java | 30 +++++++++++++++++++ src/main/java/calculator/DelimiterInfo.java | 18 +++++++++++ src/main/java/calculator/DelimiterParser.java | 8 +++++ 3 files changed, 56 insertions(+) create mode 100644 src/main/java/calculator/DefaultDelimiterParser.java create mode 100644 src/main/java/calculator/DelimiterInfo.java create mode 100644 src/main/java/calculator/DelimiterParser.java diff --git a/src/main/java/calculator/DefaultDelimiterParser.java b/src/main/java/calculator/DefaultDelimiterParser.java new file mode 100644 index 0000000000..8cea5b235b --- /dev/null +++ b/src/main/java/calculator/DefaultDelimiterParser.java @@ -0,0 +1,30 @@ +package calculator; + +import java.util.regex.Pattern; + +public class DefaultDelimiterParser implements DelimiterParser { + + private static final String BASIC_DELIMITER_REGEX = "[,:]"; + + @Override + public DelimiterInfo parse(String input) { + String finalDelimiterRegex = BASIC_DELIMITER_REGEX; + String numbersToSplit = input; + + if(input.startsWith("//")) { + int delimiterEndIndex = input.indexOf("\n"); + + if (delimiterEndIndex == -1) { + throw new IllegalArgumentException("커스텀 구분자 선언 후 반드시 줄 바꿈(\\n)을 해야합니다."); + } + + String customDelimiter = input.substring(2, delimiterEndIndex); + finalDelimiterRegex += "|" + Pattern.quote(customDelimiter); + + numbersToSplit = input.substring(delimiterEndIndex + 1); + } + + return new DelimiterInfo(finalDelimiterRegex, numbersToSplit); + } + +} \ No newline at end of file diff --git a/src/main/java/calculator/DelimiterInfo.java b/src/main/java/calculator/DelimiterInfo.java new file mode 100644 index 0000000000..fbdc6b297b --- /dev/null +++ b/src/main/java/calculator/DelimiterInfo.java @@ -0,0 +1,18 @@ +package calculator; + + +public class DelimiterInfo { + + final private String delimiterRegex; + final private String numbersString; + + public DelimiterInfo(String delimiterRegex, String numbersString) { + this.delimiterRegex = delimiterRegex; + this.numbersString = numbersString; + } + + public String getDelimiterRegex() { + return delimiterRegex; + } + +} diff --git a/src/main/java/calculator/DelimiterParser.java b/src/main/java/calculator/DelimiterParser.java new file mode 100644 index 0000000000..3439096e93 --- /dev/null +++ b/src/main/java/calculator/DelimiterParser.java @@ -0,0 +1,8 @@ +package calculator; + +public interface DelimiterParser { + + DelimiterInfo parse(String input); + +} + From 25f1471221e86a422b387a69f04e8c79fa5967dd Mon Sep 17 00:00:00 2001 From: conycomy Date: Mon, 20 Oct 2025 18:09:27 +0900 Subject: [PATCH 04/13] =?UTF-8?q?refactor:=20Application=EC=97=90=EC=84=9C?= =?UTF-8?q?=20DelimiterParser=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=A1=9C=EC=A7=81=20=EB=8C=80=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/calculator/Application.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/calculator/Application.java b/src/main/java/calculator/Application.java index 159e2ca121..b89866932c 100644 --- a/src/main/java/calculator/Application.java +++ b/src/main/java/calculator/Application.java @@ -14,6 +14,7 @@ public static void main(String[] args) { System.out.println("결과: 0"); return; } +/* String basicDelimiterRegex = "[,:]"; String finalDelimiterRegex = basicDelimiterRegex; @@ -32,9 +33,12 @@ public static void main(String[] args) { numbersToSplit = input.substring(delimiterEndIndex + 1); } + String[] parts = numbersToSplit.split(finalDelimiterRegex); +*/ + DelimiterParser parser = new DefaultDelimiterParser(); + DelimiterInfo info = parser.parse(input); - String[] parts = numbersToSplit.split(finalDelimiterRegex); int sum = 0; StringBuilder negativeNumbers = new StringBuilder(); From cd7edea46c3abb9414c40ee8606be848ec0f1737 Mon Sep 17 00:00:00 2001 From: conycomy Date: Mon, 20 Oct 2025 18:15:20 +0900 Subject: [PATCH 05/13] =?UTF-8?q?fix:=20Application=EC=97=90=EC=84=9C=20De?= =?UTF-8?q?limiterInfo=20=EA=B8=B0=EB=B0=98=20=EB=AC=B8=EC=9E=90=EC=97=B4?= =?UTF-8?q?=EC=9D=84=20split=20=EB=A1=9C=EC=A7=81=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/calculator/Application.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/calculator/Application.java b/src/main/java/calculator/Application.java index b89866932c..647b318513 100644 --- a/src/main/java/calculator/Application.java +++ b/src/main/java/calculator/Application.java @@ -39,6 +39,10 @@ public static void main(String[] args) { DelimiterParser parser = new DefaultDelimiterParser(); DelimiterInfo info = parser.parse(input); + String numbersToSplit = info.getNumbersString(); + String finalDelimiterRegex = info.getDelimiterRegex(); + + String[] parts = numbersToSplit.split(finalDelimiterRegex); int sum = 0; StringBuilder negativeNumbers = new StringBuilder(); From 34f86c97cae3acde930108c6033ae229439ba4ea Mon Sep 17 00:00:00 2001 From: conycomy Date: Mon, 20 Oct 2025 18:18:37 +0900 Subject: [PATCH 06/13] =?UTF-8?q?feat:=20DelimiterInfo=EC=97=90=20getNumbe?= =?UTF-8?q?rsString()=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/calculator/DelimiterInfo.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/calculator/DelimiterInfo.java b/src/main/java/calculator/DelimiterInfo.java index fbdc6b297b..2ff2f02afa 100644 --- a/src/main/java/calculator/DelimiterInfo.java +++ b/src/main/java/calculator/DelimiterInfo.java @@ -15,4 +15,7 @@ public String getDelimiterRegex() { return delimiterRegex; } + public String getNumbersString() { + return numbersString; + } } From 8efb09da66ac7db198ae4d5fc5fed58687186516 Mon Sep 17 00:00:00 2001 From: conycomy Date: Mon, 20 Oct 2025 23:24:55 +0900 Subject: [PATCH 07/13] =?UTF-8?q?feat:=20=EC=BB=A4=EC=8A=A4=ED=85=80=20?= =?UTF-8?q?=EA=B5=AC=EB=B6=84=EC=9E=90=20=ED=8C=8C=EC=8B=B1=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EB=82=98=EB=88=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../calculator/DefaultDelimiterParser.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/main/java/calculator/DefaultDelimiterParser.java b/src/main/java/calculator/DefaultDelimiterParser.java index 8cea5b235b..743a4df5a5 100644 --- a/src/main/java/calculator/DefaultDelimiterParser.java +++ b/src/main/java/calculator/DefaultDelimiterParser.java @@ -8,23 +8,27 @@ public class DefaultDelimiterParser implements DelimiterParser { @Override public DelimiterInfo parse(String input) { + // 테스트 입력의 "\\n" 문자열을 실제 개행 문자 '\n'으로 변환 + String sanitizedInput = input.replace("\r", "").replace("\\n", "\n"); + String finalDelimiterRegex = BASIC_DELIMITER_REGEX; - String numbersToSplit = input; + String numbersToSplit; - if(input.startsWith("//")) { - int delimiterEndIndex = input.indexOf("\n"); + if (sanitizedInput.startsWith("//")) { + int delimiterEndIndex = sanitizedInput.indexOf("\n"); if (delimiterEndIndex == -1) { throw new IllegalArgumentException("커스텀 구분자 선언 후 반드시 줄 바꿈(\\n)을 해야합니다."); } - String customDelimiter = input.substring(2, delimiterEndIndex); + String customDelimiter = sanitizedInput.substring(2, delimiterEndIndex); finalDelimiterRegex += "|" + Pattern.quote(customDelimiter); - numbersToSplit = input.substring(delimiterEndIndex + 1); + numbersToSplit = sanitizedInput.substring(delimiterEndIndex + 1); + } else { + numbersToSplit = sanitizedInput; } return new DelimiterInfo(finalDelimiterRegex, numbersToSplit); } - -} \ No newline at end of file +} From dce87e5a340b7c6beac8fcca3400aaa23adc4c97 Mon Sep 17 00:00:00 2001 From: conycomy Date: Mon, 20 Oct 2025 23:25:25 +0900 Subject: [PATCH 08/13] =?UTF-8?q?feat:=20=EC=9D=8C=EC=88=98=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/calculator/CollectAllValidator.java | 25 +++++++++++++++++++ .../java/calculator/ValidationStrategy.java | 9 +++++++ 2 files changed, 34 insertions(+) create mode 100644 src/main/java/calculator/CollectAllValidator.java create mode 100644 src/main/java/calculator/ValidationStrategy.java diff --git a/src/main/java/calculator/CollectAllValidator.java b/src/main/java/calculator/CollectAllValidator.java new file mode 100644 index 0000000000..93f8910e79 --- /dev/null +++ b/src/main/java/calculator/CollectAllValidator.java @@ -0,0 +1,25 @@ +package calculator; + +import java.math.BigInteger; +import java.util.List; + +public class CollectAllValidator implements ValidationStrategy { + + @Override + public void validate(List numbers) { + StringBuilder negativeNumbers = new StringBuilder(); + + for (BigInteger number : numbers) { + if (number.compareTo(BigInteger.ZERO) < 0) { + if (negativeNumbers.length() > 0) { + negativeNumbers.append(", "); + } + negativeNumbers.append(number); + } + } + + if (negativeNumbers.length() > 0) { + throw new IllegalArgumentException("음수는 입력이 불가능합니다 :" + negativeNumbers.toString()); } + } + +} \ No newline at end of file diff --git a/src/main/java/calculator/ValidationStrategy.java b/src/main/java/calculator/ValidationStrategy.java new file mode 100644 index 0000000000..6543faa513 --- /dev/null +++ b/src/main/java/calculator/ValidationStrategy.java @@ -0,0 +1,9 @@ +package calculator; + +import java.math.BigInteger; +import java.util.List; + +public interface ValidationStrategy { + + void validate(List number); +} From ee974105412fdb39e0e599cfd38c5a96af3ddc9f Mon Sep 17 00:00:00 2001 From: conycomy Date: Mon, 20 Oct 2025 23:25:44 +0900 Subject: [PATCH 09/13] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=20=EC=B2=98=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/calculator/InputHandler.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/main/java/calculator/InputHandler.java diff --git a/src/main/java/calculator/InputHandler.java b/src/main/java/calculator/InputHandler.java new file mode 100644 index 0000000000..6e0c6a29e6 --- /dev/null +++ b/src/main/java/calculator/InputHandler.java @@ -0,0 +1,19 @@ +package calculator; + +import camp.nextstep.edu.missionutils.Console; + +public class InputHandler { + + private static final String INPUT_MESSAGE = "덧셈할 문자열을 입력해 주세요."; + + + public String getUserInput() { + System.out.println(INPUT_MESSAGE); + return Console.readLine(); + } + + + public void close() { + Console.close(); + } +} \ No newline at end of file From 74117dae459e3a51094091850e22e49664723955 Mon Sep 17 00:00:00 2001 From: conycomy Date: Mon, 20 Oct 2025 23:27:51 +0900 Subject: [PATCH 10/13] =?UTF-8?q?test:=20StringCalculator=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/calculator/StringCalculatorTest.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/test/java/calculator/StringCalculatorTest.java diff --git a/src/test/java/calculator/StringCalculatorTest.java b/src/test/java/calculator/StringCalculatorTest.java new file mode 100644 index 0000000000..e56d405772 --- /dev/null +++ b/src/test/java/calculator/StringCalculatorTest.java @@ -0,0 +1,39 @@ +package calculator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; // 예외 테스트를 위해 추가 필요 + +import java.math.BigInteger; // BigInteger 사용을 위해 추가 +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class StringCalculatorTest { + + private StringCalculator calculator; + + @BeforeEach + void setUp() { + DelimiterParser delimiterParser = new DefaultDelimiterParser(); + NumberParser numberParser = new NumberParser(); + + // ⭐️ 수정: ValidationStrategy 인터페이스 대신 구현체인 CollectAllValidator를 인스턴스화합니다. + ValidationStrategy validationStrategy = new CollectAllValidator(); + + // StringCalculator의 생성자는 이제 ValidationStrategy를 받습니다. + calculator = new StringCalculator(delimiterParser, numberParser, validationStrategy); + } + + @Test + @DisplayName("5. 커스텀 구분자를 사용하여 합계를 계산한다.") + void calculate_custom_delimiter() { + + + assertThat(calculator.calculate("//;\n1;2;3")).isEqualTo(BigInteger.valueOf(6)); + + assertThat(calculator.calculate("//#\n10#20#5")).isEqualTo(BigInteger.valueOf(35)); + + + assertThat(calculator.calculate("//!\n1!2,3")).isEqualTo(BigInteger.valueOf(6)); + } +} \ No newline at end of file From 0af0764e5d081c66f7a959c1607b199986a5b556 Mon Sep 17 00:00:00 2001 From: conycomy Date: Mon, 20 Oct 2025 23:30:18 +0900 Subject: [PATCH 11/13] =?UTF-8?q?feat:=20Calculator=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?= =?UTF-8?q?StringCalculator/NumberParser=20=EA=B0=9D=EC=B2=B4=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/calculator/Calculator.java | 7 +++ src/main/java/calculator/NumberParser.java | 20 ++++++++ .../java/calculator/StringCalculator.java | 50 +++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 src/main/java/calculator/Calculator.java create mode 100644 src/main/java/calculator/NumberParser.java create mode 100644 src/main/java/calculator/StringCalculator.java diff --git a/src/main/java/calculator/Calculator.java b/src/main/java/calculator/Calculator.java new file mode 100644 index 0000000000..0cec60aeba --- /dev/null +++ b/src/main/java/calculator/Calculator.java @@ -0,0 +1,7 @@ +package calculator; +import java.math.BigInteger; + +public interface Calculator { + BigInteger calculate(String input); + +} diff --git a/src/main/java/calculator/NumberParser.java b/src/main/java/calculator/NumberParser.java new file mode 100644 index 0000000000..b76b28d0a2 --- /dev/null +++ b/src/main/java/calculator/NumberParser.java @@ -0,0 +1,20 @@ +package calculator; + +import java.math.BigInteger; + +public class NumberParser { + + public BigInteger parse(String token) { + String trimmedToken = token.trim(); + + if (trimmedToken.isEmpty()) { + return BigInteger.ZERO; + } + + try { + return new BigInteger(trimmedToken); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("숫자 이외의 문자열이 포함되어 변환할 수 없습니다: " + trimmedToken); + } + } +} diff --git a/src/main/java/calculator/StringCalculator.java b/src/main/java/calculator/StringCalculator.java new file mode 100644 index 0000000000..22e48aed07 --- /dev/null +++ b/src/main/java/calculator/StringCalculator.java @@ -0,0 +1,50 @@ +package calculator; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; + +public class StringCalculator implements Calculator { + + private final DelimiterParser delimiterParser; + private final NumberParser numberParser; + private final ValidationStrategy validationStrategy; // ⭐️ ValidationStrategy 필드 추가 + + public StringCalculator(DelimiterParser delimiterParser, NumberParser numberParser, ValidationStrategy validationStrategy) { + this.delimiterParser = delimiterParser; + this.numberParser = numberParser; + this.validationStrategy = validationStrategy; // ⭐️ Strategy 주입 + } + + @Override + public BigInteger calculate(String input) { + if (input == null || input.trim().isEmpty()) { + return BigInteger.ZERO; + } + + DelimiterInfo info = delimiterParser.parse(input); + + String[] parts = info.getNumbersString().split(info.getDelimiterRegex()); + + return sum(parts); + } + + private BigInteger sum(String[] parts) { + + List numbers = new ArrayList<>(); + for (String token : parts) { + numbers.add(numberParser.parse(token)); + } + + + validationStrategy.validate(numbers); + + BigInteger sum = BigInteger.ZERO; + for (BigInteger number : numbers) { + // 검증이 완료된 숫자만 합산합니다. + sum = sum.add(number); + } + + return sum; + } +} \ No newline at end of file From 3d267146beedc89e3a3a2e3dcbbd3a5687066474 Mon Sep 17 00:00:00 2001 From: conycomy Date: Mon, 20 Oct 2025 23:31:15 +0900 Subject: [PATCH 12/13] =?UTF-8?q?refactor:=20Application=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EA=B0=81=20=EB=AA=A8=EB=93=88(=ED=8C=8C=EC=8B=B1/=EA=B3=84?= =?UTF-8?q?=EC=82=B0/=EA=B2=80=EC=A6=9D)=20=EC=97=AD=ED=95=A0=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/calculator/Application.java | 81 ++++------------------- 1 file changed, 12 insertions(+), 69 deletions(-) diff --git a/src/main/java/calculator/Application.java b/src/main/java/calculator/Application.java index 647b318513..905173b38c 100644 --- a/src/main/java/calculator/Application.java +++ b/src/main/java/calculator/Application.java @@ -1,89 +1,32 @@ package calculator; -import java.util.regex.Pattern; -import camp.nextstep.edu.missionutils.Console; +import java.math.BigInteger; public class Application { public static void main(String[] args) { - System.out.println("===덧셈할 문자열을 입력해주세요==="); - String input = Console.readLine(); // Console.readLine() 사용 + InputHandler inputHandler = new InputHandler(); + String input = inputHandler.getUserInput(); try { - if (input == null || input.trim().isEmpty()) { - System.out.println("결과: 0"); - return; - } -/* + DelimiterParser delimiterParser = new DefaultDelimiterParser(); + NumberParser numberParser = new NumberParser(); + ValidationStrategy validationStrategy = new CollectAllValidator(); - String basicDelimiterRegex = "[,:]"; - String finalDelimiterRegex = basicDelimiterRegex; - String numbersToSplit = input; + Calculator calculator = new StringCalculator(delimiterParser, numberParser, validationStrategy); - // 2. 커스텀 구분자 - if (input.startsWith("//")) { - int delimiterEndIndex = input.indexOf("\n"); + BigInteger result = calculator.calculate(input); - if (delimiterEndIndex == -1) { - throw new IllegalArgumentException("커스텀 구분자 선언 후 반드시 줄 바꿈(\\n)을 해야합니다."); - } - String customDelimiter = input.substring(2, delimiterEndIndex); - - finalDelimiterRegex += "|" + Pattern.quote(customDelimiter); - - numbersToSplit = input.substring(delimiterEndIndex + 1); - } - String[] parts = numbersToSplit.split(finalDelimiterRegex); - -*/ - DelimiterParser parser = new DefaultDelimiterParser(); - DelimiterInfo info = parser.parse(input); - - String numbersToSplit = info.getNumbersString(); - String finalDelimiterRegex = info.getDelimiterRegex(); - - String[] parts = numbersToSplit.split(finalDelimiterRegex); - - int sum = 0; - StringBuilder negativeNumbers = new StringBuilder(); - - for (String part : parts) { - String trimmedS = part.trim(); - - if (trimmedS.isEmpty()) { - continue; - } - - if (!trimmedS.matches("^-?\\d+$")) { - throw new IllegalArgumentException("잘못된 입력 형식입니다. 숫자만 입력이 가능합니다: " + trimmedS); - } - - int num = Integer.parseInt(trimmedS); - - if (num < 0) { - if(negativeNumbers.length() > 0) { - negativeNumbers.append(", "); - } - negativeNumbers.append(num); - continue; - } - - sum += num; - } - - if (negativeNumbers.length() > 0) { - throw new IllegalArgumentException("음수는 입력이 불가능합니다: " + negativeNumbers.toString()); - } - - System.out.println("결과: " + sum); + System.out.println("결과 : " + result); } catch (IllegalArgumentException e) { System.out.println("오류: " + e.getMessage()); + throw e; // 예외를 다시 던져서 테스트에서 감지 가능하도록 처리 } catch (Exception e) { System.out.println("예상치 못한 오류가 발생했습니다."); - // e.printStackTrace(); + throw e; } finally { - Console.close(); + inputHandler.close(); } } } \ No newline at end of file From 36bdbf81bcf6ab0bf2b0c3065255fffc508db0c1 Mon Sep 17 00:00:00 2001 From: LimGyeongHan <91595140+conycomy@users.noreply.github.com> Date: Mon, 20 Oct 2025 23:47:26 +0900 Subject: [PATCH 13/13] =?UTF-8?q?docs:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=84=A4=EA=B3=84=20=EA=B5=AC=EC=A1=B0=20=EB=B0=8F?= =?UTF-8?q?=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20=EA=B3=BC=EC=A0=95=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=ED=95=98=EC=97=AC=20README=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 객체지향 설계(ValidationStrategy)를 포함한 최종 책임 및 역할 목록 명시 - BigInteger 도입 등 주요 난관 극복 과정 요약 - 최종 기능 요구 사항 정리 --- README.md | 43 +++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index d70a978705..5c7885f2da 100644 --- a/README.md +++ b/README.md @@ -42,35 +42,29 @@ ### 2단계: 구조화 및 객체지향 리팩토링 -- **DelimiterInfo**: 구분자 정규식과 숫자 문자열을 담는 DTO -- **DelimiterParser / DefaultDelimiterParser**: 입력 문자열 분석 및 `DelimiterInfo` 반환 -- **NumberParser**: 문자열을 정수로 변환하며 예외 처리 -- **Validator**: 음수 값 검증 후 `IllegalArgumentException` 발생 -- **StringCalculator** - - 구성 요소를 주입받아 순차적으로 호출 - - 문자열 분리 → 숫자 변환 → 검증 → 합산 로직 수행 -- **Calculator 인터페이스** - - `calculate(String input)` 메서드로 계산 행위의 규약 정의 +Application / InputHandler: 사용자 I/O 처리 및 모든 객체를 조립하여 계산 흐름을 실행합니다. (조립 및 I/O 분리) ---- +Calculator (I): calculate(String input) 메서드로 계산 행위의 규약을 정의합니다. (최상위 규약) -### 3단계: 입출력 연결 및 실행 환경 구성 +ValidationStrategy (I): 검증 로직의 규약을 정의하며, StringCalculator가 검증 방식을 유연하게 바꿀 수 있게 합니다. (전략 패턴 인터페이스) -- **InputHandler** - - `Console.readLine()`으로 사용자 입력 처리 및 자원 정리 -- **Application (main)** - - 입력 수집 → `StringCalculator` 실행 → 결과 출력 - - `try-catch-finally` 구문을 통해 예외 처리 및 리소스 정리 +StringCalculator: 모든 구성 요소를 주입받아 순차적으로 호출하며 계산 흐름을 총괄하고, 오직 숫자 합산 책임만 가집니다. (지휘자/서비스) ---- +CollectAllValidator: ValidationStrategy를 구현하여, 모든 음수를 수집하고 한 번에 예외를 던지는 복잡한 검증 전략을 수행합니다. (검증 전략 구현체) + +DefaultDelimiterParser: 입력 문자열을 분석하여 **구분자 정보(DelimiterInfo)**와 숫자 문자열을 추출합니다. (\r 문자 정규화 처리 포함) (분석 전문가) +DelimiterInfo (DTO): 구분자 정규식 및 숫자 문자열을 담는 데이터 전달 객체입니다. (데이터 홀더) + +NumberParser: 개별 토큰을 BigInteger로 변환하고 NumberFormatException을 처리합니다. (변환 전문가)의 + +--- ## 예외 상황 - 문자가 포함된 입력값 (예: `"a,2,3"`, `"1b:4"`) - 음수 입력값 (예: `"-1,2,3"`) - 구분자가 연속된 경우 (예: `"1,,2"`) - 소수 입력 (예: `"1.5,2"`) -- 정수 범위 초과 입력 - 빈 문자열 또는 공백 입력 (예: `""`, `" "`) - `null` 또는 입력이 존재하지 않는 경우 - 숫자만 단독 입력된 경우 @@ -120,3 +114,16 @@ AngularJS Commit Message 규칙을 참고했습니다. 이 프로젝트는 문자열 계산기를 절차지향적으로 구현한 뒤, 객체지향 설계 원칙에 따라 구조를 개선하며 **깨끗하고 확장 가능한 코드**로 발전시키는 과정을 담고 있습니다. + +--- +과제 소감 및 회고 + +이번 과제를 진행하며 처음에는 절차지향적으로 구현을 한 뒤, 점차 리팩토링을 통해 설계를 개선하는 과정을 보여주고 싶었습니다. 초기에는 차례대로 기능을 구현하는 데 집중했지만, 리드미에 추가된 새로운 모듈들이 등장하면서 예상치 못한 상황에 당황하기도 했습니다. 이를 통해 구현 과정에서 수정 가능성을 감안한 설계의 중요성을 깨달았습니다. + +특히 객체지향 설계 패턴을 적용하면서, StringCalculator가 모든 책임을 지는 구조에서 CollectAllValidator와 같은 전략 구현체로 책임을 위임하는 전략 패턴에 익숙해지는 데 시간이 필요했습니다. 이를 통해 인터페이스의 본질과 조립의 책임을 체감할 수 있었습니다. + +또한, 우테코 미션에서는 기능 구현만큼이나 요구사항에 명시된 출력 형식이 중요함을 배웠습니다. 최종 실패 원인이 논리적 오류가 아닌 띄어쓰기 불일치였다는 점에서 출력 형식의 중요성을 절실히 느꼈습니다. 이번 과제에서는 System.out.print()로 직접 확인하던 방식 대신 단위 테스트와 통합 테스트를 통해 안정성을 검증하며, 테스트 코드 작성의 중요성과 오류를 찾아가는 과정의 즐거움도 경험할 수 있었습니다. + +환경 독립적인 코드 작성의 중요성도 깨달았습니다. DefaultDelimiterParser에서 \r 문자를 제거하여 운영체제나 테스트 환경의 사소한 차이에도 코드가 흔들리지 않도록 입력을 정규화한 경험이 그 예입니다. + +마지막으로, 과제 수행 과정에서 겪은 고난과 역경은 코드 결과에는 그대로 드러나지 않았지만, 이를 커밋 메시지(feat, refactor, fix 등)로 기록하며 과정의 의미를 남기려 노력했습니다. 이번 경험을 통해 단순 구현을 넘어 설계, 테스트, 환경 고려, 기록까지 완성하는 과정을 배우게 되었으며, 다음 과제에서는 한층 더 발전된 나를 기대합니다.