우테코 프리코스 - 1주차 과제 회고
1주차 과제가 끝났다. 처음에 요구사항을 봤을 때는 쉽다고 생각했지만, 예외를 생각하다보니 리드미 작성부터 꽤나 시간이 많이 걸린 것 같다. 우선 1주차 과제의 기능 요구사항부터 간단하게 살펴보자.
기능 요구사항
- 쉼표(,) 또는 콜론(:)을 구분자로 가지는 문자열을 전달하는 경우 구분자를 기준으로 분리한 각 숫자의 합을 반환한다.
- 예: “” => 0, “1,2” => 3, “1,2,3” => 6, “1,2:3” => 6
- 앞의 기본 구분자(쉼표, 콜론) 외에 커스텀 구분자를 지정할 수 있다. 커스텀 구분자는 문자열 앞부분의 “//”와 “\n” 사이에 위치하는 문자를 커스텀 구분자로 사용한다.
- 예를 들어 “//;\n1;2;3”과 같이 값을 입력할 경우 커스텀 구분자는 세미콜론(;)이며, 결과 값은 6이 반환되어야 한다.
- 사용자가 잘못된 값을 입력할 경우
IllegalArgumentException을 발생시킨 후 애플리케이션은 종료되어야 한다.
이 요구사항을 충족하기 위해서 나는 다음과 같이 리드미를 작성했다.
기능 구현 사항
입력 및 출력
- 계산기 작동 시
덧셈할 문자열을 입력해 주세요.를 출력한다. - 콘솔을 통해 문자열을 입력받는다.
- 계산기의 계산 결과를
결과 : [합계]형식으로 출력한다.
구분자 처리
- 기본 구분자 (
,,:)를 기준으로 숫자를 추출한다. - 커스텀 구분자가 지정된 경우 기본 구분자에 커스텀 구분자를 포함하여 숫자를 추출한다.
- 빈 문자열·공백·커스텀 구분자만 입력되었을 때에 추출되는 숫자는 없다.
- 검증 사항
- 잘못된 커스텀 구분자 선언문 (E1)
수식 유효성 검증
- 추출된 숫자가 계산이 가능한 숫자인지 검사한다.
- 검증 사항
- 커스텀 또는 기본 구분자의 남용 (E2)
- 지정되지 않은 구분자 입력 (E3)
- 숫자가 아닌 문자의 입력 (E4)
- 구분자 사이에 빈 문자열 또는 공백 입력 (E5)
- 음수의 입력 (E6)
계산기
- 구분자 처리를 통해 추출된 숫자를 전달받아 합계를 계산한다.
- 추출 결과 아무런 숫자가 없다면 계산 결과가 0이 된다.
오류 상황
| 코드 | 상황 | 예시 입력 | 오류 메시지 |
|---|---|---|---|
| E1 | 잘못된 커스텀 구분자 선언 | //;\\1;2;3 |
잘못된 구분자 지정 방식입니다. (‘;’ 지정 예시: //;\n) |
| E2 | 커스텀 또는 기본 구분자의 중복 사용 | //;\\n1,:2;;3 |
수식에 공백이 존재합니다. |
| E3 | 지정되지 않은 구분자 입력 | //;\\n1.2;3 |
수식에 문자, 지정되지 않은 구분자, 중복 구분자가 존재합니다. |
| E4 | 숫자가 아닌 문자의 입력 | 1,w;3 |
수식에 문자, 지정되지 않은 구분자, 중복 구분자가 존재합니다. |
| E5 | 수식에 빈 문자열 또는 공백 입력 | 1,;3, 1, ;3 |
수식에 문자, 지정되지 않은 구분자, 중복 구분자가 존재합니다. |
| E6 | 음수 입력 | 1,-2:3 |
음수는 계산할 수 없습니다. |
회고
Keep
리드미 작성
리드미를 작성하면서 어떻게 하면 보기에 깔끔하면서도 상세하게 보일 수 있지? 생각을 많이 했다. 특히 오류 상황에 코드를 부여하여 정리한 부분은 예전에 보일러가 고장났을 때 출력된 오류 코드가 생각나서 그렇게 작성해봤는데 전체적으로 리드미가 다른 사람이 읽었을 때 ‘어떤 상황에서 이 제품이 오류를 발생하는지’에 대한 정보가 확 눈이 들어온다.
책임 분리 고려
이 객체는 어떤 범위의 책임을 가져야 할까에 대해서 많이 고민했다. MVC 패턴이 너무나 익숙한 사람인지라 그냥 공장에서 생산하듯이 코드를 작성해와서 이런 부분이 무뎌진 감이 있었기 때문에 오랜만에 객체의 책임을 생각 하면서 코드를 작성하는 경험은 가치있었다.
테스트를 진행하면서 개발 (TDD?)
나름대로 TDD를 적용해보면서 개발했다. 예전에 C언어로 이런 콘솔 애플리케이션을 개발했던 경험이 있었는데, 그때는 기능 구현 하나마다 실행을 하고, 실행을 통해서 문제를 발견하는 식으로 개발을 진행했었다. 하지만 이번에는 디버깅을 제외하고는 한 번도 실행하지 않고 개발을 수행하였다.
이 경험을 통해서 각 기능에 대한 단위 테스트가 얼마나 중요한지도 알게 되었다. 구분자로 숫자를 추출하는 클래스를 개발할 때, 내가 생각했던 기능에 대해서 테스트를 진행해나가며 만들고 나니까 계산기 클래스에서 이걸 사용할 때 걱정이 없었다.
Problem
기본적인 실수
이번 과제에서는 입력 관련 정적 메서드를 제공하는 클래스를 활용해야 했다. 나는 여기서 코드의 내부 구현 과정을 살펴보지 않았고, 그 결과 가장 치명적인 close() 호출을 하지 않았다.
이는 외부 라이브러리에 대한 신뢰가 코드 안정성보다 우선시 되어 발생한 문제이다.
입력 범위를 고려하지 않은 설계
입력범위가 주어지지 않았기 때문에 int 범위를 충분히 넘어서 오버플로우가 야기될 수 있기 때문에 BigInteger 같은 객체를 활용했어야 했고, 정수를 입력한다는 표현이 없기에 충분히 소수를 입력하고도 남는다.
이는 요구사항에 대해서 기능 단위와 성공 케이스에만 과도하게 초점을 맞춰서 개발했기 때문에 발생한 문제라고 생각한다.
안일한 given 처리
나는 하나의 테스트 케이스에서 두 개의 입력을 검증할 때 다음과 같이 처리했다.
@Test
@DisplayName("사용자가 아무런 수식을 입력하지 않거나 공백만 입력하면 계산 결과가 0이 된다.")
public void blankExpressionTest() {
// given
String expression = " ";
// String expression = "";
// ...
}
그 결과 실제 애플리케이션 테스트에 대해서 여러 given 중에 하나만 검증하게 되는 문제가 발생했다. 매개변수 테스트를 통해 케이스를 독립적으로 실행할 수 있다는 사실을 떠올리지 못해 발생한 문제이다.
검증에 대한 부재
나는 코드를 어느정도 작성하다가 이 부분에 대한 리팩토링을 하고 싶으면 AI와 함께 대화를 나누면서 처리하곤 했다. 하지만 이번 과제에서 문제가 드러났다.
public int calculate(String input) {
String[] numbers = delimiterParser.parse(input);
return Arrays.stream(numbers)
.peek(NumberValidator::validate)
.mapToInt(Integer::parseInt)
.sum();
}
stream() 에서 호출한 peek()는 디버깅 용 메서드로, 비즈니스 로직에 활용하는 것은 지양하라는 공식 문서를 인용한 리뷰를 받게 되었다. 어떻게 보면 앞선 기본적인 실수처럼, 외부에서 제공하는 것에 대해서 검증하는 습관이 부족한 것이 문제를 야기한 것 같다.
Try
요구사항 검토
기능 단위 구현에만 몰두하지 말고, 입력·출력·제약사항을 데이터 모델로 표현해 예외 케이스를 사전에 정의한다.
다양한 입력값에 대한 테스트 처리
여러 given이 존재하면 @ParameterizedTest를 활용한다.
@ParameterizedTest
@DisplayName("사용자가 아무런 수식을 입력하지 않거나 공백만 입력하면 계산 결과가 0이 된다.")
@ValueSource(strings = {" ", ""})
void blankInputTest(String expression) {
// ...
}
검증하는 습관을 가지자
내가 작성하지 않은 코드는 어떤 근거로 작성되었는지 모른다. 외부 코드를 사용하기 전에 내부 구현을 살펴보거나 Javadoc을 참고해 코드에 대한 검증을 습관화하자.
디자인 패턴을 활용해보자
솔직히 프로젝트의 규모가 작기 때문에 디자인 패턴을 적용하는 것이 의미가 있을까 생각하기도 하고, 디자인 패턴에 대해서 그렇게 잘 알지 못하기 때문에 이번 과제에서는 디자인 패턴을 딱히 적용하지 않았다.
하지만 이런 내 생각을 전면적으로 반박하는 블로그글이 있었고, 뭔가 뜨끔하게 되었다. 규모에 상관 없이 패턴 도입에 대한 고민을 꾸준히 해야겠다.
회고 외 고민
외부 값에만 의존하는 클래스 설계에 대한 고민
검증기를 초기에 설계할 때 이는 외부 값에만 의존하는 Math 같은 클래스로 생각하여 정적 메서드만을 구성해서 처리하면 될 것 같았다. 하지만 내부적으로 점점 private static한 메서드가 많아지고, 이에 따라서 ‘아 이건 인스턴스로 만들어야 좀 더 깔끔하지 않나?’ 생각이 들어서 결국 인스턴스로 만들었다.
하지만 다른 분의 코드 리뷰를 거치고, 공유된 블로그를 참고하다보니 private static이 많다고 꼭 그걸 인스턴스로 만들어야 하나? 라는 생각이 있었다. 여전히 이 부분은 고민이 된다.
작성해보니 굉장히 짧은 것 같지만, 어떤 문제로 발생했고, 이를 개선하기 위해서 어떤 것을 해야할지 생각하다보니 회고 작성이 굉장히 오래걸렸다. 내가 얻은 교훈을 2주차 과제에 반영하고자 한다.
댓글남기기