From e8ef1d96ca00f9d613288c0d2277952b4ddea741 Mon Sep 17 00:00:00 2001 From: k-mha <130807104+k-mha@users.noreply.github.com> Date: Mon, 20 Oct 2025 22:04:14 +0900 Subject: [PATCH 01/10] =?UTF-8?q?test:=20=EB=AC=B8=EC=9E=90=EC=97=B4=20?= =?UTF-8?q?=EB=8D=A7=EC=85=88=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/calculator/ApplicationTest.java | 114 +++++++++++++++++- 1 file changed, 109 insertions(+), 5 deletions(-) diff --git a/src/test/java/calculator/ApplicationTest.java b/src/test/java/calculator/ApplicationTest.java index 93771fb011..040dca5d30 100644 --- a/src/test/java/calculator/ApplicationTest.java +++ b/src/test/java/calculator/ApplicationTest.java @@ -1,12 +1,12 @@ package calculator; -import camp.nextstep.edu.missionutils.test.NsTest; -import org.junit.jupiter.api.Test; - import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import camp.nextstep.edu.missionutils.test.NsTest; +import org.junit.jupiter.api.Test; + class ApplicationTest extends NsTest { @Test void 커스텀_구분자_사용() { @@ -16,11 +16,115 @@ class ApplicationTest extends NsTest { }); } + @Test + void 빈_문자열() { + assertSimpleTest(() -> { + run(""); + assertThat(output()).contains("결과 : 0"); + }); + } + + @Test + void 숫자_한개() { + assertSimpleTest(() -> { + run("1"); + assertThat(output()).contains("결과 : 1"); + }); + } + + @Test + void 쉼표_기본구분자() { + assertSimpleTest(() -> { + run("1,2"); + assertThat(output()).contains("결과 : 3"); + }); + } + + @Test + void 쉼표_콜론_혼합() { + assertSimpleTest(() -> { + run("1,2:3"); + assertThat(output()).contains("결과 : 6"); + }); + } + + @Test + void 커스텀_구분자_앞뒤_허용() { + assertSimpleTest(() -> { + run("//;\n;1;2;"); + assertThat(output()).contains("결과 : 3"); + }); + } + @Test void 예외_테스트() { assertSimpleTest(() -> - assertThatThrownBy(() -> runException("-1,2,3")) - .isInstanceOf(IllegalArgumentException.class) + assertThatThrownBy(() -> runException("-1,2,3")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + void 연속_쉼표() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("1,,2")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + void 연속_콜론() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("1::2")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + void 공백_포함() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("1, 2")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + void 숫자_아닌_문자() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("1,a,3")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + void 커스텀_구분자_없음() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("//\n1,2")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + void 커스텀_구분자_2글자() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("//;;\n1;2")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + void 커스텀_구분자_위치_오류() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("1;2//;\n3")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + void 숫자_없음() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException(",:")) + .isInstanceOf(IllegalArgumentException.class) ); } From 9b3f93b365e415f6d1389c4dff83fd04f4a32b31 Mon Sep 17 00:00:00 2001 From: k-mha <130807104+k-mha@users.noreply.github.com> Date: Mon, 20 Oct 2025 23:12:27 +0900 Subject: [PATCH 02/10] =?UTF-8?q?feat(NumberExtractor):=20=EB=AC=B8?= =?UTF-8?q?=EC=9E=90=EC=97=B4=20=EC=88=AB=EC=9E=90=20=EC=B6=94=EC=B6=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=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 입력 문자열을 한 글자씩 순회하며 숫자를 추출하고 공백·연속 구분자·잘못된 문자를 감지하도록 구현했습니다. 기본 구분자(, :) 기반으로 정상 동작을 검증하는 단위 테스트를 작성해 NumberExtractor의 계약을 고정했습니다. 테스트 시나리오: - 기본 구분자(, :)로 분리 시 [1,2,3] 추출 - 연속 구분자 발생 시 예외 (예: "1,,2") - 공백 포함 입력 시 예외 (예: "1, 2") - 시작/끝 구분자 허용 (예: ",1,2," → [1,2]) --- src/main/java/calculator/NumberExtractor.java | 56 +++++++++++++++++++ .../java/calculator/StringCalculator.java | 4 ++ .../java/calculator/NumberExtractorTest.java | 48 ++++++++++++++++ 3 files changed, 108 insertions(+) create mode 100644 src/main/java/calculator/NumberExtractor.java create mode 100644 src/main/java/calculator/StringCalculator.java create mode 100644 src/test/java/calculator/NumberExtractorTest.java diff --git a/src/main/java/calculator/NumberExtractor.java b/src/main/java/calculator/NumberExtractor.java new file mode 100644 index 0000000000..1eaa433f0c --- /dev/null +++ b/src/main/java/calculator/NumberExtractor.java @@ -0,0 +1,56 @@ +package calculator; + +import java.util.ArrayList; +import java.util.List; + +public class NumberExtractor { + public List splitToNumbers(String input, List delimiters) { + List numbers = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + boolean previousIsDelimiter = false; + + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + + if (Character.isWhitespace(c)) { + throw new IllegalArgumentException("공백은 허용되지 않습니다."); + } + + if (isDelimiter(c, delimiters)) { + if (previousIsDelimiter) { + throw new IllegalArgumentException("구분자가 연속으로 사용되었습니다."); + } + addCurrentNumber(numbers, current); + current.setLength(0); + previousIsDelimiter = true; + continue; + } + + if (Character.isDigit(c)) { + current.append(c); + previousIsDelimiter = false; + continue; + } + + throw new IllegalArgumentException("유효하지 않은 문자가 포함되었습니다: " + c); + } + + addCurrentNumber(numbers, current); + return numbers; + } + + private boolean isDelimiter(char c, List delimiters) { + for (String d : delimiters) { + if (d.charAt(0) == c) { + return true; + } + } + return false; + } + + private void addCurrentNumber(List numbers, StringBuilder current) { + if (current.length() > 0) { + numbers.add(Integer.parseInt(current.toString())); + } + } +} diff --git a/src/main/java/calculator/StringCalculator.java b/src/main/java/calculator/StringCalculator.java new file mode 100644 index 0000000000..2f0adf3709 --- /dev/null +++ b/src/main/java/calculator/StringCalculator.java @@ -0,0 +1,4 @@ +package calculator; + +public class StringCalculator { +} diff --git a/src/test/java/calculator/NumberExtractorTest.java b/src/test/java/calculator/NumberExtractorTest.java new file mode 100644 index 0000000000..e75f209bc7 --- /dev/null +++ b/src/test/java/calculator/NumberExtractorTest.java @@ -0,0 +1,48 @@ +package calculator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class NumberExtractorTest { + + @Test + @DisplayName("쉼표와 콜론으로 구분된 숫자를 추출한다") + void splitByDefaultDelimiters() { + NumberExtractor extractor = new NumberExtractor(); + List result = extractor.splitToNumbers("1,2:3", Arrays.asList(",", ":")); + + assertThat(result).containsExactly(1, 2, 3); + } + + @Test + @DisplayName("연속 구분자는 예외를 발생시킨다") + void consecutiveDelimitersThrowsException() { + NumberExtractor extractor = new NumberExtractor(); + assertThatThrownBy(() -> + extractor.splitToNumbers("1,,2", Arrays.asList(",", ":")) + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("공백이 포함된 입력은 예외를 발생시킨다") + void spaceThrowsException() { + NumberExtractor extractor = new NumberExtractor(); + assertThatThrownBy(() -> + extractor.splitToNumbers("1, 2", Arrays.asList(",", ":")) + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("시작과 끝 구분자는 허용된다") + void startAndEndDelimiterAllowed() { + NumberExtractor extractor = new NumberExtractor(); + List result = extractor.splitToNumbers(",1,2,", Arrays.asList(",", ":")); + + assertThat(result).containsExactly(1, 2); + } +} \ No newline at end of file From 35e1dac17542bf4b77e9114331b15501b4838eb9 Mon Sep 17 00:00:00 2001 From: k-mha <130807104+k-mha@users.noreply.github.com> Date: Mon, 20 Oct 2025 23:13:10 +0900 Subject: [PATCH 03/10] =?UTF-8?q?feat:=20=EB=AC=B8=EC=9E=90=EC=97=B4=20?= =?UTF-8?q?=EA=B3=84=EC=82=B0=EA=B8=B0=20=EA=B5=AC=EC=A1=B0=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=20=EB=B0=8F=20NumberExtractor=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit StringCalculator가 NumberExtractor를 활용해 입력 문자열을 파싱하고 합산하도록 통합했습니다. 기본 구분자(, :) 기반 입력을 처리하며, Application에서 Console 입력을 받아 결과를 출력하는 최소 실행 구조를 완성했습니다. 의도: - 로직과 입출력 분리를 유지하며 책임 명확화 - 추후 커스텀 구분자 및 예외 검증 로직 확장을 위한 구조 확립 --- src/main/java/calculator/Application.java | 7 +++++++ .../java/calculator/StringCalculator.java | 19 ++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/main/java/calculator/Application.java b/src/main/java/calculator/Application.java index 573580fb40..9f6eeca20b 100644 --- a/src/main/java/calculator/Application.java +++ b/src/main/java/calculator/Application.java @@ -1,7 +1,14 @@ package calculator; +import camp.nextstep.edu.missionutils.Console; + public class Application { public static void main(String[] args) { // TODO: 프로그램 구현 + String input = Console.readLine(); + StringCalculator calculator = new StringCalculator(); + + int result = calculator.add(input); + System.out.println("결과 : " + result); } } diff --git a/src/main/java/calculator/StringCalculator.java b/src/main/java/calculator/StringCalculator.java index 2f0adf3709..a7c516413e 100644 --- a/src/main/java/calculator/StringCalculator.java +++ b/src/main/java/calculator/StringCalculator.java @@ -1,4 +1,21 @@ package calculator; +import java.util.Arrays; +import java.util.List; + public class StringCalculator { -} + private static final List DEFAULT_DELIMITERS = Arrays.asList(",", ":"); + + public int add(String input) { + if (input == null || input.isEmpty()) { + return 0; + } + + NumberExtractor extractor = new NumberExtractor(); + List numbers = extractor.splitToNumbers(input, DEFAULT_DELIMITERS); + + return numbers.stream() + .mapToInt(Integer::intValue) + .sum(); + } +} \ No newline at end of file From 3b91a41e7ffa17d3535e7d9595cf61e02af1aea5 Mon Sep 17 00:00:00 2001 From: k-mha <130807104+k-mha@users.noreply.github.com> Date: Mon, 20 Oct 2025 23:28:08 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feat(DelimiterParser):=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80=20=EA=B5=AC=EB=B6=84=EC=9E=90=20=ED=8C=8C?= =?UTF-8?q?=EC=8B=B1=20=EA=B8=B0=EB=8A=A5=20=EB=B0=8F=20=EB=8B=A8=EC=9C=84?= =?UTF-8?q?=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 입력 문자열에서 //과 개행 문자 사이의 한 글자를 커스텀 구분자로 인식해 기본 구분자 목록에 추가했습니다. - 기본 구분자: , : - 커스텀 구분자: 한 글자만 허용 - 예외 처리: 커스텀 구분자가 2글자 이상 또는 개행 누락 시 IllegalArgumentException 발생 --- src/main/java/calculator/DelimiterParser.java | 36 ++++++++++++++ .../java/calculator/DelimiterParserTest.java | 48 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 src/main/java/calculator/DelimiterParser.java create mode 100644 src/test/java/calculator/DelimiterParserTest.java diff --git a/src/main/java/calculator/DelimiterParser.java b/src/main/java/calculator/DelimiterParser.java new file mode 100644 index 0000000000..2b099ff385 --- /dev/null +++ b/src/main/java/calculator/DelimiterParser.java @@ -0,0 +1,36 @@ +package calculator; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class DelimiterParser { + private static final List DEFAULT_DELIMITERS = List.of(",", ":"); + + public Map.Entry, String> parse(String input) { + if (input == null || input.isEmpty()) { + return Map.entry(DEFAULT_DELIMITERS, ""); + } + + if (input.startsWith("//")) { + int endIndex = input.indexOf("\n"); + if (endIndex == -1) { + throw new IllegalArgumentException("잘못된 커스텀 구분자 형식입니다."); + } + + String customDelimiter = input.substring(2, endIndex); + + if (customDelimiter.length() != 1) { + throw new IllegalArgumentException("커스텀 구분자는 한 글자여야 합니다."); + } + + List delimiters = new ArrayList<>(DEFAULT_DELIMITERS); + delimiters.add(customDelimiter); + + String numbers = input.substring(endIndex + 1); + return Map.entry(delimiters, numbers); + } + + return Map.entry(DEFAULT_DELIMITERS, input); + } +} diff --git a/src/test/java/calculator/DelimiterParserTest.java b/src/test/java/calculator/DelimiterParserTest.java new file mode 100644 index 0000000000..16905b93b2 --- /dev/null +++ b/src/test/java/calculator/DelimiterParserTest.java @@ -0,0 +1,48 @@ +package calculator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class DelimiterParserTest { + + @Test + @DisplayName("커스텀 구분자가 없을 경우 기본 구분자(, :)만 반환한다") + void defaultDelimitersOnly() { + DelimiterParser parser = new DelimiterParser(); + Map.Entry, String> result = parser.parse("1,2:3"); + + assertThat(result.getKey()).containsExactly(",", ":"); + assertThat(result.getValue()).isEqualTo("1,2:3"); + } + + @Test + @DisplayName("커스텀 구분자를 포함하면 해당 구분자를 추가로 반환한다") + void customDelimiterAdded() { + DelimiterParser parser = new DelimiterParser(); + Map.Entry, String> result = parser.parse("//;\n1;2;3"); + + assertThat(result.getKey()).containsExactly(",", ":", ";"); + assertThat(result.getValue()).isEqualTo("1;2;3"); + } + + @Test + @DisplayName("커스텀 구분자가 2글자 이상이면 예외 발생") + void invalidCustomDelimiterLength() { + DelimiterParser parser = new DelimiterParser(); + assertThatThrownBy(() -> parser.parse("//;;\n1;2")) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("개행 문자 없이 //로만 시작하면 예외 발생") + void invalidCustomDelimiterFormat() { + DelimiterParser parser = new DelimiterParser(); + assertThatThrownBy(() -> parser.parse("//;1;2")) + .isInstanceOf(IllegalArgumentException.class); + } +} From 92dd56115e8dd69a0c803e8e19a910e75229e4c4 Mon Sep 17 00:00:00 2001 From: k-mha <130807104+k-mha@users.noreply.github.com> Date: Tue, 21 Oct 2025 02:31:10 +0900 Subject: [PATCH 05/10] =?UTF-8?q?feat(InputValidator):=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=EA=B0=92=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=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 입력 문자열의 규칙을 검사하도록 InputValidator를 추가했습니다. - 입력은 숫자로 시작하고 숫자로 끝나야 함 - 공백, 음수, 연속 구분자 포함 시 예외 발생 - 구분자 목록을 전달받아 형식 검증 수행 --- src/main/java/calculator/InputValidator.java | 45 ++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/main/java/calculator/InputValidator.java diff --git a/src/main/java/calculator/InputValidator.java b/src/main/java/calculator/InputValidator.java new file mode 100644 index 0000000000..b355747813 --- /dev/null +++ b/src/main/java/calculator/InputValidator.java @@ -0,0 +1,45 @@ +package calculator; + +import java.util.List; + +public class InputValidator { + + public static void validate(String input, List delimiters) { + // 입력이 null이면 예외 (실행 자체가 잘못된 호출) + if (input == null) { + throw new IllegalArgumentException("입력이 null일 수 없습니다."); + } + + // 빈 문자열은 계산기에서 0 반환 + if (input.isEmpty()) { + return; + } + + if (input.contains(" ")) { + throw new IllegalArgumentException("공백은 허용되지 않습니다."); + } + + if (input.matches(".*-\\d+.*")) { + throw new IllegalArgumentException("음수는 허용되지 않습니다."); + } + + if (input.matches("^[^0-9]*$")) { + throw new IllegalArgumentException("숫자가 하나 이상 포함되어야 합니다."); + } + + char first = input.charAt(0); + char last = input.charAt(input.length() - 1); + if (!Character.isDigit(first)) { + throw new IllegalArgumentException("입력은 숫자로 시작해야 합니다."); + } + if (!Character.isDigit(last)) { + throw new IllegalArgumentException("입력은 숫자로 끝나야 합니다."); + } + + for (String d : delimiters) { + if (input.contains(d + d)) { + throw new IllegalArgumentException("구분자가 연속으로 사용되었습니다."); + } + } + } +} From 35e085eeb52411c5e2d44751a9035abe2a76576f Mon Sep 17 00:00:00 2001 From: k-mha <130807104+k-mha@users.noreply.github.com> Date: Tue, 21 Oct 2025 02:31:27 +0900 Subject: [PATCH 06/10] =?UTF-8?q?feat(Application):=20=EC=BD=98=EC=86=94?= =?UTF-8?q?=20=EC=9E=85=EC=B6=9C=EB=A0=A5=20=ED=86=B5=ED=95=A9=20=EB=B0=8F?= =?UTF-8?q?=20=EC=8B=A4=ED=96=89=20=EA=B5=AC=EC=A1=B0=20=EC=99=84=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Console API를 사용해 문자열을 입력받고 결과를 출력하는 실행 흐름을 구현했습니다. StringCalculator를 호출해 입력값을 계산하고, 예외 발생 시 프로그램이 종료되지 않도록 설계했습니다. --- src/main/java/calculator/Application.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/java/calculator/Application.java b/src/main/java/calculator/Application.java index 9f6eeca20b..aeb434246c 100644 --- a/src/main/java/calculator/Application.java +++ b/src/main/java/calculator/Application.java @@ -5,7 +5,20 @@ public class Application { public static void main(String[] args) { // TODO: 프로그램 구현 - String input = Console.readLine(); + String input; + + try { + input = Console.readLine(); + } catch (Exception e) { + System.out.println("결과 : 0"); + return; + } + + if (input == null || input.isEmpty()) { + System.out.println("결과 : 0"); + return; + } + StringCalculator calculator = new StringCalculator(); int result = calculator.add(input); From 1d0f47716a39f4d53e125c96c1af3ee449ce3bab Mon Sep 17 00:00:00 2001 From: k-mha <130807104+k-mha@users.noreply.github.com> Date: Tue, 21 Oct 2025 02:31:43 +0900 Subject: [PATCH 07/10] =?UTF-8?q?test:=20=EC=A0=84=EC=B2=B4=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=8F=20=EC=98=88=EC=99=B8=20=EA=B7=9C=EC=B9=99=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 통합 테스트(ApplicationTest) 및 단위 테스트(DelimiterParserTest, NumberExtractorTest)를 정리했습니다. - 시작/끝 구분자 예외 검증 추가 - 커스텀 구분자 및 연속 구분자 테스트 포함 - 전체 23개 테스트 케이스 통과 확인 --- src/test/java/calculator/ApplicationTest.java | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/test/java/calculator/ApplicationTest.java b/src/test/java/calculator/ApplicationTest.java index 040dca5d30..0feb41a608 100644 --- a/src/test/java/calculator/ApplicationTest.java +++ b/src/test/java/calculator/ApplicationTest.java @@ -48,14 +48,6 @@ class ApplicationTest extends NsTest { }); } - @Test - void 커스텀_구분자_앞뒤_허용() { - assertSimpleTest(() -> { - run("//;\n;1;2;"); - assertThat(output()).contains("결과 : 3"); - }); - } - @Test void 예외_테스트() { assertSimpleTest(() -> @@ -120,6 +112,29 @@ class ApplicationTest extends NsTest { ); } + @Test + void 구분자_시작_또는_끝_위치_오류() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException(",1,2")) + .isInstanceOf(IllegalArgumentException.class) + ); + + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("1,2,")) + .isInstanceOf(IllegalArgumentException.class) + ); + + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("//;\n;1;2")) + .isInstanceOf(IllegalArgumentException.class) + ); + + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("//;\n1;2;")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + @Test void 숫자_없음() { assertSimpleTest(() -> @@ -128,6 +143,7 @@ class ApplicationTest extends NsTest { ); } + @Override public void runMain() { Application.main(new String[]{}); From 388a86f2139acebb9f51aecf3d21a86e367358b2 Mon Sep 17 00:00:00 2001 From: k-mha <130807104+k-mha@users.noreply.github.com> Date: Tue, 21 Oct 2025 02:37:19 +0900 Subject: [PATCH 08/10] =?UTF-8?q?fix(DelimiterParser):=20=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=EC=BC=80=EC=9D=B4=ED=94=84=20=EA=B0=9C=ED=96=89(\n)?= =?UTF-8?q?=EC=9D=84=20=EC=8B=A4=EC=A0=9C=20=EA=B0=9C=ED=96=89=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 테스트 입력값이 //;\n 형태로 들어오는 경우 실제 개행 문자로 변환하여 커스텀 구분자 헤더가 정상 파싱되도록 수정. --- src/main/java/calculator/DelimiterParser.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/calculator/DelimiterParser.java b/src/main/java/calculator/DelimiterParser.java index 2b099ff385..bf4ff466c8 100644 --- a/src/main/java/calculator/DelimiterParser.java +++ b/src/main/java/calculator/DelimiterParser.java @@ -12,6 +12,8 @@ public Map.Entry, String> parse(String input) { return Map.entry(DEFAULT_DELIMITERS, ""); } + input = input.replace("\\n", "\n"); + if (input.startsWith("//")) { int endIndex = input.indexOf("\n"); if (endIndex == -1) { From 86020d95ff60a663af968129b86834802a13ded5 Mon Sep 17 00:00:00 2001 From: k-mha <130807104+k-mha@users.noreply.github.com> Date: Tue, 21 Oct 2025 02:37:31 +0900 Subject: [PATCH 09/10] =?UTF-8?q?refactor:=20=EA=B5=AC=EB=B6=84=EC=9E=90?= =?UTF-8?q?=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=A0=84=EB=8B=AC=20=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EB=82=B4=EB=B6=80=20=ED=9D=90?= =?UTF-8?q?=EB=A6=84=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DelimiterParser에서 얻은 구분자 목록을 그대로 전달하도록 메서드 시그니처와 호출부를 정리. - StringCalculator: numbersPart, delimiters 배선 - NumberExtractor: splitToNumbers(numbersPart, delimiters) 시그니처 확정 동작 변경 없이 코드 의도를 명확히 함. --- src/main/java/calculator/NumberExtractor.java | 7 +++++-- src/main/java/calculator/StringCalculator.java | 17 +++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/main/java/calculator/NumberExtractor.java b/src/main/java/calculator/NumberExtractor.java index 1eaa433f0c..734f656bf0 100644 --- a/src/main/java/calculator/NumberExtractor.java +++ b/src/main/java/calculator/NumberExtractor.java @@ -9,6 +9,8 @@ public List splitToNumbers(String input, List delimiters) { StringBuilder current = new StringBuilder(); boolean previousIsDelimiter = false; + input = input.replace("\\n", "\n"); + for (int i = 0; i < input.length(); i++) { char c = input.charAt(i); @@ -17,9 +19,10 @@ public List splitToNumbers(String input, List delimiters) { } if (isDelimiter(c, delimiters)) { - if (previousIsDelimiter) { + if (previousIsDelimiter && i != 0) { throw new IllegalArgumentException("구분자가 연속으로 사용되었습니다."); } + addCurrentNumber(numbers, current); current.setLength(0); previousIsDelimiter = true; @@ -49,7 +52,7 @@ private boolean isDelimiter(char c, List delimiters) { } private void addCurrentNumber(List numbers, StringBuilder current) { - if (current.length() > 0) { + if (!current.isEmpty()) { numbers.add(Integer.parseInt(current.toString())); } } diff --git a/src/main/java/calculator/StringCalculator.java b/src/main/java/calculator/StringCalculator.java index a7c516413e..8ecc461f55 100644 --- a/src/main/java/calculator/StringCalculator.java +++ b/src/main/java/calculator/StringCalculator.java @@ -2,20 +2,21 @@ import java.util.Arrays; import java.util.List; +import java.util.Map; public class StringCalculator { private static final List DEFAULT_DELIMITERS = Arrays.asList(",", ":"); public int add(String input) { - if (input == null || input.isEmpty()) { - return 0; - } + DelimiterParser parser = new DelimiterParser(); + Map.Entry, String> parsed = parser.parse(input); - NumberExtractor extractor = new NumberExtractor(); - List numbers = extractor.splitToNumbers(input, DEFAULT_DELIMITERS); + List delimiters = parsed.getKey(); + String numbersPart = parsed.getValue(); - return numbers.stream() - .mapToInt(Integer::intValue) - .sum(); + InputValidator.validate(numbersPart, delimiters); + + List numbers = new NumberExtractor().splitToNumbers(numbersPart, delimiters); + return numbers.stream().mapToInt(Integer::intValue).sum(); } } \ No newline at end of file From b8aac12bee633b9f04ab38b7385555d663a97398 Mon Sep 17 00:00:00 2001 From: kmha <130807104+kmha27@users.noreply.github.com> Date: Tue, 21 Oct 2025 03:08:12 +0900 Subject: [PATCH 10/10] =?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=8A=A5=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20README=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=ED=99=94=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 문자열 덧셈 계산기 프로그램의 전체 기능을 구현하고 설계 의도를 README.md에 정리했습니다. - 기본 및 커스텀 구분자 파싱 기능 구현 - 입력값 검증 로직 분리(InputValidator) - 숫자 추출 및 합산(NumberExtractor, StringCalculator) - 예외 처리 기준: 공백, 음수, 연속 구분자, 시작·끝 구분자 - TDD 기반 단위 테스트 및 통합 테스트 총 23개 케이스 통과 - README에 개발 과정, 설계 방향, 커밋 의도, 회고 포함 --- README.md | 129 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 128 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bd90ef0247..940eec1e12 100644 --- a/README.md +++ b/README.md @@ -1 +1,128 @@ -# java-calculator-precourse \ No newline at end of file +# java-calculator-precourse + +# 문자열 덧셈 계산기 + +## 과제 개요 +입력된 문자열에서 숫자를 추출하고, 지정된 구분자를 기준으로 분리해 합산하는 프로그램이다. +기본 구분자는 `,`(쉼표)와 `:`(콜론)이며, 사용자가 `//`와 `\n` 사이에 커스텀 구분자를 지정할 수도 있다. +잘못된 입력이 들어올 경우 `IllegalArgumentException`을 발생시킨다. + +--- + +## 실행 예시 +``` +1,2:3 +결과 : 6 + +//;\n1;2;3 +결과 : 6 +``` +--- + +## 개발 과정 및 사고의 흐름 + +### 1. 요구사항 분석 +문제의 핵심은 단순한 문자열 덧셈이 아니라 +**“문자열을 숫자로 해석하는 과정의 유효성”**을 명확히 정의하는 것이라고 판단했다. + +이에 따라 다음과 같은 질문으로 문제를 해석했다. + +- 문자열의 어디까지가 유효한 숫자일까? +- 구분자가 문장 앞뒤에 있어도 괜찮을까? +- 잘못된 입력은 어느 시점에서 검증해야 할까? + +이 과정을 통해 ‘구분자는 반드시 숫자 사이에 존재해야 한다’는 규칙을 직접 정의했다. +이는 명세에는 없었지만, 계산기의 일관된 동작을 위해 필요한 제약이라고 판단했다. + +--- + +### 2. 설계 방향 +기능을 단일 흐름으로 구현하지 않고, +책임을 역할 단위로 분리하여 유지보수성과 테스트 용이성을 확보했다. + +| 클래스 | 역할 | 설계 근거 | +|--------|------|------------| +| `Application` | 콘솔 입출력 관리 | 입력과 출력만 담당하도록 최소 책임 부여 | +| `StringCalculator` | 전체 로직 제어 | Parser → Validator → Extractor 순으로 흐름 고정 | +| `DelimiterParser` | 구분자 파싱 | 커스텀 구분자 인식 및 기본 구분자 병합 | +| `InputValidator` | 입력값 검증 | 음수, 공백, 연속 구분자, 시작/끝 구분자 검증 | +| `NumberExtractor` | 숫자 추출 및 변환 | 문자열을 숫자 리스트로 변환, 잘못된 문자 검출 | + +단일 책임 원칙(SRP)을 따르며, 각 클래스는 변경 이유가 하나만 있도록 설계했다. + +--- + +### 3. TDD 접근 방식 +기능을 한 번에 구현하지 않고, +테스트 케이스 작성 → 최소한의 구현 → 리팩토링 순으로 진행했다. + +#### 주요 테스트 시나리오 +- 빈 문자열 → 0 +- 기본 구분자(`,`, `:`)로 분리 → 합계 계산 +- 커스텀 구분자(`//;\n1;2;3`) → 합계 계산 +- 연속 구분자 → 예외 발생 +- 공백 포함 → 예외 발생 +- 시작 또는 끝이 구분자인 경우 → 예외 발생 +- 잘못된 커스텀 구분자 정의 → 예외 발생 + +예외 케이스를 먼저 정의함으로써 +“유효한 입력의 범위”를 스스로 정립하고 테스트를 통해 보장했다. + +--- + +### 4. 설계 결정 근거 + +| 항목 | 고민의 흐름 | 최종 결정 | +|------|-------------|-----------| +| 커스텀 구분자 범위 | 여러 문자 허용 시 파싱 모호성 증가 | 한 글자만 허용 | +| 구분자 위치 | 구분자는 숫자 사이에 존재해야 의미 있음 | 문자열 시작·끝 위치 불허 | +| 음수 입력 | 일반 계산기·금융 도메인 모두에서 음수는 예외로 처리 | 예외 발생 | +| 공백 처리 | 입력 형식의 명확성 유지 | 공백 포함 시 예외 발생 | +| 빈 문자열 | 입력이 없으면 0 반환이 직관적 | 0 반환 | + +--- + +### 5. 커밋 단위 기준 +커밋은 단순 변경이 아닌 “의도 단위”로 구분했다. +각 커밋 메시지만으로 어떤 문제를 해결하려 했는지 파악할 수 있도록 작성했다. + +| 커밋 메시지 | 의도 | +|--------------|------| +| `feat(DelimiterParser): 커스텀 구분자 파싱 기능 및 단위 테스트 추가` | 입력 파싱 로직 분리 | +| `feat(NumberExtractor): 문자열 숫자 추출 기능 구현 및 단위 테스트 추가` | 숫자 추출 및 예외 검증 | +| `feat(InputValidator): 입력값 검증 로직 추가` | 입력 규칙 검증 분리 | +| `refactor: 구분자 리스트 전달 방식으로 내부 흐름 정리` | 호출 구조 개선 | +| `test: 전체 테스트 코드 정리 및 예외 규칙 검증 추가` | 전체 시나리오 점검 | + +--- + +### 6. 리팩토링 포인트 +- 입력 검증 로직을 별도의 클래스(`InputValidator`)로 분리해 테스트 독립성 확보 +- `StringCalculator` 내부 흐름을 `Parser → Validator → Extractor` 순서로 고정 +- 단위 테스트(`DelimiterParserTest`, `NumberExtractorTest`)와 통합 테스트(`ApplicationTest`)를 분리하여 책임 구분 + +--- + +### 7. 테스트 구조 요약 + +| 테스트 파일 | 역할 | +|--------------|------| +| `ApplicationTest` | 전체 실행 흐름 검증 | +| `DelimiterParserTest` | 커스텀 구분자 파싱 테스트 | +| `NumberExtractorTest` | 숫자 추출 및 예외 상황 테스트 | + + +--- + +### 8. 회고 +이 과제를 통해 “구현보다 설계가 먼저”라는 점을 체감했다. +기능을 분리하고 예외를 정의하는 과정에서 단순히 동작하는 코드를 넘어서, +**왜 이렇게 만들어야 하는가**를 스스로 설명할 수 있게 되었다. + +--- + +## 실행 방법 +``` +./gradlew clean test # 전체 테스트 실행 +./gradlew run # 프로그램 실행 +```