최근 MY CODING TEST(이하 MCT) 프로젝트의 백엔드 소스코드 단위 테스트에서 라인 커버리지 100%를 달성했습니다. 제 인생 첫 100% 테스트 커버리지라 무척 뿌듯했습니다. 모든 운영 코드를 테스트로 커버하니 “이제 버그에서 안전한 코드가 됐구나"라는 생각도 잠시 들었죠. 하지만 곧이어 버그와 마주하면서 “버그 없는 코드"라는 환상이 얼마나 허황된 것인지 깨닫게 됐습니다.
이번 경험은 저에게 100%라는 숫자의 의미를 다시 생각하게 했고, 진정한 테스트가 무엇인지 한 발 더 고민하는 계기가 됐습니다. 이 글에서는 테스트 커버리지 100%를 달성하며 했던 일들과 그 과정에서 얻은 깨달음을 공유하려 합니다.
왜 100%를 목표로 했을까?
MCT 프로젝트를 막 마무리 하였을 때, 테스트 커버리지는 10%도 안 됐을 겁니다. 처음 목표는 한 달 안에 기능을 완성하는 것이었기 때문에 테스트 코드 작성에 시간을 들이기보다 빠른 구현에 집중했습니다. TDD(Test-Driven Development)를 도입해볼까 고민도 했지만, 처음 접하는 방식이라 한 달 안에 프로젝트를 끝내지 못할까 봐 익숙한 방식—엔드포인트 설정, 리포지토리 작성, 비즈니스 로직 구현—으로 진행했습니다.
기능은 무사히 완성됐지만, 마음 한구석엔 늘 불안이 있었습니다. “내 코드가 정말 의도대로 잘 돌아갈까?“라는 의문이 떠나지 않았고, 실제로 A 기능을 고치다 B 기능이 먹통이 되는 경우도 생겼죠. 그때 “테스트 코드를 처음부터 작성할걸"이라는 후회가 살짝 들었지만, 한 달이라는 마감이 코앞이라 일단 서비스가 동작하는 코드를 완성하는 데 집중했습니다.
기능 완성 후 일주일쯤 지나 리팩토링을 시작하면서 믿음직한 코드를 만들고 싶다는 욕심이 생겼습니다. 그러던 중 유튜브 알고리즘의 선택을 받은 토스ㅣSLASH 21 - 테스트 커버리지 100% 영상을 보게 됐고, 관련 책도 읽으며 “나도 100%를 달성해보고 싶다"는 의지가 생겼습니다. 당시 저는 이런 기대를 품고 있었습니다.
- 버그 없는 코드를 보장받을 수 있을 거라는 기대
사실 토스ㅣSLASH 21 - 테스트 커버리지 100% 영상에서 강연자께서 100%라도 버그는 발생한다고 말씀하셨지만 토스라는 복잡한 비즈니스 로직을 다루는것과 비교해서 제 서비스의 코드는 무척 단순할것이기 때문에 100% 커버리지를 달성하면 이정도의 코드는 버그 방지가 확실히 되지 않을까 하는 생각을 가지고 있었습니다.
100%를 향한 여정에서 한 일
테스트 코드를 작성하려면 기존 운영 코드를 다시 들여다봐야 했는데, 이 과정에서 많은 개선점을 발견했습니다. 그중 몇 가지를 소개합니다.
1. 불필요한 코드 제거
운영 코드를 점검하며 더 이상 필요 없는 로직이나 중복 코드를 정리했습니다. 테스트를 작성하면서 “이건 왜 여기 있지?“라는 의문이 생긴 부분을 과감히 삭제하며 코드가 간결해졌습니다.
2. 모호한 코드 개선
불과 두 달도 안 된 제 코드를 다시 보는데, 읽다가 멈칫하는 부분이 있었습니다. 분석해보니 이런 코드들은 대체로 “한 메서드에 여러 책임이 있는 경우(SRP 위반)“였습니다. SRP(Single Responsibility Principle)를 지키기 위해 책임을 분리하고, 메서드를 추출하며 가독성과 유지보수성을 높였습니다.
3. 로직 점검
단순 CRUD를 넘어 복잡한 비즈니스 로직을 테스트하며 “더 효율적인 방법은 없을까?” 고민하게 됐습니다. 이 과정에서 로직을 개선하고, 잠재적 문제점을 미리 잡아내는 계기가 됐습니다.
커버리지 100% 달성
마침내 커버리지 리포트를 열었을 때 “100%“라는 숫자를 보고 솔직히 감격했습니다. 제가 작성한 코드 한 줄 한 줄이 테스트로 보호받고 있다는 느낌은 개발자로서 큰 만족감을 주었죠. 특히 “굳이 테스트 안 해도 되겠지"라며 넘겼던 유틸리티 함수까지 모두 커버하며 코드 전체에 대한 신뢰가 생겼습니다.
“이제 버그는 안 생기겠지?“라는 생각이 잠깐 들었지만, 곧 현실을 마주했습니다. 제 코드에서도 커버리지 100%는 버그 방지를 100% 보장하지 않는다는 것을 깨달았습니다.
커버리지 100%가 버그 방지를 보장하지 않는다는 사실
커버리지 100%를 달성하고 나서 “이제 버그는 안 생기겠지"라는 기대를 잠시 품었지만, 현실은 그리 녹록지 않았습니다. 배포 후 실제로 버그가 터지면서 100%라는 숫자가 모든 것을 해결해주지 않는다는 사실을 뼈저리게 깨달았습니다.
API 호출에서 401 응답 발생
배포에 성공후 서비스를 이용하는 도중, 인증이 불필요한 API 호출에서 401 Unauthorized 응답이 발생했습니다. “분명 커버리지가 100%인데 어떻게 된 거지?“라는 당혹감이 들었죠. 곧이어 문제의 원인을 파헤쳐 보게 됐습니다.
버그의 출처는 아래 코드에서 시작됐습니다:
|
|
이 메서드는 HttpServletRequest에서 특정 쿠키 값을 가져오는 역할을 합니다. 문제는 request.getCookies()가 null을 반환할 수 있는데, 저는 이 경우를 전혀 고려하지 않았습니다. 그 결과 NPE(NullPointerException)가 발생했고, 이 예외는 SecurityFilterChain에 설정된 exceptionHandling에서 잡혀 401 응답으로 변환된 것이었습니다. Security 설정에서 인증이 불필요하다고 설정한 API임에도 불구하고 말입니다.
단위 테스트를 살펴보니, “쿠키가 없는 경우"라는 엣지 케이스를 아예 작성하지 않았더군요. 그런데도 커버리지는 100%를 유지했습니다. 왜냐하면 커버리지 도구는 단순히 코드 라인이 실행됐는지 여부만 측정하기 때문입니다. 제가 테스트에서 request.getCookies()가 null인 상황을 다루지 않았음에도, 다른 케이스에서 코드가 “읽히기만” 하면 커버리지가 올라가더군요. 결국 이 사건은 커버리지 수치가 버그를 막아주지 않는다는 명백한 증거였습니다.
이후 null
을 체크하는 로직을 추가해 버그 수정은 하였고 이에대한 단위 테스트도 추가하였습니다.
|
|
|
|
+++ 또 드는 생각은 저 메서드가 SRP를 너무나 위반했다는것이 보이는거 같습니다. getJwtFromCookie
에서 ‘request에서 추출’, ‘추출된 쿠키를 검증한다’라는 2개의 꽤 큰 책임이 동시에 있어보입니다. 책임 분리를 실시하여 각 책임을 작게 해주면 이런 엣지 케이스도 더 잘 보였을거 같다라는 생각이 듭니다.
커버리지의 한계
이 경험을 통해 커버리지 100%가 버그 없는 코드를 보장하지 않는다는 사실이 확실해졌습니다. 테스트 커버리지는 “코드가 실행됐는지"만 확인하는 지표일 뿐, 그 테스트가 얼마나 의미 있는지, 실제 사용 시나리오를 제대로 반영했는지는 숫자로 알 수 없습니다. 제가 작성한 테스트가 엣지 케이스를 다루는지, 현실적인 문제를 예방하는지는 결국 제 설계와 고민에 달려 있었습니다.
그럼에도 불구하고 100% 커버리지의 이점
제가 생각해본 이번에 100%를 달성하며 가진 의미를 몇가지 추려보았습니다.
- 운영 코드의 재점검을 통한 버그 예방
- 그래도 안한 것 보다는 낫다는 사실
- 새로운 기능 추가나 리팩토링시 심적 안정 제공
- 코드 이해도 향상(테스트 코드 작성하며 다시 코드를 보게되므로)
배운 점
오히려 SOLID의 중요성을 오히려 더 깨닫게 되는 계기
테스트 코드를 작성하며 SOLID 원칙, 특히 SRP의 가치를 새삼 느꼈습니다. 책임이 잘 분리된 코드였다면 엣지 케이스를 더 쉽게 발견했을 테고, 테스트도 간결해졌을 겁니다. 이번 경험은 초창기 소프트웨어 개발 원칙이 왜 현대까지 이어져 오는지 몸소 체험한 계기가 됐습니다.
대충 테스트할 거면 차라리 하지 마라
대강 100% 채우려고 아무생각없이 테스트 코드를 작성할 거면 안하는게 맞다는 생각이 들었습니다. 테스트 코드도 결국은 ‘코드’입니다. 수치에 급급해 대충 작성한 부분을 반성하며, 이후 리팩토링으로 테스트의 질을 높여가야겠다는 다짐을 했습니다.
마무리
시작할 때는 Security 의존성 문제와 지루한 작업에 “과연 가능할까?“라는 걱정도 있었지만, 결국 해냈습니다. 이 과정에서 코드와 테스트에 대해 많은 걸 배웠고, 100%라는 숫자가 목표가 아니라 도구임을 깨달았습니다. 진짜 중요한 건 테스트로 얻는 신뢰와 안정감, 그리고 이를 유지하려는 끊임없는 노력입니다. 앞으로는 100%라는 기준을 넘어, 더 의미 있고 실질적인 테스트를 작성하는 데 집중하려 합니다.