이번에 BCSDLab에서 주최하는 백엔드 프리코스를 진행하였다. 초록스터디에서 진행되는 프로젝트로 백엔드 기초에 대해 배울수 있을 것 같아 참여하게 되었다.
저번 우아한테크코스 과정이 내가 공부하는 이유를 알려줬기 때문에 이번에도 꺠달음을 얻을 수 있지 않을까 싶다. 아쉬운 점은 4주차 미션을 제외하고는 우테코에서 이미 진행한 미션이기 때문에 중복된 요소가 너무 많다.
하지만 우테코에서 낸 개발새발 코드를 수정하고 피드백도 받을 수 있는 좋은 기회라 생각한다. 그때는 내 코드가 창피해서 피드백도 못받아봤는데 이제는 부끄러워도 괜찮다.
아쉬운 점은 MVC패턴이나 spring, database는 배우지 않는 점... 아마 프리코스라 그런 것 같다. 이번학기 BCSD 백엔드 비기너 과정에 참여할 것이니 그때를 위해 미리 독학해봐야겠다.

우선 간단한 계산기를 먼저 구현해보는 미션이 있었다. 간단한 계산기는 말 그대로 간단하다.
덧셈, 뺄셈, 곱셈, 나눗셈을 구현하면 된다.
코드는 다음과 같다.
public class SimpleCalculator {
public int add(int firstNum, int secondNum){
return firstNum + secondNum;
}
public int multiply(int firstNum, int secondNum){
return firstNum * secondNum;
}
public int divide(int firstNum, int secondNum){
return firstNum / secondNum;
}
public int minus(int firstNum, int secondNum){
return firstNum - secondNum;
}
}
너무나도 간단하지 않은가? 하지만 신경쓰지 못한 점이 매우 많다. 바로 오버플로우와 0 나누기이다. 간단하지만 신경쓸 점은 있는 계산기였다. 하지만 간단하다는 점에 눈이 멀어 많은 부분을 신경쓰지 못했다.
오버플로우를 신경 쓰자.
항상 C++과제나 자바 과제에서 오버플로우를 신경쓰지 못했다. 이번에도 마찬가지로 오버플로우를 신경 쓰지 않아 피드백이 들어왔다. 더 꼼꼼하게 봐야겠다.
0 나누기를 고려하자.
나눗셈에서 가장 문제는 0 나누기이다. 이 부분도 신경쓰지 못했다. 피드백은 들어오지 않았지만 다른 사람들의 코드를 리뷰하면서 알아 차렸다.

문자열 계산기는 특정 구분자를 통해 문자를 구분하여 덧셈을 진행하는 계산기이다.
이때 사용자로부터 받은 커스텀 구분자도 존재한다.
커스텀 구분자를 구분하기 위해 startWith 메소드를 통해 "//"시작 부분을 찾았다.
그 후 "\n" 문자가 있는지 파악하여 있다면 커스텀 구분자를 채택한다.
코드는 다음과 같다.
/*
input을 받고 빈문자열인지 null인지 체크
구분자 체크 -> 구분자 예외 체크
구분자 대로 문자열 계산기 구현
*/
import java.util.regex.Pattern;
public class StringCalculator {
//입력 문자열이 null인지 확인
public String checkInput(String input){
if(input == null) throw new RuntimeException("null은 허용되지 않습니다.");
return input;
}
//커스텀 구분자가 있는지 확인
public int checkDelimiter(String checkedInput) {
if(checkedInput.isEmpty()) return -2; //문자열이 빈 경우
if (checkedInput.startsWith("//")) { //커스텀 구분자를 설정하는가?
int idx = checkedInput.indexOf("\n"); //제대로된 형식인가?
if (idx == -1) throw new RuntimeException("잘못된 커스텀 구분자 형식입니다.");
return idx; //구분자 위치 전달
}
return -1; //기본 구분자대로 문자열을 나눔
}
private String[] splitInput(String checkedInput, int idx){
if(idx == -1){
String delimiter = "[,:]";
return checkedInput.split(delimiter); //기본 구분자대로 나누기
}
else if(idx == -2){
return new String[0]; //빈 문자열인 경우
}
else{
String delimiter = checkedInput.substring(2, idx); //구분자 나누기
String regex = Pattern.quote(delimiter); //정규표현식에 사용되는 문자가 커스텀 구분자일 경우를 위해 설정
return checkedInput.substring(idx + 1).split(regex); //커스텀 구분자대로 문자열을 나눔
}
}
//문자열 덧셈 실행
public int sum(String[] strings){
if(strings.length == 0) return 0;
int sum = 0;
for(var n : strings){
if(n.isEmpty()) throw new RuntimeException("빈 문자열입니다.");
int num = checkException(n);
//예외 처리
if(sum > Integer.MAX_VALUE - num || sum < Integer.MIN_VALUE + num) throw new RuntimeException("int 범위를 벗어났습니다.");
sum += num;
}
return sum;
}
private int checkException(String n){
int num;
try{
num = Integer.parseInt(n);
if(num < 0) throw new RuntimeException("음수는 입력이 불가합니다.");
}
catch(NumberFormatException msg){
throw new RuntimeException("int 값을 벗어났거나 잘못된 숫자 형식입니다.");
}
return num;
}
public int calculate(String input){
String checkedInput = checkInput(input);
int idx = checkDelimiter(checkedInput);
String[] strings = splitInput(checkedInput, idx);
return sum(strings);
}
}
오버플로우를 신경 쓰자.
여기 또한 오버플로우를 신경 쓰지 못했었다. 하지만 피드백을 통해 수정하였다.
한 메소드는 하나의 일만
메소드 이름은 checkDelimiter이면서 구분자를 체크하고 추출까지 하는 두개의 일을 한다는 피드백이 들어왔다. 그래서 checkDelimiter와 splitInput 함수로 두개 나누어서 진행했다.
코드가 너무 깊어지지 않게 모듈화 하자.
이건 우테코에서 제한 사항으로 있던 사항과 유사하다. 코드가 깊어지면 복잡하고 이해하기 어렵다. 그래서 코드가 너무 깊어지지 않도록 모듈화를 하는 습관을 들여야한다.
원래는 checkExecption 함수가 없어서 코드가 깊었지만 checkExecption 함수를 통해 모듈화하였다.
그 외에도 내 개인적으로 아쉬운 부분이 많다...
일단 구분자를 구분하기 위한 "\n"을 찾는 부분이다. 이 문자는 줄넘김 문자이다. 만약 내가 줄넘김 문자를 찾고 싶다면 indexOf("\n")를 진행하면 된다.
하지만 줄넘김 문자가 아닌 말 그대로 "\n" 이 문자 그대로를 찾고 싶다면 "\n" 이스케이프 문자를 고려해야한다.
패턴매칭 Pattern.quote 메소드를 배웠다. 이 메소드는 정규 표현식에 사용되는 문자를 제대로 구분할 수 있도록 한다. 원래는 정규표현식에 사용되는 문자는 "\n" 이런 형태 처럼 따로 이스케이프 처리를 해주어야 한다. 만약 해주지 않으면 예외가 발생한다. 하지만 Pattern.quote 이 메소드는 내가 따로 처리 해주지 않아도 알아서 처리를 해주는 고마운 메소드이다. 이 메소드를 통해 커스텀 구분자가 정규 표현식에 사용되는 . ? / [ ] 이어도 간단하게 처리 가능하다.
자바 API 문서에서 quote 탭을 보면 이해가 갈 것이다.
이번 미션에서는 공통적으로 "메인 메소드를 사용하지 마라"라는 요구사항이 있었다. 처음에는 어떻게 메인 메소드를 사용안하지? 생각했지만 단위 테스트 학습 내용이 있는 것을 보고 "아, 단위 테스트를 사용하는 거구나"라고 깨달았다.
java의 단위 테스트로는
두 개가 있는데 이번 미션에서는 두개 모두를 학습하고 사용한다.
@Test
void default_delimiter_Test(){
StringCalculator calc = new StringCalculator();
assertEquals(3, calc.calculate("1,2"));
assertEquals(6, calc.calculate("1,2,3"));
assertEquals(3, calc.calculate("1:2"));
assertEquals(38, calc.calculate("1:2,3:4,5:6:7:10"));
assertEquals(0, calc.calculate(""));
}
이런 식으로 @Test annotation을 붙여 Test함수임을 명시한다. 그리고 Jnit5의 함수를 통해 테스트를 검증한다.
예를들면 assretEquals(정답, 내 코드 실행)을 통해 내가 실행한 코드가 주는 값과 정답이 맞는지 확인한다.
다음은 예외 검증이다.
@Test
void exceptionTest(){
StringCalculator calc = new StringCalculator();
RuntimeException exception = assertThrows(RuntimeException.class, () ->
calc.calculate("-1,2")
);
assertEquals("음수는 입력이 불가합니다.", exception.getMessage());
예외가 발생하는지는 이런식으로 assertThrows 함수를 통해 확인할 수 있다. 현재 음수가 들어오면 RuntimeException 예외를 일으키도록 설정했기 때문에 코드가 제대로 작동한다면 잘 통과 될 것이다.
@Test
void stringCalcTest(){
StringCalculator calc = new StringCalculator();
assertThat(calc.calculate("//;\n1;2;3")).isEqualTo(6);
AssertJ는 @Test annotation을 붙이는 것은 Junit5와 똑같지만 assertThat 함수를 통해 내 코드가 반환하는 값이 정답과 같은지를 isEqualTo를 통해 확인한다.
예외 검증은 다음과 같다.
@Test
void exceptionTest(){
StringCalculator calc = new StringCalculator();
assertThatThrownBy(() ->
calc.calculate("-1,2")).isInstanceOf(RuntimeException.class);
assertThatThrownBy(() -> calc.calculate("//:123")).isInstanceOf(RuntimeException.class);
}
assertThatThrwonBy와 isInstanceOf 메소드를 통해 어떤 클래스이 예외인지 파악할 수 있다.
피드백을 통해 많이 성장한 기분이다. 특히 앞에서도 말했듯이 우테코에서는 내 코드가 부끄러워서 피드백을 못 받았는데 이번에는 양껏 피드백 받고 나도 남에게 피드백을 줬다. 또 다른 사람들이 받은 피드백도 구경하면서 내 코드도 보완하였다. 참 뿌듯했다.
java의 메소드나 단위 테스트 기능들도 배우게 되었다. 아쉬운 점은 다양한 고려사항들을 생각지 못했다는 것이다. 그래도 이를 위해 피드백을 받는 것이니 계속해서 성장해나갈 것이다.