



https://github.com/softmoca/java-convenience-store-7/tree/softmoca-tdd
GitHub - softmoca/java-convenience-store-7
Contribute to softmoca/java-convenience-store-7 development by creating an account on GitHub.
github.com
시작하며
드디어 마지막 편의점 미션을 TDD로 다시 경험해보았다.
요구사항 탐색 도구로서의 TDD를 활요하니 작년 그도록 어렵고 복잡했던 편의점 미션이 정말 놀라울 정도로 스무스하게 진행되었다. 물론 작년에 한번 경험했었기에 당연히 그보다 수월하게 느껴지는 것은 당연하겠지만 그런 사실을 감안하더라도 매 단계 점진적으로 진행을 할수록 너무 놀라웠다.
그간 TDD없이 무작정 꼼꼼히 분석하고 개발하기 위해 노력한 과거의 내가 얼마나 비효율적이었는지 몸소 느낄수 있어 충격적이었다.
TDD로 개발하니 상상이상으로 시간이 많이 걸려 전반적인 계획들이 딜레이되었지만, 그에 비해 내가 얻은것이 압도적으로 많았다. 지금 생각해보니 실제 전체적으로 보았을때 이런 TDD적인 방법이 정답이라 생각느꼈다. 그저 악으로 깡으로 꼼꼼히 개발하자라는 마음으로 개발하고 테스트를 한 뒤 이후의 에러와 버그를 꼼꼼히 잡는 것보다, 시스템적으로 자연스럽고 강제로 최대한 틈없이 단단하게 개발도록 하는것이 근본적으로 더 좋은 순서이고 맞는것 같다.
또한 4번의 TDD 미션을 거치며, 왜 TDD가 페어 프로그래밍과 궁합이 좋다고 하는지 조금은 알 것 같았다. 테스트를 설계하고, 작은 단위로 구현하고, 리팩토링하는 사이클 속에서 누군가와 함께 대화하며 고민했다면, 이 모든 장점이 훨씬 더 크게 증폭되었을 것 같다는 아쉬움이 자주 들었다.
한편으로로 장기 프로젝트에서 실제로 수정 사항이 계속 쌓이고, 이를 반영하며 유지보수하는 과정을 충분히 경험하지 못한 점도 아쉽다. 이번 오픈 미션을 통해 TDD의 “초기 개발 단계”는 온전히 체험했지만, TDD의 가장 큰 장점 중 하나인 “장기적인 유지보수와 변화 대응”을 몸으로 느껴보지 못했기 때문이다.
미션 마지막 시간을 보내며 이런 아쉬움이 남는 동시에, 본 교육 과정에서 페어 프로그래밍을 하며 TDD를 함께 적용하게 될 날을 상상하니 뭔가 가슴이 벅차며 더욱 나의 동기가 폭발하는것을 느꼈다.
꼭 합격해서 크루원으로서 많은 활동을 하며 이런 가슴벅함을 실제로 느끼고 싶다 !
“요구 사항 탐색 도구 로서의 TDD”
편의점 미션에서 프로모션 계산 로직을 TDD로 만들면서, 처음에는 “2+1이면 3개 사면 1개 공짜” 정도만 떠올렸고, 단순히 그 케이스만 통과하면 충분하다고 생각했다. 그런데 테스트 코드를 직접 작성하다 보니 자연스럽게 더 많은 질문이 생겼다.
“그럼 5개 사면 어떻게 되지?”, “6개에서는 몇 개가 무료로 나가야 하지?”, “7개면?”, 이런 애매한 케이스들이 테스트 메서드 이름을 짓는 순간부터 눈에 들어오기 시작했다.
테스트를 통해 5개, 6개, 7개 같은 케이스를 하나씩 적어보면서, 결국 “세트 단위(2+1이면 3개씩 끊어서)”로 계산해야 한다는 규칙을 스스로 발견했다. 만약 구현부터 시작했다면 “대충 나눠서” 때려 넣었을 가능성이 높았고, 나중에 엣지 케이스에서 이상한 값이 나와 디버깅에 시간을 썼을 것이다.
실제로 작년에는 이렇게 세부 사항에 대해 깊이 생각하지 않고 그저 그때 그때 로직에 맞게 동작하도록 구현했다.그리고 테스트를 돌린 제출 전날 부터 이런 부분 부분 구멍이 있던 로직들을 뜯어 고치느라 모든것이 꼬였었다. TDD는 “미리 구현한 로직에 테스트를 맞추는” 게 아니라, 테스트를 작성하면서 요구사항 자체를 다시 정의하게 만드는 도구라는 걸 체감했다. 프로모션 재고 부족 케이스와 멤버십 할인 로직에서도 반복됐다.
“프로모션 재고가 7개인데, 손님이 10개를 담으면 몇 개가 프로모션 적용이지?”라는 질문을 테스트 코드로 정면 돌파하게 됐다. 처음에는 7개 전부를 프로모션으로 볼지, 아니면 세트가 완성되는 6개까지만 볼지 헷갈렸는데, 테스트를 작성하며 요구사항 문구를 다시 읽어보니 “프로모션 재고 내에서만 세트 단위로 혜택을 준다”는 규칙이 자연스럽게 도출됐다.
그 결과, “6개(두 세트)는 프로모션, 나머지 4개는 정가”라는 구체적인 시나리오를 테스트로 못 박을 수 있었고, 구현도 그 테스트에 맞춰 명확하게 정리할 수 있었다. 처음엔 그냥 “총 금액의 30%를 할인해주면 되겠지?”라고 막연히 생각했는데, 프로모션과 멤버십을 동시에 생각하다 보니 질문이 꼬였다.
“프로모션으로 이미 할인된 금액에도 또 멤버십 할인을 얹어야 하나?”, “프로모션 미적용 금액만 기준으로 잡는 게 맞나?” 이 애매한 부분을 테스트에 명시하려고 하니 자연스럽게 규칙이 정리됐다.
그래서 에너지바처럼 프로모션이 없는 상품만 멤버십 할인 대상이 되고, 2+1으로 이미 혜택을 본 콜라는 제외하는 구조를 테스트로 먼저 못 박았다. 이 과정이 없었다면, 구현마다 기준이 조금씩 달라지고, 테스트도 케이스마다 설명할 수 없는 미묘한 차이가 생겼을 것 같다.
이 경험 전체를 통해 내가 머릿속으로 “이렇게 동작하겠지”라고 가정하던 부분들을, TDD는 “테스트로 먼저 적나라하게 드러내 보라”고 강제로 요구한다는 점이다 ! 테스트를 작성하는 순간, 모호한 요구사항이 구체적인 숫자, 케이스, 조건으로 내려오고, 그 과정에서 숨은 규칙들을 미리 발견할 수 있었다.
결국 TDD는 버그를 잡는 도구를 넘어서, 요구사항을 다시 읽게 만들고, 애매한 부분을 질문하게 만들고, 설계를 더 명확하게 만드는 “탐색 도구” 역할을 아주 제대로 해줬다 ! “이렇게 돌아가겠지”가 아니라, “그렇게 돌아가야 한다면, 테스트로 먼저 써보자”라는 태도를 몸으로 익힌 순간이었다.이 하나의 태도와 접근이 너무 큰 영향을 미친다는걸 작년의 경험들과 비교해보며 아주 뼈저리게 느꼈다 !
“설계 피드백 도구로서의 TDD”
Product와 Promotion의 관계를 설계하면서, 처음에는 단순히 “상품은 프로모션을 가진다”라고 가볍게 생각했다. 하지만 테스트를 먼저 작성해보니, 자연스럽게 “모든 상품이 프로모션을 가지는 건 아니지 않은가?”, “그럼 프로모션이 없는 상품은 어떻게 표현해야 하지?”, “상품이 프로모션을 아는 게 맞을까?”를 고민하게 되었다. 그래서 테스트에서 product.hasPromotion() 같은 메시지를 먼저 써보았고, 그 문장을 읽는 순간 “상품이 자기 프로모션 여부를 아는 구조가 자연스럽다”는 결론에 도달했다.
그 결과, Product가 Promotion을 Optional하게 소유하는 구조로 설계했고, 프로모션이 없는 상품도 깔끔하게 표현할 수 있었다. 이건 단순한 구조 선택이 아니라, 테스트가 요구하는 문장을 통해 도메인 관계를 다시 정리한 결과였다.
다음으로 고민이 된 건 “프로모션 계산은 누가 해야 하는가?”였다. 처음에는 Product가 계산하면 될 것 같았고, 그다음에는 Promotion이 하는 게 맞지 않나 싶었다. 하지만 테스트를 작성하다 보니, 이 계산은 단순히 “공짜 개수 계산”만이 아니라, 재고, 날짜, 추가 구매 제안, 정가 결제 여부까지 포함된 복합적인 행위였다 !
이걸 Product에게 넘기면 Product가 지나치게 많은 걸 알게 되고, Promotion에게 맡기면 프로모션이 상품 재고와 구매 흐름까지 침범하게 되는 문제가 있었다. 테스트에서 calculator.calculate(product, quantity, date)라는 형태의 메시지를 쓰게 되면서,자연스럽게 PurchaseCalculator라는 별도의 객체가 필요하다는 결론으로 이어졌다.
역시나 처음부터 계획한 게 아니라, 테스트 코드가 요구하는 메시지를 따라가다 보니 나온 결과였다.
그리고 계산 결과를 어떤 형태로 반환할지 고민하는 과정에서, payQuantity, freeQuantity만으로는 부족하다는 사실을 테스트 작성 중에 깨달았다. 추가 구매 제안 여부, 정가 결제 수량, 상태 메시지까지 필요해지면서, “이건 단순한 결과가 아니라 하나의 의미 있는 결과 객체다”라는 감각이 생겼다.
그래서 PurchaseResult라는 객체를 만들게 되었고, 필드가 점점 늘어나는 걸 보며 생성자의 파라미터가 감당하기 어려워지는 시점에 Builder 패턴을 도입해야겠다는 판단을 내리게 됐다.
이것도 설계 이론에서 출발한 게 아니라, 테스트를 쓰면서 “이 객체를 생성하기 너무 어렵다”는 불편함을 몸으로 느낀 결과였다. 설계가 어색할 때, 테스트가 그 불편함을 드러내준다는게 포인트였다 !
메시지를 쓰기 어려우면설계가 잘못됐다는 신호였고, 객체 생성이 복잡해지면, 역할이 제대로 정리되지 않았다는 신호였으며, 파라미터가 기하급수적으로 늘어나면, 책임이 과도하게 한 곳에 몰렸다는 신호였다.
그리고 그 신호들을 테스트 작성 과정에서 계속 마주했기 때문에, 나는 하나의 큰 설계를 한 번에 만드는 게 아니라, 테스트를 통해 문제를 인식하고, 그때그때 설계를 조정하는 소중한 경험을 했다.
“리팩토링 안전망으로서의 TDD”
미션 후반부에 크고 작은 리팩토링을 하는 과정, 특히 프로그래밍 요구사항을 반영하는 시간에 TDD의 힘을 또한번 제대로 느꼈다. 재고 차감 로직 처음 구현은 “프로모션 재고가 모자라면, 그냥 일반 재고에서 빼자” 정도의 단순한 if-else 로직이었는데, 테스트를 돌려보니 “프로모션 재고가 0이 되어야 하는 상황에서 3개가 그대로 남아 있는” 버그가 사전에 바로 드러났다.
머리로만 읽을 땐 당연해 보이던 코드였는데, 테스트를 통과시키려면 “프로모션 재고를 먼저 다 쓰고, 남은 수량을 일반 재고에서 빼야 한다”는 진짜 규칙대로 고쳐야 했다. 이때 느낀 건, “읽을 땐 맞아 보이는 코드도 테스트 앞에서는 솔직해진다”는 거였다. 가장 큰 산은 PurchaseCalculator의 대대적인 리팩토링이었다.
처음에는 하나의 메서드 안에 프로모션 여부 확인, 추가 구매 제안, 재고 부족 시 정가 결제, 정상 프로모션 적용이 줄줄이 이어진 100줄은 아득히 넘는 코드였다.
종종 읽을 때마다 여기선 뭐 하지 이 조건은 저기랑 겹치지 않나 라는 느낌이 들었지만, 이미 잘 돌아가고 있기도 했고, 건들면 여러 곳에서 문제가 생기지 않을까 하는 두려움도 꽤나 들었다. 하지만 TDD 과정의 살아있는 테스트 코드를 믿으며 테스트와 리팩토링을 반복하며 하나씩 잘 리팩토링을 마칠 수 있었다.
긴 메서드를 쪼깨고, 클래스를 새로 떠올리고, 이름을 바꾸고 책임을 옮길 때 “이렇게 바꿔도 될랑가”하는 질문에 답해준건 나의 감각과 근거없는 용기가 아니라 항상 똑같은 대답을 해주는 테스트 결과 색들이었다.
TDD 사이클이 주는 심리적 장점들이 역시나 너무 든든했다.
“View에서 Domain 참조 - DTO 도입 vs 직접 참조”
미션을 진행하며 늘 “View가 Domain을 어디까지 알아도 되는가?”,“이 상황에서 DTO를 도입하는 게 맞는가?”에 대해 고민했다. 직관적으로 이해할때는 전혀 문제가 없었지만 roduct.getPromotion().getName() 와 같은 체이닝들이 View에서 자주 보이며 계층 분리관점에 대해 자꾸만 의문이 남았고 DTO를 떠올렸다.
DTO를 도입하면 분명 장점은 있다. view는 DTO같은 단순한 구조만 알고, Domain이 바뀌더라도 “DTO 변환 로직”만 고치면 되기 때문이다. 하지만 3주차에 적용해본 경험과 함께 현재 상황에 대해 다시 고민해 보니 다른 결론에 도달했다. 어디까지나 단일 프로세스의 콘솔 프로그램이고, API도 없고, JSON 직렬화도 없고, 외부 시스템과의 통신도 없다. View가 하는 일은 “그냥 예쁘게 출력해주는 것”이 전부다 !
이 상황에서 DTO를 도입하면, DTO 클래스가 여러 개 생기고, DTO 변환 코드가 여기저기 생기고, 테스트 코드도 DTO를 추가로 다뤄야 하고, 정작 “도메인 변경 파급”은 지금 구조에서도 거의 크지 않다.
즉 현재 DTO는 실익보다 복잡도와 관리 비용이 훨씬 더 크기 때문에 최종적으로는 “DTO는 도입하지 않고, 대신 Domain을 안전하게 노출하는 방향”으로 결정했다. 조회 전용 getter는 허용하되, 컬렉션은 방어적 복사나 불변 리스트로 반환하고, 비즈니스 로직은 View로 끌어가지 않고 Domain 안에 두는 방식으로 정리했다.
즉, “View가 Domain을 읽는 것은 허용하지만, Domain의 로직을 빼내어 View에서 조합하지는 않는다”라는 기준을 세웠다.
“Getter를 지양하라”는 말은 “getter로 다 꺼내서 밖에서 처리하지 마라”에 더 가깝지, “조회용 Getter 자체를 죄악시하라”는 뜻은 아니라는 것 !
작은 콘솔 프로그램에서까지 DTO, 계층, 패턴을 전부 끌어오는 건 OOP 학습을 한다는 핑계로 스스로를 복잡도 지옥에 밀어 넣는 일일 수도 있다는 것 !
결국 설계는 “이 프로젝트의 크기와 목적에 비해 가치가 있냐?”로 판단해야 한다는 것 !
덕분에 그간 프리코스를 진행하며 고민한 여러 트레이드오프들과 YAGNI는 내가 합리화를 하고 있는게 아닐까 라는 추상적인 의문에 명확한 답을 내릴수 있어 너무 뿌듯했다!!
“더 나은 설계의 환상 - YAGNI와 TDD의 본질”
기능 구현을 마치고 리팩토링 사이클을 진행 하던 도중 나도 모르게 머릿속이 또 한 번 “더 나은 설계” 욕심으로 가득기 시작했다.
ConvenienceStore는 상품 저장소 역할을 하는 products 맵을 들고 있으면서, 구매 검증도 하고, PurchaseCalculator를 통해 계산도 위임하고, PurchaseContext도 생성·관리하는 등 겉으로 보기엔 책임이 굉장히 많아 보였다. “이건 도메인 객체인가? 애플리케이션 서비스인가? 아니면 그냥 저장소인가?” “책임이 너무 많은 거 아닌가? Repository랑 Service로 쪼개야 하는 거 아닌가?”
2주차에 컨트롤러 테스트를 위해 구조를 과하게 분리했던 경험과 3주차에 Service 계층을 도입했다가 다시 철회했던 경험이 있었지만 나는 또다시 “더 객체지향적으로 보이는 구조”를 만들고 싶은 마음에 끌리고 있었다. products를 DB로 옮길 수도 있으니 Repository 인터페이스를 도입할까? 컨트롤러를 더 가볍게 만들기 위해 PurchaseService를 빼야 하나? PurchaseContext는 엔티티 같은데, 이렇게 컨트롤러에서 막 수정해도 되나? 와 같은 생각이 들었다. 또한 Repository를 도입하면 책임 분리가 명확해지고, Service가 있으면 계층 구조가 더 예뻐지고, Context를 더 철저하게 감추면 “더 캡슐화된 느낌”이 난다고 생각했다.
하지만 고민을 할수록 점차 다시 머리가 복잡해져 상황을 정리하는 과정에서 이전 경험들이 다시금 떠올랐다.
컨트롤러 테스트를 위해 수많은 Mock과 인터페이스를 도입했지만, 실제로 얻는 가치에 비해 유지 비용이 훨씬 더 컸던 경험과 서비스를 도입했지만 결국 그저 Mapper 였던 경험과 같이 TDD와 OOP를 동시에 만족시키려다, 지금 필요한 것보다 “이상적인 설계”에 더 집착해버렸던 순간들을 ! 그 후 다시 나 스스로에게 되물어봤다.
현재 코드에 실제 버그가 있나? 없다. 모든 테스트가 통과했고, 시나리오도 정상 동작한다.
현재 코드가 읽기 어렵나? 아니다. 오히려 구조가 단순하고, 읽으면 바로 이해된다.
변경하기 어려운 구조인가? 아니다. 도메인과 계산 로직은 이미 테스트로 잘 감싸져 있고, 리팩토링하기에도 부담이 없다.
이 프로젝트에서 DB, 외부 API, 복잡한 인프라를 쓸 예정인가? → 아니다. 학습용 콘솔 프로그램이다.
그런데도 나는 ConvenienceStore를 억지로 Repository/Service 구조로 쪼개려 하고 있었다 !
실제 문제를 해결하려는 게 아니라, “더 객체지향적으로 보이는 구조”를 만들고 싶을 뿐이었다 !!
“전문적인 설계처럼 보이고 싶다”, “배운 패턴들을 실제 코드에 써먹고 싶다”, “지금은 작지만, 나중에 확장될 수 있으니까 미리 준비해두자”와 같은 욕심들이 모여서, 실제로는 필요하지도 않은 추상화와 계층을 계속 상상하고 있었다는 걸 인정하게 됐다. “더 나은 설계”를 쫓다가 “이미 충분히 좋은 설계”를 놓칠 뻔했다. 충분하다는 가치를 내가 너무 무시하고 있었다 !
2주차 부터 비슷한 고민을 했지만 전혀 자괴감이 들지 않았다. 왜냐하면 1,2,3,4주차 미션을 진행하며 이러한 경험들이 쌓이며 분명 개선되는게 느껴졌기 때문이다 ! 무엇보다 똑같은 문제에 대해 똑같이 고민하는 것이 아니라, 여러 상황에 대한 경험치를 쌓는 과정으로 느껴져서 그저 뿌듯할 뿐이었다 ! 이전 학교 수업에서 “결국 이런 저런 이론보다 실무에서는 전문가들의 경험을 더 중요시 여긴다”는 당시에는 전혀 이해하지 못하고 와닿지 않던 말이 이제는 이해가 된다는 점 또한 너무 신기했다.
마치며
이번 4차 회고는 결과를 정리하는 글이 아니라, 내가 왜 이 과정을 시작했고, 무엇을 깨뜨렸고, 무엇을 남겼는지 돌아보는 정리의 순간이라고 느껴진다. 처음에는 “테스트를 좀 더 잘 써보고 싶다”는 단순한 욕심에서 시작했지만, 이제는 그보다 훨씬 본질적인 질문에 닿아 있다고 느낀다. 나는 지금까지 테스트를 도구로 썼던 걸까, 아니면 테스트에 끌려 다녔던 걸까?
선정 배경에서 스스로에게 던졌던 질문들이 있다.
왜 테스트를 했는데도 버그를 늦게 발견했을까?
왜 테스트가 있었는데도 리팩토링이 불안했을까?
왜 테스트가 설계를 돕지 못했을까?
이 질문들의 공통된 원인은 분명했다.
나는 테스트를 “정답을 검증하는 수단”으로만 사용했고, “요구사항을 발견하고 설계를 이끄는 도구”로는 쓰지 못하고 있었다는 것.
이번 1~4차 회고를 거치며 그 오해는 조금씩 무너졌고, 테스트에 대한 관점은 서서히 바뀌었다.
테스트는 버그를 잡는 마지막 방어선이 아니라, 버그를 만들지 않기 위해 끊임없이 질문하는 도구였고,구현을 뒷받침하는 부속물이 아니라, 설계를 이끄는 출발점이 될 수 있다는 것을 이제는 실제 경험을 통해 받아들이게 되었다 !!
처음 세웠던 네 가지 목표를 다시 생각해보면 완벽하게 도달했다고 말할 수는 없겠지만, 적어도 이제는 그 목표들이 막연한 이상이 아니라 실제 코드와 경험 위에서 체득되고 있다는 감각이 있다.
이전의 나는 “설계를 먼저 고민하고, 그에 맞게 구현하고, 나중에 테스트를 덧붙이는 구조”였다면 지금의 나는 “테스트를 통해 요구사항을 구체화하고, 그 위에서 설계 방향을 정하고, 그 설계를 다시 테스트로 검증하는 흐름”을 경험하고 있다.
이 차이는 생각보다 훨씬 크고, 이 차이가 바로 이번 TDD 여정의 핵심 성과라고 느낀다 !!
기술을 익혔다기보다 완전히 정신개조가 되었다는 느낌이 든다 !
이번 4차 회고까지 오면서 가장 크게 무너진 생각은 "처음부터 끝까지 늘 잘 설계해야한다"이다.
중요한 건 멋있는 구조를 한 번에 만드는 게 아니라, 틀려도 무너지지 않고, 바꿔도 무너지지 않는 상태를 유지하는 것이라는 걸
TDD를 통해 직접 체감하게 되었다.
그 상태를 가능하게 해준 건 설계 감각이 아니라, 리팩토링을 두려워하지 않게 해 준 TDD 사이클 속 살아있는 테스트라는 안전망이었다 !
이 여정은 TDD를 마스터하기 위한 여정이 아니라 내가 가졌던 오해를 하나씩 깨는 여정이었다.
테스트를 나중에 붙여도 된다는 오해, 설계를 먼저 완벽히 해야 한다는 오해, 계층은 많을수록 좋다는 오해, 테스트가 많으면 무조건 좋은 거라는 오해 !
이번 경험을 통해 그 오해들이 조금은 흐릿해졌고, 대신 나만의 기준이 조금씩 생겨나기 시작했다.
아직 부족하고, 분명 또 흔들릴 것이고, 실무에서는 다시 다른 고민이 시작되겠지만 지금 나는 적어도 테스트를 통해 사고하는 개발자로 한 걸음 들어왔다는 확신은 있다. 이러한 정신개조 그 자체로 너무나 큰 수확이다 !
너무 많은 걸 느끼고 깨달은 이번 오픈미션 경험이 너무나 소중하다 ! 이렇나 경험들과 감정들이 더욱 더 크루원으로서 우테코에 합류하고자 하는 나의 열정을 불태운다 ! 내년 2월 페어프로그래밍이 너무 너무 기대된다 딱대 !
'외부활동 > 우아한테크코스 [프리코스]' 카테고리의 다른 글
| [우아한테크코스 8기] 최종 코딩테스트 후기 및 최종 합격 (2) | 2026.01.15 |
|---|---|
| [우아한테크코스 8기] 프리코스 오픈미션 최종회고 (0) | 2025.11.26 |
| [우아한테크코스 8기] 프리코스 오픈미션 3차 회고- 로또 TDD (0) | 2025.11.19 |
| 테스트 주도개발 TDD 실천법과 도구 정리 도서 요약[FAQ & 설계사고과정] (0) | 2025.11.13 |
| 테스트 주도개발 TDD 실천법과 도구 정리 도서 [핵심 요약] (0) | 2025.11.13 |