Java 클린 코드와 TDD 회고: StringCalculator 리팩토링 여정

2026. 2. 17.
JavaTDDClean CodeJUnitRefactoringWoowahan Tech Course

1. 우아한테크코스 참여 및 규칙 수립

이번 문자열 계산기 개발은 우아한테크코스(우테코) 과정의 일환으로 진행되었습니다. 단순한 기능 구현을 넘어, 팀(또는 미래의 나)과의 협업을 고려한 컨벤션 수립부터 시작했습니다.

  • 테스트 메소드명: 가독성에 대한 고민 끝에 결국 자바 표준과의 일관성을 위해 카멜 케이스(camelCase)로 통일하기로 결정했습니다.
  • 커밋 메시지: 도구 호환성을 위해 타입과 스코프는 영문으로 유지하되, 주된 소통 대상이 한국인임을 고려하여 제목과 본문은 한국어로 작성하기로 규칙을 정했습니다. (예: feat(calculator): 쉼표 구분자 덧셈 기능 추가)
  • 요구사항 정의: '숫자' 범위를 정수로 명확히 정의하고, 소수점 처리는 추후 확장 기능으로 판단하여 현재 범위에서 제외하여 스코프를 좁혔습니다.

2. TDD와 설계에 대한 깊은 고찰

이번 프로젝트에서 가장 큰 수확은 설계와 테스트의 관계에 대한 깨달음입니다.

고전파 TDD의 한계와 런던파 TDD의 필요성

처음에는 StringCalculator라는 공개된 창구(Public API)만 테스트하는 이른바 '고전파 TDD' 방식으로 접근했습니다. 이 방식은 내부 구현을 유연하게 바꿀 수 있다는 장점이 있었지만, 내부 부품인 ParserNumbers의 동작을 간접적으로만 검증한다는 명확한 한계를 느꼈습니다. 이를 통해 각 클래스를 철저히 고립시켜 모의 객체(Mock) 등을 활용해 단위 테스트를 촘촘히 작성하는 '런던파 TDD' 방식의 장점과 필요성을 깊이 고민하는 계기가 되었습니다.

테스트 용이성을 위한 구조 변경 설계

설계를 진행하다 보니, 테스트 용이성을 위해 객체의 책임을 재분배하는 경험을 했습니다. 초기 설계에서는 ParserNumbers 객체를 직접 생성하여 반환했습니다. 하지만 이로 인해 Parser의 테스트가 Numbers 클래스의 구현에 강하게 의존하게 되는 문제를 발견했습니다.

이를 해결하기 위해, 테스트의 독립성을 높이도록 설계를 변경했습니다. Parser는 순수하게 파싱된 문자열 배열(String[])만 반환하도록 책임을 축소시키고, 객체 생성의 책임은 상위인 StringCalculator가 갖도록 변경했습니다. 이 과정을 통해 **"초기 설계는 절대적인 것이 아니며, 테스트와 구현 사이에서 발견되는 피드백을 통해 끊임없이 리팩토링해야 한다"**는 클린 코드의 핵심 원칙을 체험했습니다.


3. Java 언어 및 구동 환경에 대한 학습

TDD 외에도 Java라는 언어 본연의 동작 방식에 대해 몇 가지 깊이 있는 학습을 할 수 있었습니다.

컬렉션 변환에 대한 재고

String.split()의 결과물은 크기가 고정된 자바 배열(String[])입니다. 이후 파싱된 데이터의 추가나 삭제가 일어나지 않는다면 굳이 List로 감싸 변환하기보다 배열 구조를 그대로 사용하는 것이 메모리와 코드 가독성 측면에서 더 간결함을 이해했습니다.

컴파일러와 정규식의 '이중 이스케이프'

정규표현식 엔진을 위한 이스케이프(\)와 자바 컴파일러를 위한 이스케이프(\)가 중첩되는 문제를 겪었습니다. 예를 들어 숫자만을 매칭하려는 \d를 자바 문자열로 작성할 때는 결국 컴파일러를 거치며 \\d로 쓰여야 하는 이 '이중 이스케이프'의 원리를 체득했습니다. 같은 맥락에서 소스 코드 내부의 \n과 유저가 콘솔에 입력하는 \n이 다르게 파싱되는 현상도 자바 컴파일러의 해석 과정 개입 유무라는 것을 명확히 이해했습니다.

JUnit 환경과 공유 자원(Static)의 생명주기

가장 까다로웠던 트러블슈팅은 Scanner와 같은 static 공유 자원 문제였습니다. 프로그램 종료 시 운영체제(OS)가 모든 자원을 일괄 정리해 주는 일반적인 애플리케이션 실행 환경과 달리, 단일 JVM 프로세스 안에서 여러 테스트가 순차적으로 실행되는 JUnit 환경에서는 static 자원이 계속 살아남습니다. 따라서 각 테스트(또는 @AfterEach) 후에 명시적으로 close()를 호출하여 자원을 정리해주지 않으면 테스트 간 간섭(Test Pollution)이 발생한다는 중요한 사실을 깨달았습니다.


마무리

단순 계산기 구현처럼 보이는 작은 미션이었지만, 그 안에서 TDD의 방법론, 객체 파악과 책임 할당, 그리고 Java 컴파일러 및 JVM 특성까지 매우 밀도 있게 학습할 수 있었습니다. 앞으로의 프로젝트에서도 이 교훈을 바탕으로 더 단단한 코드를 유연하게 짜보고자 합니다.