전에 만들었던 문자열 계산기를 JUnit 테스트로 검증하면서 요구사항대로 다시 구현해봤다.
막상 시작하려고 보니 TDD 방식은 처음이라 “어디서부터 손대야 하지?”가 제일 먼저 들었다.
찾아보니까 보통은 이런 흐름으로 간다고 한다.
요구사항을 테스트 코드로 먼저 고정해두고, 그 테스트를 통과시키는 방식으로 프로덕션 코드를 완성한다.
이게 좋은 이유가 명확했다. 그냥 머리로만 요구사항을 이해하고 구현하면, 중간에 꼭 하나씩 빠뜨리게 되는데, 테스트를 먼저 써두면 “내가 구현해야 할 범위”가 코드로 딱 고정된다.
- 요구사항이 문서가 아니라 실행 가능한 명세로 고정됨
- 빠뜨린 요구사항이 바로 테스트 실패로 드러남
- 나중에 리팩토링 해도 동작 보장이 됨
즉, “일단 구현하고 맞추기”가 아니라
요구사항 → 테스트로 고정 → 구현으로 통과
이 흐름으로 가는 거다.
요구사항
전달받은 문자열을 구분자로 분리한 뒤, 각 숫자의 합을 구해 반환해야 한다.
기본 구분자
- , 또는 : 를 구분자로 가지는 문자열을 전달하면 분리한 각 숫자의 합을 반환
- 예시
- "" ⇒ 0
- "1,2" ⇒ 3
- "1,2,3" ⇒ 6
- "1,2,:3" ⇒ 6 (중간에 빈 값이 생겨도 계산은 진행)
커스텀 구분자
- 문자열 앞부분의 "//" 와 "\\n" 사이 1글자를 커스텀 구분자로 사용
- 예: "//;\\n1;2;3" → 구분자 ;, 결과 6
음수 예외
- 음수가 포함되면 RuntimeException 계열로 예외 처리
- → 자바에서는 보통 IllegalArgumentException 사용
테스트 코드 먼저 작성
프로덕션 클래스 StringCalculator를 호출할 테스트부터 작성했다.
(TDD가 처음이라 몰랐지만, 요구사항 전체를 한 번에 쓰기보다, 대표 케이스부터 점진적으로 늘리는 것이 일반적이라고 한다.)
package jwp_2nd_onion.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import jwp_2nd_onion.src.StringCalculator;
public class StringCalculatorTest {
private StringCalculator strCal;
@BeforeEach
public void setup() {
strCal = new StringCalculator();
}
@Test
public void add() {
assertEquals(6, strCal.add("1,2,3"));
}
@Test
public void negativeThrows() {
assertThrows(IllegalArgumentException.class, () -> strCal.add("1,-2,3"));
}
}
왜 이렇게 썼냐면
1) @BeforeEach setup()
테스트마다 StringCalculator를 새로 만든다.
테스트는 서로 독립적이어야 하고, 이전 테스트의 상태가 다음 테스트에 영향 주면 그 순간부터 “신뢰할 수 없는 테스트”가 된다.
그래서 객체 생성은 @BeforeEach에서 처리해서 테스트 간 상태 공유를 원천 차단했다.
2) assertEquals
입력 "1,2,3"이면 결과 6이어야 한다.
이걸 코드로 박아두면 구현할 때 “내가 뭘 만족해야 하는지”가 흔들리지 않는다.
3) assertThrows
음수가 들어오면 예외가 터져야 한다는 요구사항도 코드로 고정한다.
이 시점에서는 프로덕션 코드가 없거나 미완성이니까 테스트는 당연히 실패한다.
근데 이게 TDD 흐름에서는 정상이다.
이제 목표는 하나: 테스트가 통과할 만큼만 프로덕션 코드를 구현한다.
프로덕션 코드 구현 흐름을 먼저 글로 정리
바로 코드 치려니까 머릿속에서 흐름이 꼬일 것 같았다.
특히 커스텀 구분자 처리랑 기본 구분자 처리가 섞이면, 구현하다가 분기 흐름이 지저분해질 것 같아서 아예 먼저 순서를 글로 적어놨다.
- 입력이 null 또는 빈 문자열이면 0 반환
- 커스텀 구분자 형식인지 검사
- 맞으면: 구분자 + 본문 분리
- 아니면: 기본 구분자 , 또는 : 사용
- 구분자로 본문을 split해서 토큰 배열 생성
- 토큰을 숫자로 변환하면서
- 빈 토큰은 무시
- 음수면 예외
- 누적 합 반환
이렇게 써두니까 “지금 뭐부터 처리해야 하지?”가 사라지고, 그대로 코딩만 하면 됐다.
프로덕션 코드 구현 (테스트 통과시키기)
1) 빈 입력 처리
if (text == null || text.isEmpty()) {
return 0;
}
- "" -> 0 요구사항 처리
- null도 같이 방어 (안전장치)
2) 커스텀 구분자 파싱 //(.)\\n(.*)
Pattern p = Pattern.compile("//(.)\\n(.*)");
Matcher m = p.matcher(text);
정규식 의미:
- // : 커스텀 구분자 시작
- (.) : 구분자 1글자 캡처 (여기가 m.group(1))
- \\n : 줄바꿈 기준
- (.*) : 나머지 본문 전체 (여기가 m.group(2))
커스텀 구분자일 때는
String customDelimiter = m.group(1);
tokens = m.group(2).split(customDelimiter);
이렇게 “구분자/본문을 분리”해서 본문만 split 한다.
예: "//;\\n1;2;3"
- m.group(1) -> ;
- m.group(2) -> "1;2;3"
3) 기본 구분자 처리
tokens = text.split(",|:");
,|:는 “, 또는 :” 둘 다 구분자로 쓰겠다는 뜻이다.
4) 빈 토큰 무시
if (token.isEmpty()) continue;
"1,2,:3" 같은 입력은 split 결과에 빈 문자열이 생길 수 있다.
요구사항에서 이 경우도 6이 나와야 하니까 빈 토큰은 스킵했다.
5) 음수 예외 처리
if (n < 0) {
throw new IllegalArgumentException("음수는 허용되지 않습니다: " + n);
}
요구사항에서 RuntimeException 계열이라고 했고, 자바에서는 보통 IllegalArgumentException을 쓴다.
테스트 → 구현 → 통과가 남긴 것
이번에 느낀 건
- 테스트가 요구사항을 실행 가능한 형태로 고정해줬고
- 구현은 그 테스트를 통과시키는 방향으로만 진행됐고
- 나중에 리팩토링해도 테스트가 안전망이 되어준다
그래서 최소한 이런 확신은 생겼다.
테스트가 통과하면, 요구사항을 만족하는 동작이 확보된 것이다.
최종 코드
package jwp_2nd_onion.src;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class StringCalculator {
public int add(String text) {
if (text == null || text.isEmpty()) {
return 0;
}
String[] tokens;
Pattern p = Pattern.compile("//(.)\n(.*)");
Matcher m = p.matcher(text);
if (m.find()) {
String customDelimiter = m.group(1);
tokens = m.group(2).split(customDelimiter);
} else {
tokens = text.split(",|:");
}
int sum = 0;
for (String token : tokens) {
if (token.isEmpty()) {
continue;
}
int n = Integer.parseInt(token);
if (n < 0) {
throw new IllegalArgumentException("음수는 허용되지 않습니다: " + n);
}
sum += n;
}
return sum;
}
}
출처 : 《자바 웹 프로그래밍 Next Step》, 박재성, 로드북
'Book > 자바 웹 프로그래밍 Next Step' 카테고리의 다른 글
| 3장_ 원격 서버 배포 전 준비 : UTF-8 세팅하기 (0) | 2026.01.28 |
|---|---|
| 3장_ 실습 환경 구축 (0) | 2026.01.28 |
| 2장_ 문자열 계산기 추가 요구사항 : 중복 제거, 읽기 좋은 코드를 구현하기 위한 리팩토링 (0) | 2026.01.27 |
| 2장_ 문자열 계산기 : main()으로 찍어보는 테스트, 왜 결국 막히는가? (0) | 2026.01.27 |
| 왜 이 책으로 시작했나 (0) | 2026.01.27 |
