깃허브 과제 링크 : https://github.com/lchyeong/java-calculator-7/tree/lchyeong
GitHub - lchyeong/java-calculator-7: 우테코 프리코스 1주차
우테코 프리코스 1주차. Contribute to lchyeong/java-calculator-7 development by creating an account on GitHub.
github.com
우테코 7기 프리코스 1주차가 지났습니다!
첫 주이니만큼 아무래도 환경설정, git 사용법, 커밋 컨벤션, 코드 스타일 등 기본 규칙들을 지켜야 할 사항이 많기 때문에 많은 지원자들이 다소 어려움을 겪었을 것이라 생각해봅니다.
디스코드랑 오픈카톡에 실제로 많은 기초 질문들이 올라왔고 공지사항으로 해당 부분들을 스스로 해결하는 것 또한 프리코스의 목적이라는 우테코의 공지로 대부분의 질문은 일단락되었습니다.
TDD에 대한 부분을 많이 알았던 것 같고, 잘 알지는 못하겠지만 눈에 익히는 아주 좋은 기회가 되었습니다.
첫 주의 과제는 바로바로
"문자열 덧셈 계산기"
과제 진행 요구 사항
- 미션은 문자열 덧셈 계산기 저장소를 포크하고 클론하는 것으로 시작한다.
- 기능을 구현하기 전 README.md에 구현할 기능 목록을 정리해 추가한다.
- Git의 커밋 단위는 앞 단계에서 README.md에 정리한 기능 목록 단위로 추가한다.
- AngularJS Git Commit Message Conventions을 참고해 커밋 메시지를 작성한다.
- 자세한 과제 진행 방법은 프리코스 진행 가이드 문서를 참고한다.
기능 요구 사항
입력한 문자열에서 숫자를 추출하여 더하는 계산기를 구현한다.
- 쉼표(,) 또는 콜론(:)을 구분자로 가지는 문자열을 전달하는 경우 구분자를 기준으로 분리한 각 숫자의 합을 반환한다.
- 예: "" => 0, "1,2" => 3, "1,2,3" => 6, "1,2:3" => 6
- 앞의 기본 구분자(쉼표, 콜론) 외에 커스텀 구분자를 지정할 수 있다. 커스텀 구분자는 문자열 앞부분의 "//"와 "\n" 사이에 위치하는 문자를 커스텀 구분자로 사용한다.
- 예를 들어 "//;\n1;2;3"과 같이 값을 입력할 경우 커스텀 구분자는 세미콜론(;)이며, 결과 값은 6이 반환되어야 한다.
- 사용자가 잘못된 값을 입력할 경우 IllegalArgumentException을 발생시킨 후 애플리케이션은 종료되어야 한다.
입출력 요구 사항
입력
- 구분자와 양수로 구성된 문자열
출력
- 덧셈 결과
결과 : 6
실행 결과 예시
덧셈할 문자열을 입력해 주세요.
1,2:3
결과 : 6
사실 pattern과 matcher를 사용해보지 않아 초반에 문제가 쉽다던 다른 지원자들에 공감을 잘 하지는 못했으나.. ㅎㅎㅎ 결국 해결을 했습니당. 제가 찬양하던 G선생을 못쓰고 코파일럿마저 꺼버리니 오직 인텔리제이와 함께한 개발 기간이 되지 않았나 싶습니다. 오히려 좋아... 100번 반복하면 몸이 먼저 나가듯 하도 손코딩하니까 많이 익숙해진 것 같습니다. 이 또한 큰 가르침이 아닐지...
처음엔 클래스를 3개정도로 분리해 기능을 대부분 때려박았습니다. 크게 메서드가 많이 필요한 과제도 아니었으며 단순한 계산기 수준이었기 때문에 그랬는데,,, 다 구현하고 고민하면서 설계부분에 엄청 시간을 많이 들인 것 같습니다. 어떻게 분리해야 객체지향적으로 분리하는거지? MVC 패턴으로 구현하려면 어떻게 되는거지? 에 대한 여러 고민들을 거치면서 과감하게 버릴 건 버리고 취할 건 취하면서 나온 저만의 문자열 덧셈 계산기 입니다 ㅎㅎㅎ...
PatternParser - 패턴 관련한 메서드를 모았습니다.
public class PatternParser {
public static final String CUSTOM_PATTERN = "^//(.*?)\\\\n";
public static final String RESERVE_PATTERN = "[,:]";
public String[] splitPattern(String input) {
if (isCustomPattern(input)) {
String delimiter = parseCustomPattern(input);
return input.split(delimiter);
}
return input.split(RESERVE_PATTERN);
}
private Matcher matcher(String regex, String input) {
return Pattern.compile(regex).matcher(input);
}
public boolean isCustomPattern(String input) {
Matcher matcher = matcher(CUSTOM_PATTERN, input);
return matcher.find() && (matcher.start() == 0);
}
public boolean isReservePattern(String input) {
Matcher matcher = matcher(RESERVE_PATTERN, input);
Set<String> delimiters = new HashSet<>();
while (matcher.find()) {
delimiters.add(matcher.group());
}
return delimiters.contains(",") || delimiters.contains(":");
}
public String parseCustomPattern(String input) {
Matcher matcher = matcher(CUSTOM_PATTERN, input);
if (matcher.find()) {
return matcher.group(1);
}
throw new IllegalArgumentException();
}
public boolean validatePattern(String input) {
if (isReservePattern(input) || isCustomPattern(input)) {
return true;
}
throw new IllegalArgumentException();
}
}
G선생의 도움을 받아 표로 정리해봅니다.
splitPattern(String input) | 입력 문자열을 커스텀 구분자 또는 예약된 구분자(콤마, 콜론)로 분할하여 배열로 반환함. |
matcher(String regex, String input) | 주어진 정규 표현식과 입력 문자열을 기반으로 Matcher 객체를 생성하여 반환함. |
isCustomPattern(String input) | 입력 문자열이 커스텀 패턴(//로 시작하는 구분자 정의)을 포함하고 있는지 여부를 확인함. |
isReservePattern(String input) | 입력 문자열에 예약된 구분자(콤마, 콜론)가 포함되어 있는지 여부를 확인함. |
parseCustomPattern(String input) | 커스텀 패턴에서 구분자를 추출하여 반환함. 커스텀 패턴이 없으면 IllegalArgumentException을 던짐. |
validatePattern(String input) | 입력 문자열이 커스텀 구분자 또는 예약된 구분자를 포함하고 있는지 검증함. 그렇지 않으면 예외 발생. |
PatternParser 클래스는 예약된 구분자(콤마 ,, 콜론 :)와 커스텀 구분자를 제외한 모든 입력에 대해 IllegalArgumentException을 발생시키며 애플리케이션이 종료되도록 설계되었습니다. 이와 더불어, matcher 메소드는 정규 표현식을 더 쉽게 사용할 수 있도록 커스텀하여 편의성을 높였습니다.
예약된 구분자인 콤마와 콜론의 중복 문제를 해결하기 위해 Set 자료구조를 사용했습니다. 이는 자바 강의에서 배운 자료구조 개념을 적용한 것으로, 중복을 자동으로 제거할 수 있기 때문에 적합하다고 판단했습니다.
한편, parseCustomPattern 메소드에서 예외를 발생시키는 부분에 대해서는 아직 확실하지 않은 고민이 남아있습니다. 예외가 발생할 상황이 많지는 않겠지만, 일단 기본적인 예외 처리를 넣어둔 상태입니다. 추후 더 구체적인 사례를 검토해봐야 할 것 같습니다.
NumberParser - 숫자 관련한 메서드를 모았습니다.
public class NumberParser {
public static final String NEGATIVE_PATTERN = "-\\d+";
public String[] removeNonDigits(String[] splitArr) {
for (int i = 0; i < splitArr.length; i++) {
splitArr[i] = splitArr[i].replaceAll("[^0-9]", "");
}
return splitArr;
}
public int sumNumber(String[] inputArr) {
int sum = 0;
for (int i = 0; i < inputArr.length; i++) {
if (!inputArr[i].isEmpty()) {
sum += Integer.parseInt(inputArr[i]);
}
}
return sum;
}
public boolean checkNegative(String[] inputArr) {
Pattern pattern = Pattern.compile(NEGATIVE_PATTERN);
for (int i = 0; i < inputArr.length; i++) {
Matcher matcher = pattern.matcher(inputArr[i]);
if (matcher.matches()) {
throw new IllegalArgumentException();
}
}
return true;
}
}
아래는 NumberParser 클래스의 메소드와 그 기능을 간단하게 정리한 표입니다:
removeNonDigits(String[] splitArr) | 입력된 문자열 배열에서 숫자가 아닌 모든 문자를 제거하고, 숫자만 남긴 배열을 반환함. |
sumNumber(String[] inputArr) | 문자열 배열을 순회하며 각 요소를 숫자로 변환해 합산하고 그 결과를 반환함. 빈 문자열은 무시함. |
checkNegative(String[] inputArr) | 배열 내 음수를 체크하여 음수가 발견되면 IllegalArgumentException을 발생시킴. 음수가 없으면 true 반환. |
여기서는 숫자가 아닌 것들을 다 잘래내고, 음수를 구별하고, 합을 구하는 기능을 합니다.ㅎㅎ
CalculatorViewer - 입출력 관련해서 모았습니다.
package calculator;
public class CalculatorViewer {
public void showInputPrompt() {
System.out.println("덧셈할 문자열을 입력해 주세요.");
}
public void displayResult(int result) {
System.out.println("결과 : " + result);
}
}
여기는 설명을 생략하겠습니다...
CalculatorUseCase
public class CalculatorUseCase {
private final CalculatorViewer calculatorViewer = new CalculatorViewer();
private final NumberParser numberParser = new NumberParser();
private final PatternParser patternParser = new PatternParser();
public void startCalculate() {
calculatorViewer.showInputPrompt();
String input = Console.readLine();
patternParser.validatePattern(input);
String[] splitInput = patternParser.splitPattern(input);
numberParser.checkNegative(splitInput);
String[] removeNonDigitsInput = numberParser.removeNonDigits(splitInput);
int result = numberParser.sumNumber(removeNonDigitsInput);
calculatorViewer.displayResult(result);
}
}
여기서는 실제 계산이 실행되는 메인이라고 보시면 될 것 같습니다.
-
- showInputPrompt() - 입력 요청
- readLine() - 사용자 입력 받기
- validatePattern(input) - 입력 패턴 검증
- splitPattern(input) - 입력 문자열 분할
- checkNegative(splitInput) - 음수 체크
- removeNonDigits(splitInput) - 숫자가 아닌 문자 제거
- sumNumber(removeNonDigitsInput) - 숫자 합산
- displayResult(result) - 결과 출력
이 클래스의 흐름은 사용자 입력 → 패턴 검증 및 분할 → 음수 체크 → 숫자 추출 → 합계 계산 → 결과 출력으로 이어집니다.
최대한 흐름에 맞게 보기 쉽도록 하겠다고 애썼는데 잘 됐는지 모르겠습니다... 저는 제꺼 계속보니까 익숙할 수밖에 없어서 더 그런 것 같습니다 ㅋㅋㅋ...
Application
public class Application {
public static void main(String[] args) {
CalculatorUseCase calculatorUseCase = new CalculatorUseCase();
calculatorUseCase.startCalculate();
}
}
먼저 Applicaition에는 단순히 UseCase에서 계산을 시작하는 로직만 불러왔습니다.
가장 애먹었던 부분인데 테스트를 하는데 입력을 inputStream으로 받으면서 여러개의 테스트를 동시에 돌리니까 console(우테코에서 커스텀한 Scanner)의 값이 이상해지는 현상이 발생했습니다. 단위테스트일 때는 문제가 없었는데 전체돌리면 문제가 생겼습니다. 이거저거 다 해보다가 close()로 테스트단위 끝나면 닫아주는 방법으로 하니 잘 통과했습니다 ㅎㅎㅎ...ㅠ
@AfterEach
public void closeConsole() {
Console.close();
}
@Test
public void testReservePattern() throws Exception {
//given
String testInput = "1,2:3,5";
InputStream inputStream = new ByteArrayInputStream(testInput.getBytes());
System.setIn(inputStream);
OutputStream outputStream = new ByteArrayOutputStream();
System.setOut(new PrintStream(outputStream));
//when
calculatorUseCase.startCalculate();
String result = "덧셈할 문자열을 입력해 주세요.\n결과 : 11\n";
//then
Assertions.assertEquals(result, outputStream.toString());
}
이제껏 정신없이 기능 구현에 집중했다면 이번 과제에선 간단하지만 이것을 어떻게 분리할 것인가에 대한 깊은 고민을 했던 것 같습니다. 실제로 처음과 완전히 다른 구조로 변하기도 했고 단일책임 원칙을 최대한 지키려고 노력했던 것 같습니다.
잘 된 것은 아닐 지 모르나 다른 분들의 코드를 보면서 인사이트를 얻어봐야겠습니다!
첫주 모두 고생 많이하셨습니다.
너무 재밌어요! ㅎㅎㅎ
'우아한테크코스 7기' 카테고리의 다른 글
우아한테크코스 7기 프리코스 2주차 회고 (26) | 2024.11.18 |
---|