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

[우아한테크코스 8기] 프리코스 오픈미션 3차 회고- 로또 TDD

softmoca__ 2025. 11. 19. 11:27
목차

 

 

 

 

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

 

GitHub - softmoca/java-lotto-8

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

github.com

 

 

시작하며

드디어 처음 나에게 TDD를 경험해 보고 싶게 만든 3주차 로또 미션을 마쳤다.

벌써 3번째 사용해 보는데 사용할수록 너무 TDD의 장점을 크게 느낀다.

무엇보다 요구사항 탐색과 리팩토링 안정망 도구로서의 TDD의 힘이 가장 잘 체감 된다.

그에비해 설계 피드백 도구로서의 TDD의 장점은 온전히 느끼고 있지 못한것 같다. 2주차 회고에서도 느꼈지만 이러한 OOP적인 설계를 녹여내기 위해서는 책한권, 이론 한줄 안은것이 아닌 무엇보다 경험이 가장 중요한것 같다. 아주 작고 사소하지만, 혹은 잘못되었을지 모르지만 그래도 직접 부딪혀보며 확실히 이런 경험치가 싸여 성장하고 있음이 느껴져서 너무 좋다.

 

“요구사항 탐색 & 설계 피드백 도구로서의 TD”

Rank를 대할 때 의식적으로 더 신경을 쓴 만큼 더욱 직접으로 TDD의 위력을 체감할 수 있었다.

처음에는 “일치 개수 + 보너스 여부로 등수를 구한다”는 단순한 이해로 출발했지만, 테스트를 작성하면서 요구사항이 훨씬 구체적으로 드러났다. 특히 Rank.of(matchCount, hasBonus) 형태의 테스트를 작성하면서 등수는 단순한 조건 분기가 아니라, 각 등수가 스스로 자신의 조건을 판단해야 한다는 설계 방향으로 자연스럽게 흘러갔다.

이 과정에서 Rank를 enum으로 정의하고, 각 등수에 일치 개수, 보너스 필요 여부, 상금을 가지게 하며 책임을 응집시키는 구조로 개선할 수 있었다.

또한 “낙첨은 어떻게 표현하지?”라는 질문에서 null이나 플래그가 아닌 Optional<Rank>를 사용하기로 결정했는데, 이는 단순한 구현 선택이 아니라 테스트 작성 과정에서 ‘결과가 없을 수 있음’을 명확히 인식했기 때문이었다. 보너스 번호 로직에서 처음에는 “보너스가 없으면 탈락”이라는 잘못된 가정을 하고 구현하려 했지만, 테스트를 통해 “1등, 3등, 4등, 5등은 보너스와 무관하게 결정된다” 라는 실제 요구사항을 다시 정확히 이해하게 되었고,이 덕분에 3주차 로또 미션에서 겪었던 동일한 유형의 버그를 사전에 차단할 수 있었다.

또 다른 고민은 로또 번호의 정렬 책임을 어디에 둘지였다.

출력 시 정렬할지, 아니면 Lotto 생성 시 정렬할지를 고민했는데, 테스트를 작성하며 “Lotto는 어디에서든 사용될 수 있는 도메인 객체”라는 점을 다시 생각하게 되었고, 결국 생성 시점에 정렬하여 불변 조건을 보장하는 방향으로 설계를 선택했다.

이 경험을 통해 단순히 기능을 구현하는 것이 아니라, 테스트를 통해 요구사항을 더 정확히 이해하고, 그 이해를 바탕으로 설계를 결정하는 경험을 하게 되었다.

TDD는 단순한 개발 기법이 아니라, 내가 무엇을 만들고 있는지를 끊임없이 되묻는 사고 도구라는 사실을 체감한 순간이었다. 과거에 1차원적으로 출력의 책임으로만 보았던 부분을 TDD를 통해 더 깊게 고민하며 지난 결정과 다른 결론이 나온점 또한 인상 깊었다.

무엇보다 당장의 시야에서 보이는 근거가 확실했으며 TDD라는 점진적으로 개선할수 있는 시스템과 함께여서 이전 트레이드오프에 대해 여러 고민하는 과정으로 혼란스러움이 없다는 점이 너무 크게 체감 되었다.

 

 

“설계 피드백 도구 & 리팩토링 안전망으로서의 TDD”

3주차 프리코스에서는 Lotto의 필드가 수정할수 없어서 아쉬웠다. 이 부분을 만질수 있다면 유기적으로 많은 이점을 가질수 있을것 같다는 생각했기 떄문이다. 그리고 TDD를 진행하며 이런 부분을 개발 초기부터 온전히 고려하게 되고 과거의 나의 생각이 맞았음을 느끼게되어 뿌듯했다.

처음Lotto를 개발할 때, 숫자 범위 검증을 추가하기 위해 먼저 테스트를 작성했다.

처음에는 Lotto 내부에서 직접 1~45 검증 로직을 구현했지만,리팩토링 단계에서 이미 LottoNumber에 동일한 검증 로직이 존재한다는 사실을 발견했다.

이때 고민은 “이미 있는 책임을 다시 가져올 것인가, 아니면 재사용할 것인가”였다.

그리고 중복된 검증 로직이 두 곳에 분산되는 것보다, Lotto가 List<Integer>가 아닌 List<LottoNumber>를 보유하도록 설계를 변경하는 쪽이 더 옳다고 판단했다.

이렇게 변경하면 로또 번호의 모든 검증 책임이 LottoNumber에 모이게 되고, Lotto는 조합 역할에만 집중할 수 있었다.이 과정에서 가장 인상 깊었던 점은, 테스트가 이미 존재했기 때문에 기존 동작을 두려움 없이 유지한 채 리팩토링을 시도할 수 있었다는 점이다.

TDD가 없었다면, 중복을 발견하더라도 “괜히 건드렸다가 깨질까 봐” 그대로 두었을 가능성이 높았을 것이다.

또한 WinningLotto를 설계하는 과정에서도 TDD가 큰 역할을 했다.

처음에는 당첨 번호와 로또를 비교해 등수를 반환하는 기능을 어떻게 설계할지 막막했지만,테스트를 먼저 작성하면서 자연스럽게 match(Lotto)라는 메시지가 드러났고, 우승 번호 객체가 로또를 검사하고 Rank를 반환하는 구조로 정리할 수 있었다.

이때 중요한 고민은 “WinningLotto가 직접 모든 계산을 할 것인가, 기존 객체들에게 역할을 나눠줄 것인가”였다. 결론적으로 WinningLotto는 조정자 역할만 수행하고,번호 비교는 Lotto, 등수 판단은 Rank에게 위임하는 구조가 더 객체지향적이라고 판단했다.

이 경험을 통해 객체를 잘 만들어두면, 나중에는 새로운 기능을 추가할 때 “조합만” 하면 된다는 말을 실제로 체감할 수 있었다. 그리고 그 과정 전체를 가능하게 해 준 것은 리팩토링에도 흔들리지 않는 테스트라는 안전망이었다. 무엇보다 2주차 OOP와 TDD 사이의 균형을 잡지못한 점이 큰 실책이있지만 이번 경험은 이전 경험들이 좋은 성장 발판으로 되돌아와 좋은 성장을 할수 있었다 !

“Service 계층 도입과 철회 - 과도한 설계의 발견”

DTO를 도입한 이후에도 컨트롤러가 여전히 무겁다는 느낌이 강하게 들었다.

특히 LottoStatistics를 LottoResult로 변환하는 책임이 컨트롤러에 있는 것이 맞는지 계속 의문이 들었고, “Domain → DTO 변환은 어디서 책임져야 할까?”라는 고민에서 Service 계층을 도입하게 되었다.

LottoResultService를 만들면서 도메인과 DTO 간 역할 분리가 깔끔해지고, 구조도 더 계층적으로 보이게 되었지만 이후 다시 코드를 보며 스스로에게 질문하게 되었다.

 

“지금 이 프로젝트에서 이게 정말 Service 계층이 맞는가?”

현재 프로젝트는 DB도 없고, 외부 시스템도 없고, 단순 콘솔 프로그램이었다.

Service가 실제로 하는 일은 비즈니스 조율이 아니라 DTO 변환과 수익률 계산 정도에 불과했다. 이건 ‘비즈니스 흐름 관리자’가 아니라 사실상 Mapper에 가까운 역할이라는 걸 인식하게 되었다.

결국 나는 미래를 대비하겠다는 명분으로 현재 필요 없는 구조를 미리 만들어버린 것이라는 결론에 도달했고,

Service 계층을 제거하고 해당 책임을 다시 구조적으로 자연스러운 위치로 되돌리는 결정을 했다.

이 경험을 통해 “계층 분리는 많이 할수록 좋다”가 아니라 “프로젝트 규모와 복잡도에 맞는 수준의 분리만이 좋은 설계다”라는 걸 체감했고,TDD의 핵심 원칙인 YAGNI(지금 필요 없으면 만들지 않는다)를 이론이 아니라 실제 코드로 경험할 수 있었다. Service를 도입했다가 철회한 과정 자체가 내가 과도한 설계의 유혹을 직접 경험하고, 그 안에서 균형점을 찾아갔던 중요한 학습 포인트였다.

또한 3주차와 달리 서비스 계층에 대해 고민하는 과정과 도입하고 제거하는 과정에서 TDD 사이클 덕분에 훨씬 수월하게 고민하고 리팩토링을 할수 있다는 점을 느껴 너무 좋았다.

 

 

마치며

처음 나를 TDD로 끌어당겼던 바로 그 3주차 로또 미션을, 이제는 TDD로 다시 끝까지 완주했다는 사실 자체가 이번 여정의 의미를 잘 보여주는 것 같다. 예전에는 제출 전날 보너스 번호 버그를 뒤늦게 발견하고, 요구사항을 놓친 채 설계 고민에만 빠져 있었지만, 이번에는 같은 미션을 “테스트를 먼저 두고, 설계와 구현을 그 위에 쌓아 올리는 방식”으로 다시 걸어갔다.

선정 배경에서 내가 세웠던 질문들  “왜 테스트를 했는데도 버그가 늦게 발견됐을까?”, “왜 리팩토링이 이렇게 불안했을까?”, “왜 테스트가 설계를 도와주지 못했을까?” 모두 이번 1~3차 TDD 회고와 3주차 로또 미션의 경험들은 그 질문들에 대한 부분적인, 그러나 분명한 답을 보여줬다고 느낀다. 처음 세웠던 목표들 역시 하나씩 몸으로 확인해 가는 과정이었다.

물론, 여전히 나는 “설계 피드백 도구로서의 TDD”를 완전히 자기 것으로 만들었다고 말하긴 어려운것 같다 분명히 너무나도 해당 도구를 잘 모쓰고 있는것 같은 느낌이 든다 
책 한 권 읽고, 이론 한 줄 안다고 되는 게 아니라는 것도 충분히 느꼈다.
하지만 분명한 건, 이제 테스트는 단순히 정답을 확인하는 마지막 체크리스트가 아니라, 요구사항을 더 정확히 보게 만들고, 잘못된 가정을 조기에 드러내 주고, 설계를 조금 더 나은 방향으로 밀어 올려 주고, 리팩토링을 겁내지 않게 해 주는 그런 “생각의 프레임”이 되기 시작했다는 점이다 !!

이번 3주차 로또 미션 TDD 경험은 내가 세웠던 목표들 오해를 깨고, 진실을 체득하고 싶다에 완벽히 도달한 것은 아니지만, 적어도 그 방향이 어디인지는 몸으로 알게 된 단계라고 느낀다.

이제 남은 건, 더 많은 미션, 더 다양한 도메인, 더 복잡한 현실 속에서 이 기준을 계속 확인하고, 부수고, 다시 세워 가는 일일 것이다.

그리고 그 과정을, 나는 앞으로도 테스트 코드 위에 하나씩 기록해 나갈 생각이다 !

자 이제 마지막 대망의 요구사항 폭탄의 편의점 미션만 남았다..! 작년 그토록 복잡하고 어려워 많이 갈아엎으며 고통을 받았던 경험들이 TDD와 함께라면 또 어떤 경험을 줄지 너무 기대된다 !