-
Notifications
You must be signed in to change notification settings - Fork 0
[문자열 덧셈 계산기] 이예진 미션 제출합니다. #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
b590b43
9d0f175
6c8f668
3f35ee1
9d76295
f81cb22
cb14324
d0c3c39
c41ce6e
11351d6
a354e87
b38a7e1
6599966
f31199b
13bab35
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,140 @@ | ||
| # java-calculator-precourse | ||
| # java-calculator-precourse | ||
|
|
||
| ## 문자열 덧셈 계산기 | ||
|
|
||
| 입력한 문자열에서 **숫자를 추출하여 합산하는 계산기**를 구현한다. | ||
| 기본 구분자는 쉼표(`,`)와 콜론(`:`)이며, | ||
| 사용자는 `"//[구분자]\\n"` 형식으로 **커스텀 구분자**를 지정할 수 있다. | ||
| 잘못된 입력에 대해서는 `IllegalArgumentException`을 발생시키고 프로그램을 종료한다. | ||
|
|
||
| --- | ||
|
|
||
| ## 1. 기능 구현 목록 | ||
|
|
||
| ### 1. 입력 | ||
| - [x] 콘솔에서 문자열을 입력받는다 | ||
| - `camp.nextstep.edu.missionutils.Console.readLine()`을 사용 | ||
| - 예: `"1,2:3"` | ||
|
|
||
|
|
||
| ### 2. 입력 검증 | ||
| - [x] null, 빈 문자열, 커스텀 구분자 헤더 형식을 검증한다 → `IllegalArgumentException` | ||
| - [x] null 입력 → 예외 발생 | ||
| - [x] 빈 문자열(`""`) 입력 → 정상 처리 | ||
| - [x] 커스텀 구분자 형식 오류 (예: `"//;\n"` 누락) → 예외 발생 | ||
| - [ ] 비숫자/음수 검증은 문자열 파싱 단계에서 수행 | ||
|
|
||
|
|
||
| ### 3. 구분자 파싱 | ||
| - [x] 입력값의 맨 앞에 `"//"`가 있고, 그 뒤에 `"\n"`이 있으면 커스텀 구분자로 인식한다 | ||
| - `"//;\n1;2;3"` → 구분자: `;` → 결과: `6` | ||
| - [x] 커스텀 구분자는 **여러 문자를 허용**한다 | ||
| - `"//***\n1***2***3"` → 결과: `6` | ||
| - [x] 커스텀 구분자에 공백 포함 시 → 예외 발생 `IllegalArgumentException` | ||
| - [x] 예약어(`//`, `\n`, `,`, `:`) 사용 불가 → 예외 발생 `IllegalArgumentException` | ||
| - [x] 커스텀 구분자 식별에 실패할 경우 → `IllegalArgumentException` | ||
|
|
||
|
|
||
| ### 4. 문자열 파싱 | ||
| - [x] 구분자(기본 또는 커스텀)를 기준으로 문자열을 분리한다 | ||
| - [x] 분리된 각 요소가 숫자로만 이루어져 있는지 검사한다 | ||
| - `"a"`, `"--"`, `"-2"` 등 → `IllegalArgumentException` | ||
| - [x] 숫자로 변환 불가 시 → `IllegalArgumentException` | ||
| - [x] 정상 입력이면 문자열 배열을 반환한다 | ||
| - 입력: `"1,2:3"` → 출력: `["1", "2", "3"]` | ||
|
|
||
| ### 5. 계산 | ||
| - [x] 문자열 배열을 정수 배열로 변환한다 | ||
| - [x] 모든 정수를 합산하여 결과를 반환한다 | ||
| - [x] 음수 또는 비정상 입력 시 → `IllegalArgumentException` | ||
| - 입력: `["1", "-2", "3"]` → 예외 발생 | ||
| - 입력: `["1", "a", "3"]` → 예외 발생 | ||
|
|
||
|
|
||
| ### 6. 출력 | ||
| - [x] 최종 합산 결과를 `"결과 : X"` 형식으로 출력한다 | ||
| - 예: `"결과 : 6"` | ||
|
|
||
| --- | ||
|
|
||
| ## 2. 예외 처리 정책 | ||
|
|
||
| ### 1. 입력 단계 | ||
| | 조건 | 처리 방식 | 메시지 | | ||
| |------|----------------------------|-------------| | ||
| | null 입력 | `IllegalArgumentException` | `"입력값이 존재하지 않습니다."` | | ||
| | 빈 문자열 | 정상 처리 | - | | ||
| | 커스텀 구분자 형식 오류 | `IllegalArgumentException` | `"커스텀 구분자 형식이 올바르지 않습니다."` | | ||
|
|
||
|
|
||
| ### 2. 검증 단계 | ||
| | 조건 | 처리 방식 | 메시지 | | ||
| |------|-------------|-------------| | ||
| | 음수 입력 | `IllegalArgumentException` | `"음수는 입력할 수 없습니다."` | | ||
| | 비숫자 포함 | `IllegalArgumentException` | `"숫자와 구분자만 입력해주세요."` | | ||
|
|
||
|
|
||
| ### 3. 구분자 파싱 단계 | ||
| | 조건 | 처리 방식 | 메시지 | | ||
| |------|-------------|-------------| | ||
| | 커스텀 구분자 식별 실패 | `IllegalArgumentException` | `"커스텀 구분자 형식이 올바르지 않습니다."` | | ||
| | 공백 포함 | `IllegalArgumentException` | `"공백은 구분자로 사용할 수 없습니다."` | | ||
| | 예약어(`//`, `\n`, `,`, `:`) 포함 | `IllegalArgumentException` | `"예약어는 구분자로 사용할 수 없습니다."` | | ||
|
|
||
|
|
||
| ### 4. 문자열 파싱 단계 | ||
| | 조건 | 처리 방식 | 메시지 | | ||
| |------|-------------|-------------| | ||
| | 숫자 변환 실패 | `IllegalArgumentException` | `"숫자 형식이 올바르지 않습니다."` | | ||
| | 음수 존재 | `IllegalArgumentException` | `"음수는 입력할 수 없습니다."` | | ||
|
|
||
|
|
||
| ### 5. 계산 단계 | ||
| | 조건 | 처리 방식 | 메시지 | | ||
| |------|-------------|-------------| | ||
| | 파싱 결과가 비어 있음 | `IllegalArgumentException` | `"계산할 숫자가 없습니다."` | | ||
|
|
||
|
|
||
| --- | ||
|
|
||
| ## 3. 프로젝트 구조 | ||
|
|
||
| ``` | ||
| src | ||
| └─ main/java/calculator | ||
| ├─ Application.java # 실행 진입점(main) | ||
| │ | ||
| ├─ controller | ||
| │ └─ CalculatorController.java # 전체 흐름 제어 | ||
| │ | ||
| ├─ view | ||
| │ ├─ InputView.java # 사용자 입력 | ||
| │ └─ OutputView.java # 결과 출력 | ||
| │ | ||
| ├─ model | ||
| │ ├─ calculator # 문자열 파싱 및 합산 로직 | ||
| │ │ ├─ Calculator.java # 계산 로직(덧셈) | ||
| │ │ └─ Result.java # 계산 결과를 값 객체로 캡슐화 | ||
| │ ├─ parser | ||
| │ │ ├─ DelimiterParser.java # 기본/커스텀 구분자 추출 | ||
| │ │ ├─ NumberParser.java # 문자열 파싱, 숫자 배열로 변환 | ||
| │ │ └─ DelimiterPattern.java # 커스텀 구분자 패턴 정의 | ||
| │ └─ validation | ||
| │ └─ Validator.java # 입력값 유효성 검사 | ||
| │ | ||
| └─ util | ||
| ├─ Constants.java # 기본 구분자, 정규식 패턴 등의 상수 | ||
| └─ ExceptionMessages.java # 예외 메시지 | ||
| ``` | ||
|
|
||
| --- | ||
| ## 4. 품질 요구사항 | ||
|
|
||
| - **코드 컨벤션 준수** | ||
| - Java Style Guide | ||
| - Git Commit Convention | ||
| - **단일 책임 원칙 준수** | ||
| - **예외 명세화** | ||
| - ExceptionMessages를 도입해 예외 메시지를 상수로 관리 | ||
| - 하드코딩된 메시지를 제거하여 유지보수성과 일관성 강화 | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,9 @@ | ||
| package calculator; | ||
|
|
||
| import calculator.controller.CalculatorController; | ||
|
|
||
| public class Application { | ||
| public static void main(String[] args) { | ||
| // TODO: 프로그램 구현 | ||
| new CalculatorController().run(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| package calculator.controller; | ||
|
|
||
| import calculator.model.calculator.Calculator; | ||
| import calculator.model.calculator.Result; | ||
| import calculator.model.parser.DelimiterParseResult; | ||
| import calculator.model.parser.DelimiterParser; | ||
| import calculator.model.parser.NumberParser; | ||
| import calculator.model.validation.Validator; | ||
| import calculator.view.InputView; | ||
| import calculator.view.OutputView; | ||
|
|
||
| public class CalculatorController { | ||
|
|
||
| private final InputView inputView = new InputView(); | ||
| private final OutputView outputView = new OutputView(); | ||
|
|
||
| public void run() { | ||
| String input = readInput(); | ||
| Result result = processInput(input); | ||
| printResult(result); | ||
| } | ||
|
|
||
| /** 사용자 입력 */ | ||
| private String readInput() { | ||
| return inputView.read(); | ||
| } | ||
|
|
||
| /** 입력 처리 로직 (공통 사용: run() + testRun()) */ | ||
| private Result processInput(String input) { | ||
| Validator.validateInput(input); | ||
|
|
||
| DelimiterParseResult parseResult = DelimiterParser.parse(input); | ||
| String[] tokens = NumberParser.parse(parseResult.getBody(), parseResult.getDelimiters()); | ||
|
|
||
| return Calculator.calculate(tokens); | ||
| } | ||
|
|
||
| /** 결과 출력 */ | ||
| private void printResult(Result result) { | ||
| outputView.printResult(result); | ||
| } | ||
|
Comment on lines
+38
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| /** 테스트 전용 메서드 (콘솔 I/O 배제) */ | ||
| public String testRun(String input) { | ||
| Result result = processInput(input); | ||
| return "결과 : " + result.getValue(); | ||
| } | ||
|
Comment on lines
+43
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 프로덕션 코드에 테스트 전용 메서드가 있는 것은 한번 생각할 필요가 있어보입니다.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 빠르게 테스트 환경을 구성하는 과정에서 구조적으로 고려가 부족했던 부분인데, 지적 감사합니다 |
||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 개행도 컨벤션 입니다! 물론 관습적인 부분이지만 깃허브에서 에러로 표시하는 만큼 지킬 필요가 있다고 생각합니다!
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 부분에 대해서는 미처 신경쓰지 못했습니다😓 |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package calculator.model.calculator; | ||
|
|
||
| import calculator.util.ExceptionMessages; | ||
| import java.util.Arrays; | ||
|
|
||
| /** | ||
| * 파싱된 문자열 토큰을 정수로 변환해 합산한다 | ||
| */ | ||
| public class Calculator { | ||
|
|
||
| private Calculator() {} | ||
|
|
||
| public static Result calculate(String[] tokens) { | ||
| if (tokens == null || tokens.length == 0) { | ||
| throw new IllegalArgumentException(ExceptionMessages.INPUT_EMPTY.get()); | ||
| } | ||
|
|
||
| int sum = Arrays.stream(tokens) | ||
| .mapToInt(Calculator::parseAndValidate) | ||
| .sum(); | ||
|
|
||
| return new Result(sum); | ||
| } | ||
|
Comment on lines
+13
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기에서 static을 사용하신 이유가 있으신가요? 저는 static은 존재만으로도 객체지향에서 많이 벗어난다고 생각합니다. 😄
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Calculator가 상태를 가지지 않고, 입력을 받아 단순히 계산만 수행하는 순수 함수에 가깝다고 판단해서 static으로 작성했습니다. 말씀하신 부분을 생각해보니 객체지향적으로는 책임을 가진 객체가 동작을 수행하는 구조가 더 바람직하다고 생각되어서 이후 확장을 고려하면 인스턴스 메서드로 전환하는 편이 맞을 것 같습니다. |
||
|
|
||
| private static int parseAndValidate(String token) { | ||
| if (token == null || token.isBlank()) { | ||
| throw new IllegalArgumentException(ExceptionMessages.INPUT_EMPTY.get()); | ||
| } | ||
|
Comment on lines
+13
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 부분에서 왜 tokens랑 token을 둘 다 확인하는건가요?!
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저는 코드 흐름상으로는 먼저
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 둘의 차이가 뭘까 궁금했는데 세세하게 잘 구분해서 검사하신 거였네요! 람다식 축약형까지 잘 배워갑니다! |
||
|
|
||
| try { | ||
| int number = Integer.parseInt(token); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| if (number < 0) { | ||
| throw new IllegalArgumentException(ExceptionMessages.NEGATIVE_NUMBER.get()); | ||
| } | ||
| return number; | ||
| } catch (NumberFormatException e) { | ||
| throw new IllegalArgumentException(ExceptionMessages.INVALID_NUMBER_FORMAT.get()); | ||
|
Comment on lines
+30
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저는
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
이 두 가지 의미의 질문이라고 해석되는데요 맞을까요? 공식 테스트는 저는 제 Controller 코드를 발췌했는데요 try-catch를 쓰지 않았고, 예외가 발생하면 자동으로 상위로 던져져서, 프로그램이 종료되는 구조예요 return으로 메시지를 출력한 후 종료시키는 방식은 예외가 던져지는게 아니고, 예외 메시지를 콘솔에 출력 후 main 메서드가 정상 종료되는 구조로ㅡ 예외가 던져지지 않아서 공식 테케 통과가 안될 것이라고 생각하는데요 그리고 제 생각에는 return으로 메시지를 출력 후 종료하는 방식은 로직과 출력이 분리되지 않아서 좋지 않은 설계인거 같아요ㅠ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 잡고 던진다는 게 어떤 뜻인가 이해가 잘 안 갔었는데 설명과 코드 같이 보면서 이해했습니다! 친절한 설명 감사드려요 😄 저는 무조건 try-catch로 잡아서 던져야 하는 줄 알았는데 아니었더라구요 return으로 메시지 출력 후 종료하는 방식에 대한 생각도 잘 들었습니다! 직접 적용해보고 확인해서 알려주신다니 감사합니다 👍 |
||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package calculator.model.calculator; | ||
|
|
||
| public class Result { | ||
| private final int value; | ||
|
|
||
| public Result(int value) { | ||
| this.value = value; | ||
| } | ||
|
|
||
| public int getValue() { | ||
| return value; | ||
| } | ||
|
|
||
| @Override | ||
| public String toString() { | ||
| return "결과 : " + value; | ||
| } | ||
|
Comment on lines
+14
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 말씀하신 부분처럼 구현에서 다만 말씀 주신 대로 혼동을 줄이려면 이 메서드는 제거하거나, |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package calculator.model.parser; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public class DelimiterParseResult { | ||
| private final List<String> delimiters; | ||
| private final String body; | ||
|
|
||
| public DelimiterParseResult(List<String> delimiters, String body) { | ||
| this.delimiters = delimiters; | ||
| this.body = body; | ||
| } | ||
|
|
||
| public List<String> getDelimiters() { | ||
| return delimiters; | ||
| } | ||
|
|
||
| public String getBody() { | ||
| return body; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| package calculator.model.parser; | ||
|
|
||
| import calculator.util.Constants; | ||
| import calculator.util.ExceptionMessages; | ||
| import java.util.Arrays; | ||
| import java.util.List; | ||
|
|
||
| public class DelimiterParser { | ||
|
|
||
| private static final List<String> DEFAULT_DELIMITERS = List.of( | ||
| Constants.DEFAULT_DELIMITER_COMMA.get(), | ||
| Constants.DEFAULT_DELIMITER_COLON.get() | ||
| ); | ||
|
Comment on lines
+10
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| private DelimiterParser() {} | ||
|
|
||
| /** | ||
| * 입력 문자열에서 구분자 목록과 본문(body)을 함께 추출한다 | ||
| * | ||
| * @param input 전체 입력 문자열 | ||
| * @return 구분자 목록과 본문을 담은 DelimiterParseResult | ||
| */ | ||
| public static DelimiterParseResult parse(String input) { | ||
| // 개행 문자 이스케이프 처리 | ||
| input = input.replace( | ||
| Constants.CUSTOM_NEWLINE_ESCAPE.get(), | ||
| Constants.CUSTOM_NEWLINE_ACTUAL.get() | ||
| ); | ||
|
|
||
| // 커스텀 구분자가 없는 경우 | ||
| if (!input.startsWith(Constants.CUSTOM_PREFIX.get())) { | ||
| return new DelimiterParseResult(DEFAULT_DELIMITERS, input); | ||
| } | ||
|
|
||
| int newlineIndex = input.indexOf(Constants.CUSTOM_NEWLINE_ACTUAL.get()); | ||
| if (newlineIndex == -1) { | ||
| throw new IllegalArgumentException(ExceptionMessages.INVALID_CUSTOM_FORMAT.get()); | ||
| } | ||
|
|
||
| String customDelimiter = input.substring( | ||
| Constants.CUSTOM_PREFIX.get().length(), | ||
| newlineIndex | ||
| ); | ||
|
|
||
| validateCustomDelimiter(customDelimiter); | ||
|
|
||
| List<String> delimiters = Arrays.asList( | ||
| customDelimiter, | ||
| Constants.DEFAULT_DELIMITER_COMMA.get(), | ||
| Constants.DEFAULT_DELIMITER_COLON.get() | ||
| ); | ||
|
|
||
| String body = input.substring(newlineIndex + 1); | ||
|
|
||
| return new DelimiterParseResult(delimiters, body); | ||
| } | ||
|
|
||
| /** | ||
| * 커스텀 구분자 유효성 검사 | ||
| */ | ||
| private static void validateCustomDelimiter(String delimiter) { | ||
| if (delimiter.isBlank() || delimiter.contains(" ")) { | ||
| throw new IllegalArgumentException(ExceptionMessages.INVALID_CUSTOM_WHITESPACE.get()); | ||
| } | ||
|
|
||
| if (delimiter.contains(Constants.DEFAULT_DELIMITER_COMMA.get()) | ||
| || delimiter.contains(Constants.DEFAULT_DELIMITER_COLON.get()) | ||
| || delimiter.contains(Constants.CUSTOM_PREFIX.get()) | ||
| || delimiter.contains(Constants.CUSTOM_NEWLINE_ACTUAL.get())) { | ||
| throw new IllegalArgumentException(ExceptionMessages.INVALID_CUSTOM_RESERVED.get()); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package calculator.model.parser; | ||
|
|
||
| import java.util.List; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| public class DelimiterPattern { | ||
| private static final String REGEX_SPECIALS = "([\\\\^$.|?*+()\\[\\]])"; | ||
|
|
||
| // 모든 구분자를 OR (|)로 연결해서 하나의 정규식 패턴 생성 | ||
| public static String buildPattern(List<String> delimiters) { | ||
| return delimiters.stream() | ||
| .map(DelimiterPattern::escapeSpecialChars) | ||
| .collect(Collectors.joining("|")); | ||
| } | ||
|
|
||
| // 정규식 예약문자(\, *, [, ]) 등을 이스케이프 처리 | ||
| private static String escapeSpecialChars(String delimiter) { | ||
| return delimiter.replaceAll(REGEX_SPECIALS, "\\\\$1"); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
대부분의 주석은 필요 없다는 말이 있습니다. 물론 사람마다 의견이 다르겠지만 저는 어느정도 공감이 되는 것 같아요.
주석으로 설명해야 하는 코드라면 이미 가독성이 떨어지는 코드가 아닌가 생각합니다. 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
말씀하신 부분 공감합니다 이후 개발에 반영하도록 하겠습니다.