외부활동/우아한테크코스 [프리코스]

[우아한테크코스 8기] 프리코스 오픈미션 1차 회고- 문자열계산기 TDD

softmoca__ 2025. 11. 9. 22:18
목차

 

 

 

 

https://github.com/softmoca/java-calculator-8/tree/softmoca-tdd

 

GitHub - softmoca/java-calculator-8

Contribute to softmoca/java-calculator-8 development by creating an account on GitHub.

github.com

 

 

시작하며

드디어 첫 TDD 경험을 마쳤다 ! 처음 예상했던 것 보다 훨씬 시간과 리소스가 많이 소요 되었다.

처음 시도를 하려고하니 여러 레퍼런스와 도서에 읽은 너무 많은 방법들이 떠오르며 중간중간 각 레퍼런스마다 모순되는 내용들이 떠올라 상당히 혼란스러웠다. TDD에서의 요구 사항 분석 및 문서화, 리드미 작성, 커밋 방법, 프리코스 미션의 요구 사항들 등등 모두 고려 하다 보니 실직적으로 내가 현재 해당 프로젝트를 왜 진행하는지 방향성을 잃기도 했다.

그렇게 여러번 프로젝트를 뒤엎던 중 다시금 목표를 명확히 세워 정리하고, 세부적인 모든 규칙과 기록을 제외 하고 온전히 ‘ TDD를 체득하며 격는 여러 경험’에 몰입하기로 방향을 정했다.

 

 

“ 요구사항 탐색 도구로서의 TDD”

단위 테스트는 모두 통과했고, 커스텀 구분자 파싱 로직도 완벽하게 작동했다. 하지만 ApplicationTest를 실행하니 실패했다. 해당 테스트를 TDD구현 첫 사이클로 생각하며 원인을 파악했고 초기 요구사항을 제대로 파악하지 못했음을 확인했다. 환경 차이를 가볍게 여겨 \n 개행문자와 콘솔 입력의 \n의 차이를 대충 이해하고 인식 조차 못한것이 문제였다 !

미션 1주차 부터 바로 ‘요구사항 탐색 도구로서의 TDD’를 제대로 경험했다 !

이 경험을 통해 여러 가지를 느꼈다.

우선, TDD 사이클을 반복하는 과정 자체가 요구사항을 놓치지 않도록 나를 계속해서 되묻게 만들고, 단순히 “읽고 이해하는 것”이 아니라 시스템적으로, 강제적으로 이해하게 만든다는 점이 인상 깊었다.

또한 TDD는 단순히 만들면서 테스트하는 것이 아니라, 테스트하면서 요구사항을 발견하는 과정이라는 사실을 체감했다.

만약 구현을 먼저 끝내고 테스트를 작성했다면 이번에 발견한 버그는 훨씬 늦게 알아차렸을 것이고,

수정에는 지금보다 훨씬 많은 시간이 들었을 것이다.

작은 단위로 검증하며 나아가기 때문에 큰 실수를 조기에 발견할 수 있다는 점도 크게 와닿았다.

전체 기능을 모두 구현한 뒤 테스트를 했다면 어디서 문제가 발생했는지 파악하는 데 지금보다 몇 배의 시간이 더 걸렸을 것이라고 느꼈다.

마지막으로 “이렇게 입력하겠지”라는 내 추측이 아니라, “실제로 확인해보자”는 태도가 필요하다는 것을 깨달은 점이 가장 인상 깊다 !

TDD는 나의 막연한 가정을 줄이고, 확실한 검증 위에서 구현하도록 나를 훈련시키는 도구였다 !!

이전 3주차 로또 미션 당시 보너스 번호 매칭 로직 버그를 제출 전날 발견하며 상당히 혼란스러워하며 디버깅에 많은 시간을 썼다. 구현을 먼저 하고 테스트를 나중에 작성했기 때문에 요구사항 누락을 뒤늦게 발견할 수 밖에 없었다. 하지만 TDD로 작은 단위씩 검증하며 진행했기에 이스케이프 시퀀스 문제를 조기에 발견할 수 있었다. 통합 테스트 실행 직후 바로 문제를 인지했고, 범위가 좁았기 때문에 원인 파악도 빨랐다 !

 

“리팩토링 안전망으로서의 TDD”

코드를 작성하던 중 Tell, Don’t Ask 원칙을 위반한 점을 발견했다.

이전에는 작동에는 문제 없으니 한번에 싹 리팩토링을 진행하자 라고 지나쳤을 상황이지만 이번엔 달랐다.

구현 과정 중 이미 살아있는 테스트가 있기 때문에, “테스트가 있으니 고쳐도 안전하다”는 확신이 들었고, 그것이 리팩토링을 시도할 수 있는 용기가 되었다.

“StringCalculator 입장에서 Expression을 어떻게 쓰고 싶은가?”, “Expression에게 어떤 책임을 위임하고 싶은가?”를 기준으로 고민했고, 결국 expression.toNumbers()라는 메서드로 역할을 이동시키는 설계를 떠올렸다. 내부 구현은 Expression이 책임지고, StringCalculator는 결과만 사용하도록 구조를 변경하고 싶었다.

곧바로 구현하지 않고, 먼저 이 사용 방식을 검증하는 테스트를 작성했다.

StringCalculator가 Expression의 내부를 알 필요 없이 사용하도록 expression.toNumbers()로 책임을 이동시키는 구조를 테스트부터 설계했고, TDD 사이클을 통해 점진적으로 리팩토링을 진행했다. 코드 구조는 더 단순해졌고, 객체 간 의존성도 줄어들어 객체의 책임과 캡슐화도 자연스럽게 개선되었다.

변경 후 테스트를 실행했을 때 모든 테스트가 통과하는 순간, “사용처를 바꿨는데도 기존 테스트가 모두 통과한다”는 사실이 큰 안정감을 주었다.

이 경험은 로또 미션에서 Service 계층을 도입하기 위해 여러 리팩토링을 하는 과정에서 엄청난 피로를 느낀 기억과 강하게 대비 되었다.

당시에는 구현 후에 작성한 테스트가 구조에 종속되어 있었기 때문에 한곳을 바꾸면 여기 저기 깨지는 곳이 많아 불안과 함께 상당한 수정을 하며 스트레스를 받았었다

반면 이번에는 TDD 방식으로 행위 중심 테스트를 먼저 작성했기 때문에, 구현을 바꿔도 테스트가 무너지지 않았고, 오히려 구조 변경의 안전망 역할을 해주었다.

이 경험을 통해 TDD 사이클 안에는 리팩토링이 자연스럽고 강제적으로 포함되어 있다는 중요성을 체감하게 되었다. 리팩토링은 선택이 아니라 필수였고, 테스트와 프로덕션 코드를 동시에 발전시키다 보니 테스트에 대한 신뢰가 생겼으며, 그 신뢰가 리팩토링에 대한 두려움을 없애주었다.

이제 리팩토링은 더 이상 불안한 작업이 아니라, 테스트를 통해 검증 가능한 실험이 되었고, 수정의 족쇄가 아니라 구조 변경을 가능하게 하는 진정한 안전망이 되었다.

무엇보다 인상 깊었던 점은, 테스트가 코드의 정확성뿐 아니라 개발자로서의 심리적 안정까지 제공해준다는 것이었다. 생각보다 이런 심리적인 이점이 너무나도 크게 와닿았다 !

 

“설계 피드백 도구로서의 TDD”

TDD에서의 테스트는 단순한 검증 도구가 아니라 설계에 대한 즉각적인 피드백 도구가 된다는 점을 느꼈다.

커스텀 구분자 파싱 테스트를 작성하며 Delimiter의 동작을 검증 할때 예상과 달리 서로 다른 객체로서 인식되며 실패 했다.

이전 같았으면 Delimiter 클래스를 완성한 뒤 당시의 특정 상황에서 오케이 잘 되네 하고 넘어가면서 equals()의 필요성 조차 인식하지 못했을 것이다.

하지만 TDD로 테스트를 작성하는 순간 “비교가 필요한 상황”에 즉시 마주했고, 테스트 실패를 통해 설계의 빈틈을 명확하게 발견했다 !

과거에는 객체의 역할과 책임만을 머릿속으로만 고민하며 추상적인 수준에서 설계를 시도했다면, TDD에서는 테스트를 먼저 작성하면서 “이 객체를 어떻게 사용하게 될까?”, “실제로 비교가 필요하네?” 라는 식으로 설계 요구가 자연스럽게 코드 레벨에서 드러났다.

설계가 머리에서 시작되는 것이 아니라, 사용 시나리오를 표현한 테스트 코드로부터 도출되는 과정을 처음으로 체감한 것이다.

이 깨달음이 머릿속 설계 고민이 아니라, 테스트 코드를 작성하는 과정에서 자연스럽게 떠올랐다는 점이 너무 신기했다. 내가 설계를 끌어낸 것이 아니라, 테스트가 설계를 끌어올려 줬다는 느낌을 처음으로 느꼈다 !

결과적으로 이 경험을 통해 테스트는 버그를 사후에 잡는 도구만이 아니라, 좋은 설계를 사 끌어낼수도 있는 도구라는 점을 경험했다. equals() 구현이라는 작은 사례였지만, 이 경험 하나만으로도 TDD가 설계를 어떻게 바꾸는지, 그리고 왜 테스트를 “설계 피드백 도구”라고 부르는지 확실히 느낄 수 있었다.

“TDD 과정에서의 Getter 사용 vs 행위 테스트”

TDD를 적용하면서 가장 크게 고민한 점은 Getter를 이용한 상태 검증과 행위 검증 사이의 선택이었다.

초기에는 Expression의 파싱 결과를 확인하기 위해 내부 값을 그대로 검증하는 테스트를 작성했다. 그 순간에는 매우 합리적인 선택처럼 느껴졌다.

하지만 이후 필드 이름을 조금 더 명확하게 바꾸고자 했을 뿐인데, 테스트가 모두 깨졌고, getter 이름을 어떻게 할지에 대한 고민까지 이어졌다. 필드명과 getter명을 맞추기 위해 getRawNumbers()로 바꾸자니 모든 테스트를 수정해야 했고, 기존 이름을 유지하자니 코드의 일관성이 깨졌다.

“필드명 하나 바꾸는데 테스트를 이렇게 많이 고쳐야 하는 게 맞나? 내가 테스트를 위해 내부 구조를 고정시키고 있는 건 아닐까?” 또한 Expression.from()에서 trim() 같은 전처리를 추가하며 내부 구현을 조금 개선했을 뿐인데, 기존 상태 검증 테스트들이 다시 깨졌다.

이떄 명확히 이 테스트는 Expression의 행위를 검증하는 게 아니라, 내부 구현 방식에 매달리고 있었다는걸 꺠달았다.

Expression의 진짜 목적은 내부 문자열을 저장하는 것이 아니라, 숫자들을 추출해서 Numbers 객체로 만드는 것이었다. 그래서 더 이상 내부 상태를 검증하지 않고, 최종 행위인 toNumbers()의 결과를 검증하는 테스트로 변경했다.

이 변화를 통해 “무엇(What)과 어떻게(How)”를 구분하는 감각이 확실히 생겼다. 이전 테스트는 “어떻게 저장했는가?”를 검증하는 테스트였고, 구현 세부사항에 강하게 의존하고 있었으며, 리팩토링에 극도로 취약했다. 반면 변경된 테스트는 “무엇을 만들어내는가?”를 검증하는 테스트였고, 구현 방식이 바뀌어도 테스트는 그대로 유지되었으며, 캡슐화도 자연스럽게 보장되었다. 이 경험을 통해 잘못된 테스트는 리팩토링을 가로막는 족쇄가 될 수 있고, 올바른 테스트는 리팩토링을 가능하게 하는 날개가 될 수 있다는 사실을 직접 체감했다.

“Expression 테스트에서 Numbers.from() 사용”

Expression 테스트에서 Numbers.from()을 이용해 기대값을 만들게 되면서, “이게 정말 순수한 단위 테스트가 맞나?”라는 의문이 들기 시작했다.

“만약 Numbers.from()에 버그가 있다면, expected 값과 actual 값이 동시에 잘못 만들어져서 테스트가 통과하는 건 아닐까?”, “Expression을 테스트하면서 Numbers에 의존하는 게 맞나?”, “Mock으로 Numbers를 대체해야 하는 거 아닐까?”와 같은 고민이 이어졌다.

여기서 최종적으로 선택한 방식은 고전파스타일의 접근이었다. Numbers는 외부 시스템이 아니라 같은 도메인 레이어의 값 객체이며, Expression의 협력 대상이 아니라 결과물에 가까운 객체라고 판단했다.

String이나 Integer를 테스트할 때 Mock으로 대체하지 않는 것처럼, Numbers 역시 실제 객체를 사용하는 것이 더 자연스럽다고 느꼈다.

또한 각 객체는 각자의 테스트에서 자신의 책임을 검증하고 있었기 때문에, 신뢰의 체인을 만들 수 있었다. NumbersTest에서 Numbers.from()의 정확성을 검증하고, 그 위에서 ExpressionTest는 “Expression이 올바른 Numbers를 생성하는가”에 집중하는 구조였다. 하위 테스트가 통과되면 상위 테스트에서 그것을 신뢰하고 사용하는 방식이 오히려 테스트를 더 단순하게 만들었다.

이 경험을 통해 모든 의존성을 무조건 Mock으로 격리해야 하는 것은 아니라는 사실도 다시금 깨달았다. DB, 외부 API, 랜덤 값, 시간 같은 외부 의존성은 Mock으로 제어해야 하지만, 도메인 객체는 빠르고 안정적이며 통제 가능하기 때문에 실제 객체를 사용하는 편이 오히려 유지보수성과 가독성 측면에서 유리했다.

중요한 것은 “무엇을 격리해야 하는가”를 기술이 아니라 설계 관점에서 판단하는 것이었다.

결과적으로 이 과정은 나에게 중요한 감각을 남겼다.

단위 테스트란 무조건 모든 의존성을 제거하는 것이 아니라, 테스트의 목적을 명확히 하고, 신뢰할 수 있는 범위 내에서 의존성을 관리하는 것이라는 사실이었다. 그리고 이 선택 역시, 이론이 아니라 실제 구현과 테스트를 하며 겪은 고민 속에서 체득했기 때문에 훨씬 오래 남을 것 같다고 느꼈다 !!

 

 

마치며

이번 첫 TDD 경험은 단순히 새로운 개발 방법론을 배운 것을 넘어, 개발을 바라보는 관점 자체가 달라지는 전환점이 되었다.
이전까지는 구현 후  테스트의 흐름에 익숙해 있었고, 테스트는 결과를 확인하는 도구라고 생각했지만, 이번 경험을 통해 테스트가 요구사항을 탐색하고, 설계를 이끌며, 리팩토링을 가능하게 하는 핵심 도구라는 것을 몸으로 느낄 수 있었다.

특히 인상 깊었던 것은 TDD가 단순히 코드를 안전하게 만들어주는 기술이 아니라, 개발자의 사고 방식과 태도를 변화시키는 훈련 도구라는 점이었다.
“이렇게 입력하겠지”라는 추측이 아니라, “실제로 어떤 일이 일어나는지 확인해보자”는 태도를 갖게 되었고, 불안 속에서 리팩토링을 미루는 개발자가 아니라, 테스트라는 안전망 위에서 의도적으로 구조를 개선하는 개발자로 한 발 더 나아갈 수 있었다.

물론 아직 TDD에 완전히 익숙해졌다고 말하기에는 부족함이 많다.
속도도 느렸고, 중간에 방향을 잃고 헤매기도 했으며, 여전히 매 순간이 선택의 연속이었다.

하지만 확실히 이번 경험을 통해 나는 더 이상 “TDD를 아는 개발자”가 아니라, TDD를 통해 고민하고, 흔들리고, 성장해 본 개발자가 되었다는 것이다.

앞으로도 이 경험을 발판 삼아, 2,3,4주차 미션들로 조금씩 더더 복잡한 도메인, 더 큰 시스템에서도 TDD를 나만의 도구로 쓰는 개발자로 성장해 나가고 싶다 !!