우아한테크코스 - 프리코스 1주차 : 문자열 덧셈 계산기
미션은 문자열 덧셈 계산기 저장소를 포크하고 클론하는 것으로 시작한다.
기능을 구현하기 전 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
프로그래밍 요구 사항
- JDK 21 버전에서 실행 가능해야 한다.
- 프로그램 실행의 시작점은 Application의 main()이다.
- build.gradle 파일은 변경할 수 없으며, 제공된 라이브러리 이외의 외부 라이브러리는 사용하지 않는다.
- 프로그램 종료 시 System.exit()를 호출하지 않는다.
- 프로그래밍 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 등의 이름을 바꾸거나 이동하지 않는다.
- 자바 코드 컨벤션을 지키면서 프로그래밍한다.
- 기본적으로 Java Style Guide를 원칙으로 한다.
라이브러리
- camp.nextstep.edu.missionutils에서 제공하는 Console API를 사용하여 구현해야 한다.
- 사용자가 입력하는 값은 camp.nextstep.edu.missionutils.Console의 readLine()을 활용한다.
구현 세부사항
명시되지 않은 요구사항에 대한 구현
커스텀 구분자
- 커스텀 구분자와 기본 구분자(
,,:)를 혼합하여 사용 가능 - 커스텀 구분자는 반드시 입력 문자열의 맨 앞에 정의되어야 함
- 커스텀 구분자는 문자 1개만 가능하며, 숫자는 불가
- 공백 문자(
' ')도 커스텀 구분자로 사용 가능 - 커스텀 구분자를 정의했으나 사용하지 않아도 정상 동작 (기본 구분자만 사용하거나 구분자 없이 숫자 하나만 입력)
입출력 범위
- 입력 숫자:
int범위 (-2,147,483,648 ~ 2,147,483,647) - 출력 결과:
long범위 (-9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807)
예외 상황
다음의 경우 IllegalArgumentException 발생:
| 예외 상황 | 예시 |
|---|---|
| 커스텀 구분자 문자의 길이가 1이 아닌 경우 | "//ab\n1", "// ;\n1" |
| 커스텀 구분자 형식이 올바르지 않은 경우 | "\n//", "// \n\n", "////\n" |
| 커스텀 구분자가 숫자인 경우 | "//5\n1:2" |
| 구분자가 연속으로 나오는 경우 | "1,,2", "1::2:3" |
| 양수가 아닌 값(0 또는 음수)이 입력된 경우 | "1,-2,3", "0,1,2" |
| 구분자와 숫자의 개수가 맞지 않는 경우 | "1,2,", ",1,2" |
코드 구조
메인 흐름: main()
main()
├─ readInput() // 문자열 입력 받아오기
├─ calculate() // 연산 수행
└─ printAnswer() // 결과 출력
연산 프로세스: calculate()
calculate()
├─ extractCustomSeparator() // 커스텀 구분자 추출
├─ checkCustomSeparatorFormat() // 커스텀 구분자 정의 형식 확인
└─ checkSeparatorIsNotDigit() // 커스텀 구분자 숫자 여부 확인
├─ getNumberString() // 숫자 문자열 추출 (커스텀 구분자 정의 제외 부분)
└─ sumNumbers() // 덧셈 연산 수행
├─ buildSeparatorPattern() // 구분자 패턴 생성
├─ checkNumberCount() // 개수 검증
└─ parseAndCheckNumber() // 숫자 파싱 및 검증
주요 메서드 설명
입력된 문자열 처리
| 메서드 | 설명 |
|---|---|
readInput() |
사용자로부터 문자열 입력 받기 |
calculate() |
입력 문자열을 처리하여 합계 연산 수행 |
printAnswer() |
계산 결과 출력 |
구분자 처리
| 메서드 | 설명 |
|---|---|
extractCustomSeparator() |
커스텀 구분자 추출 및 검증 |
checkCustomSeparatorFormat() |
커스텀 구분자 형식 확인 |
checkSeparatorIsNotDigit() |
구분자가 숫자인지 확인 |
buildSeparatorPattern() |
정규식 패턴으로 구분자 설정 |
getNumberString() |
구분자 설정 부분 제거 후 숫자 문자열 반환 |
계산 및 검증
| 메서드 | 설명 |
|---|---|
sumNumbers() |
문자열을 분리하여 덧셈 수행 |
parseAndCheckNumber() |
문자열을 숫자로 변환 및 검증 |
checkPositive() |
양수 여부 확인 |
checkNumberCount() |
구분자와 숫자 개수 일치 여부 확인 |
유틸리티
| 메서드 | 설명 |
|---|---|
countSeparators() |
문자열 내 구분자 개수 계산 |
isSeparator() |
특정 문자가 구분자인지 판단 |
내부 클래스
CustomSeparator
커스텀 구분자 정보를 담는 클래스
필드:
separator: 커스텀 구분자 문자hasCustomSeparator: 커스텀 구분자 존재 여부
메서드:
getSeparator(): 구분자 반환hasCustomSeparator(): 커스텀 구분자 존재 여부 반환
Application.java
package calculator;
import camp.nextstep.edu.missionutils.Console;
import java.util.regex.Pattern;
public class Application {
private static final String CUSTOM_SEPARATOR_BEFORE = "//";
private static final String CUSTOM_SEPARATOR_AFTER = "\\n";
private static final int CUSTOM_SEPARATOR_LENGTH = 5;
private static final String DEFAULT_SEPARATORS = ",|:";
public static void main(String[] args) {
try{
String inputStr = readInput();
long answer = calculate(inputStr);
printAnswer(answer);
}catch(IllegalArgumentException e){
System.out.println("잘못된 입력: " + e.getMessage());
throw e;
}finally{
Console.close();
}
}
private static String readInput(){
System.out.println("덧셈할 문자열을 입력해 주세요.");
return Console.readLine();
}
// 합 연산 결과 리턴
private static long calculate(String inputStr){
if (inputStr.isEmpty()) return 0;
CustomSeparator separator = extractCustomSeparator(inputStr);
String numberString = getNumberString(inputStr, separator);
if (numberString.isEmpty()) return 0;
return sumNumbers(numberString, separator);
}
// 커스텀 구분자 추출
private static CustomSeparator extractCustomSeparator(String inputStr){
if (!inputStr.startsWith(CUSTOM_SEPARATOR_BEFORE)) return new CustomSeparator(null, false);
checkCustomSeparatorFormat(inputStr);
char separator = inputStr.charAt(2);
checkSeparatorIsNotDigit(separator);
return new CustomSeparator(separator, true);
}
// 커스텀 구분자 형식 확인
private static void checkCustomSeparatorFormat(String inputStr){
if (inputStr.length() < CUSTOM_SEPARATOR_LENGTH) throw new IllegalArgumentException("커스텀 구분자 형식 오류.");
if (!inputStr.substring(3, 5).equals(CUSTOM_SEPARATOR_AFTER)) throw new IllegalArgumentException("커스텀 구분자 형식 오류.");
}
// 커스텀 구분자 숫자 여부 확인
private static void checkSeparatorIsNotDigit(char separator){
if (Character.isDigit(separator)) throw new IllegalArgumentException("커스텀 구분자 숫자 불가.");
}
// 구분자 설정 부분 문자열에서 제거
private static String getNumberString(String inputStr, CustomSeparator separator){
if (separator.hasCustomSeparator()) return inputStr.substring(CUSTOM_SEPARATOR_LENGTH);
return inputStr;
}
// 문자열 분리 후 합 연산
private static long sumNumbers(String numberString, CustomSeparator separator){
String separators = buildSeparatorPattern(separator);
String[] numbers = numberString.split(separators);
checkNumberCount(numberString, numbers.length, separator);
long sum = 0;
for (String number : numbers){
sum += parseAndCheckNumber(number);
}
return sum;
}
// 구분자 설정 값 리턴
private static String buildSeparatorPattern(CustomSeparator separator){
if (!separator.hasCustomSeparator()) return DEFAULT_SEPARATORS;
return DEFAULT_SEPARATORS + "|" + Pattern.quote(String.valueOf(separator.getSeparator()));
}
// 연산자, 피연산자 개수 확인
private static void checkNumberCount(String numberString, int numberCount, CustomSeparator separator){
int separatorCount = countSeparators(numberString, separator);
if (separatorCount+1 != numberCount) throw new IllegalArgumentException("구분자와 숫자 개수 형식 불일치.");
}
// 구분자 개수 리턴
private static int countSeparators(String numberString, CustomSeparator separator){
int count = 0;
for (char c : numberString.toCharArray()){
if (isSeparator(c, separator)) count++;
}
return count;
}
// 구분자 여부 boolean 리턴
private static boolean isSeparator(char c, CustomSeparator separator){
if (c == ',' || c == ':') return true;
return separator.hasCustomSeparator() && c == separator.getSeparator();
}
// 입력 값 int 범위 여부 확인 및 숫자형 포맷 여부 확인
private static int parseAndCheckNumber(String number){
if (number.isEmpty()) throw new IllegalArgumentException("빈 값 입력 불가.");
try{
int num = Integer.parseInt(number);
checkPositive(num);
return num;
}catch(NumberFormatException e){
throw new IllegalArgumentException("올바른 숫자 형식이 아님.");
}
}
// 양수 여부 판단
private static void checkPositive(int number){
if (number <= 0) throw new IllegalArgumentException("양수만 입력 가능.");
}
// output 출력
private static void printAnswer(long answer){
System.out.println("결과 : " + answer);
}
// 커스텀 구분자 정보 class
private static class CustomSeparator{
private final Character separator;
private final boolean hasCustomSeparator;
public CustomSeparator(Character separator, boolean hasCustomSeparator){
this.separator = separator;
this.hasCustomSeparator = hasCustomSeparator;
}
public char getSeparator(){ return separator; }
public boolean hasCustomSeparator(){ return hasCustomSeparator; }
}
}
회고
1주차 프리코스를 마무리하며 크게 두 가지 분류(코드를 작성하며 개발할 때 겪었던 경험, 깃허브를 통한 작업 과정)를 통해 회고해보고자 한다.
평소 JS, C++ 위주로 알고리즘 공부와 개발을 진행하였기에 오랜만에 마주한 자바 코드의 문법들이 낯설어 다시 기본 문법부터 정리하는 시간을 가질 수 있었다. 특히 입력값에 대한 형식 검증을 위해 정규표현식을 사용하였는데 리터럴로 입력된 특수문자를 일반 문자로 처리하는 과정에서 Pattern 클래스의 quote()메서드를 활용하면 사용자가 입력한 문자를 안전하게 일반 문자로 처리할 수 있다는 것을 알게 되었다.
또한 기본적 문법 외에도 코드를 작성하며 자바 코드 컨벤션을 지키며 변수명, 메서드명 등을 선언하는 것이 추후 개발 작업에 있어서 공동 작업자와 커뮤니케이션을 위한 중요한 습관임을 알 수 있었다. (기존에 혼자 개발해오던 정해지지 않은 코드 스타일이 커뮤니케이션 뿐 아니라 가독성 측면에서도 좋지 않다는 것)
사용자의 입력값을 처리하는 과정에서 모든 입력 사항을 고려하는 것이 이번 개발의 주요 포인트라 생각하였다. 따라서 다양한 표준 오류 처리, 예외 처리를 진행하였는데, 모든 입력 예시가 명확히 제시되지 않은 만큼 스스로 다양한 입력 경우를 생각하여 입력 형식의 제한을 두고 예외 처리를 진행한 경험이 실무에서 실제 개발을 할 때 생길 수 있는 의도하지 않은 실행 및 버그를 찾아낼 수 있는 초석이 될 것이라 생각하였다.
다음 내용은 깃허브를 활용한 작업 수행에 대한 회고이다. 깃허브 레포지토리에 혼자서만 개발해오던 프로젝트, 알고리즘 공부 파일 등을 단순 클라우드처럼 사용하여 저장해왔다. 이번 1주차 프리코스를 진행하며 git fork를 통해 레포지토리를 복사하여 PR(Pull Request)하는 일련의 과정을 통해 깃허브를 통한 협력 개발의 진행 방식을 알 수 있었다.
추가로 커밋 메시지 작성에 있어 항상 신경쓰지 않고 깃허브에 push하던 이전의 습관들이 추후 코드를 리팩토링하거나 진행 사항을 인지하는데 매우 부적합 했다는 것을 커밋 메시지 컨벤션 가이드를 확인하며 알게 되었다. 실무에서는 개발 능력 만큼 중요한 것이 커뮤니케이션 능력이라는 것을 재차 확인할 수 있는 기회였다.
마지막으로 1주차를 마무리하며 느낀 부족한 점은, 유지보수성을 위해 최대한 기능별로 구분하여 메서드를 작성하였으나 전체적인 코드가 객체 지향보다는 절차 지향에 가깝다 느껴졌으며 내부에 선언한 클래스가 단순 데이터 컨테이너 역할만 수행하고 외부 메서드에 의존한다는 점이 더 큰 프로젝트나 복잡한 로직, 팀 프로젝트에 있어 문제가 된다 느꼈다. 앞으로의 프리코스에서는 코드의 확장성과 재사용 등을 고려하여 객체 지향적으로 개발하며 개선할 수 있도록 해야겠다.
우아한테크코스 - 프리코스 1주차 : 문자열 덧셈 계산기