문자열 덧셈 계산기를 통한 TDD/리팩토링 실습
기능 요구사항
- 쉼표(,) 또는 콜론(:)을 구분자로 가지는 문자열을 전달하는 경우 구분자를 기준으로 분리한 각 숫자의 합을 반환 (예: “” => 0, "1,2" => 3, "1,2,3" => 6, “1,2:3” => 6)
- 앞의 기본 구분자(쉼표, 콜론)외에 커스텀 구분자를 지정할 수 있다. 커스텀 구분자는 문자열 앞부분의 “//”와 “\n” 사이에 위치하는 문자를 커스텀 구분자로 사용한다. 예를 들어 “//;\n1;2;3”과 같이 값을 입력할 경우 커스텀 구분자는 세미콜론(;)이며, 결과 값은 6이 반환되어야 한다.
- 문자열 계산기에 숫자 이외의 값 또는 음수를 전달하는 경우 RuntimeException 예외를 throw한다.
프로그래밍 요구사항
- indent(들여쓰기) depth를 2단계에서 1단계로 줄여라.
- depth의 경우 if문을 사용하는 경우 1단계의 depth가 증가한다. if문 안에 while문을 사용한다면 depth가 2단계가 된다.
- 메소드의 크기가 최대 10라인을 넘지 않도록 구현한다.
- method가 한 가지 일만 하도록 최대한 작게 만들어라.
- else를 사용하지 마라.
기능 요구사항 분리 및 힌트
1. 빈 문자열 또는 null 값을 입력할 경우 0을 반환해야 한다.(예 : “” => 0, null => 0)
if (text == null) {}
if (text.isEmpty()) {}
2. 숫자 하나를 문자열로 입력할 경우 해당 숫자를 반환한다.(예 : “1”)
int number = Integer.parseInt(text);
3. 숫자 두개를 컴마(,) 구분자로 입력할 경우 두 숫자의 합을 반환한다.(예 : “1,2”)
String[] numbers = text.split(",");
// 앞 단계의 구분자가 없는 경우도 split()을 활용해 구현할 수 있는지 검토해 본다.
4. 구분자를 컴마(,) 이외에 콜론(:)을 사용할 수 있다. (예 : “1,2:3” => 6)
String[] tokens= text.split(",|:");
5. “//”와 “\n” 문자 사이에 커스텀 구분자를 지정할 수 있다. (예 : “//;\n1;2;3” => 6)
// java.util.regex 패키지의 Matcher, Pattern import
Matcher m = Pattern.compile("//(.)\n(.*)").matcher(text);
if (m.find()) {
String customDelimiter = m.group(1);
String[] tokens= m.group(2).split(customDelimiter);
// 덧셈 구현
}
6. 음수를 전달할 경우 RuntimeException 예외가 발생해야 한다. (예 : “-1,2,3”)
- 구글에서 “junit4 expected exception”으로 검색해 해결책을 찾는다.
TestCase 소스 코드
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
public class StringAddCalculatorTest {
@Test
public void splitAndSum_null_또는_빈문자() {
int result = StringAddCalculator.splitAndSum(null);
assertThat(result).isEqualTo(0);
result = StringAddCalculator.splitAndSum("");
assertThat(result).isEqualTo(0);
}
@Test
public void splitAndSum_숫자하나() throws Exception {
int result = StringAddCalculator.splitAndSum("1");
assertThat(result).isEqualTo(1);
}
@Test
public void splitAndSum_쉼표구분자() throws Exception {
int result = StringAddCalculator.splitAndSum("1,2");
assertThat(result).isEqualTo(3);
}
@Test
public void splitAndSum_쉼표_또는_콜론_구분자() throws Exception {
int result = StringAddCalculator.splitAndSum("1,2:3");
assertThat(result).isEqualTo(6);
}
@Test
public void splitAndSum_custom_구분자() throws Exception {
int result = StringAddCalculator.splitAndSum("//;\n1;2;3");
assertThat(result).isEqualTo(6);
}
@Test
public void splitAndSum_negative() throws Exception {
assertThatThrownBy(() -> StringAddCalculator.splitAndSum("-1,2,3"))
.isInstanceOf(RuntimeException.class);
}
}
기능 구현
우선 코드 구조를 상상해 보았다. 어떤 기능이 필요하고, 어떤 기능이 먼저 오고 뒤에 와야 할 지 선후 관계를 생각해 보았다.
대략적인 코드를 끝마친 뒤 README를 작성해 보았다.
# 로또
## 진행 방법
* 로또 요구사항을 파악한다.
* 요구사항에 대한 구현을 완료한 후 자신의 github 아이디에 해당하는 브랜치에 Pull Request(이하 PR)를 통해 코드 리뷰 요청을 한다.
* 코드 리뷰 피드백에 대한 개선 작업을 하고 다시 PUSH한다.
* 모든 피드백을 완료하면 다음 단계를 도전하고 앞의 과정을 반복한다.
## 온라인 코드 리뷰 과정
* [텍스트와 이미지로 살펴보는 온라인 코드 리뷰 과정](https://github.com/next-step/nextstep-docs/tree/master/codereview)
## toDo
1. Input 확인
- null 값 인 경우 0을 반환
- "" 값 인 경우 0을 반환
2. 문자열에 구분자가 있는 경우
- 구분자로 나누기
- 나눈 값에 음수가 있으면 Runtime Exception 에러 출력
- 음수가 없으면 모든 값을 더한 뒤 결과 출력
3. 문자열에 구분자가 없는 경우
- ",|:" 구분자로 나누기
- 나눈 값에 음수가 있으면 Runtime Exception 에러 출력
- 음수가 없으면 모든 값을 더한 뒤 결과 출력
그런 다음은 테스트 코드에 맞춰 프로덕션 코드를 아래와 같이 작성해보았다.
package step1;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class StringAddCalculator {
public static int splitAndSum(String o) {
if (!validInputCheck(o)) {
return 0;
}
Matcher m = Pattern.compile("//(.)\n(.*)").matcher(o);
if (m.find()) {
String customDelimiter = m.group(1);
String[] tokens= m.group(2).split(customDelimiter);
// 덧셈 구현
return sumStringAry(tokens);
}
return sumStringAry(splitInputString(o));
}
private static String[] splitInputString(String o) {
return o.split(",|:");
}
private static int sumStringAry(String[] stringAry) {
int totalRes = 0;
for (String s: stringAry) {
int i = Integer.parseInt(s);
validIntegerValueCheck(i);
totalRes += i;
}
return totalRes;
}
private static void validIntegerValueCheck(int i) {
if (i < 0) {
throw new UserCustomException("입력 값은 0보다 커야 합니다.");
}
}
private static boolean validInputCheck(String o) {
if (o == null || o.isEmpty()) {
return false;
}
return true;
}
}
class UserCustomException extends RuntimeException {
public UserCustomException(String message) {
super(message);
}
}
이렇게 테스트 코드를 기반하여 코드를 작성하니 뭔가 새로운 느낌이었다.
첫번째로는 실시간으로 코드를 테스를 하면서 작성하니 너무 즐거웠다. 어떤 부분이 부족한지 실시간으로 알 수 있으며 해야할 일이 정해져 있으니 좀 더 타이트하게 코드를 작성할 수 있었다.
두번째로는 모든 테스트를 통과한 것을 보니 뭔가 내 코드에 의구심이 사라졌다.
매번 코드를 다 작성하고, 제대로 코드를 짠 게 맞는지 의심만 했었는데 모든 코드를 테스트 한 것을 보니 그런 걱정을 할 필요가 없어졌다.
결과적으로 테스트 기반으로 하여 코드를 작성하는 것이 많은 부분에서 도움이 된 다는 것은 얼추(아직 완벽하게 체감한 상태는 아닌 것 같다.) 느끼고 있으나, 이러한 테스트 코드를 자연스럽게 내가 작성하는 장면이 아직은 상상이 안가는게 현실이다.
이번 로또 프로젝트를 끝마칠 때 쯤은 자연스럽게 테스트에 기반하여 코드를 짤 수 있는 역량을 갖추기를 바란다. 그리고 그렇게 되는 것을 목표로 해야겠다.
천천히, 그리고 꾸준히
'강의 > TDD, Clean Code with Java 12기' 카테고리의 다른 글
[로또] step2 - 로또(자동) (18일차) (0) | 2021.08.06 |
---|---|
[로또] step2 - 로또(자동) (17일차) (0) | 2021.08.05 |
Step5 - 자동차 경주(우승자) - 피드백 (15일차) (0) | 2021.08.03 |
Step5 - 자동차 경주(우승자) (14일차) (0) | 2021.08.03 |
Step4 - 자동차 경주(우승자) - 피드백 반영 (11일차) (0) | 2021.07.29 |