서론
최근 테스트 코드와 관련된 많은 깨달음을 얻었습니다.
테스트 코드의 필요성과 테스트하기 좋은 구조란 무엇인지, 그리고 왜 MVC 패턴에서 테스트가 어려운지를 체감할 수 있었습니다.
테스트 코드 없이 앱 리뉴얼 하기
최근 사내 앱의 UI 리뉴얼을 진행했습니다.
기존 기능은 그대로 유지하면서 UI만 새롭게 구현하는 작업이었습니다.
이 과정에서 기존 동작 검증에 어려움을 느꼈습니다.
기존 동작이 제대로 유지되는지 확인하기 위해서는 기존 앱과 리뉴얼된 앱을 일일이 실행하며 비교해야 했습니다.
테스트 코드가 있었다면 딸깍 한 번으로 검증이 가능했을 텐데... 많이 아쉬웠습니다.
작업 시간뿐만 아니라 심리적인 부담도 상당했습니다.
"테스트 코드 없는 리팩토링은 도박과 같다"는 말이 있듯이 코드를 수정할 때마다 "이 수정이 안전한 걸까? 이런 구현에 특별한 이유가 있지 않았을까?"라는 고민이 끊이지 않았습니다.
동작을 확인하면서도 '혹시 놓친 숨겨진 기능이 있진 않을까', '내가 확인하는 이 동작이 정말 A.C.가 맞을까'하는 불안감이 계속됐습니다.
테스트 코드의 부재는 작업 시간 증가라는 실질적인 문제도 있었지만, 이러한 심리적 불안감으로 인해 그 필요성을 더욱 절실히 느낄 수 있었습니다.
테스트 코드를 작성하려고 보니
테스트 코드 없는 리팩토링이라는 '도박'을 무사히 마친 후, 이제는 틈날 때마다 테스트 코드를 작성하려 노력하고 있습니다.
이 과정에서 어떤 구조가 테스트하기 쉽고 어려운지 많은 것을 배웠습니다.
특히 이론으로만 접했던 "MVC 패턴에서의 테스트 작성의 어려움"을 찐하게 체험했습니다.
제가 담당하는 프로젝트는 MVC 패턴으로 구현된 ViewController도 많습니다.
프로젝트를 한 명이 전담하는 구조이기에 MVC가 나쁘지 않은 선택이기도 합니다.
하지만 테스트 코드를 작성하려 하니 난관에 부딪혔습니다.
아이러니하게도 접근 제어자를 적절히 사용했기 때문에 테스트가 더욱 어려워졌습니다.
외부에서 접근할 필요가 없는 메서드와 변수들은 private으로 설정하게 됩니다.
따라서 MVC에서 대부분의 로직 메서드들이 private으로 정의되어 있었고, 이는 테스트를 더욱 어렵게 만들었습니다.
로직만 따로 테스트하기 위해서는 실제 코드의 접근 제어자를 수정해야 하는 상황이 발생했습니다.
"테스트를 위해 실제 코드를 수정하지 말라"는 원칙은 널리 알려져 있죠.
이로 인해 현재 구조를 전면 수정할 것인지, 테스트 코드 작성을 포기할 것인지, 아니면 테스트를 위해 접근 제어자를 수정할 것인지 많은 고민이 있었습니다.
테스트 코드를 위해
현재는 많은 책임을 가지고 있는 ViewController에서 기능(책임)을 점진적으로 분리해나가는 방향을 선택했습니다.
이러한 결정을 내리게 된 이유도 설명해드리겠습니다.
테스트 코드 작성을 위해 기존의 MVC 구조를 전면 수정하는 것은 현재 상황에서 적절하지 않다고 판단했습니다.
테스트 코드 작성을 포기하는 것은 단순히 문제를 회피하는 것에 불과했습니다. 미래에도 같은 문제가 반복될 것이 뻔했죠. 저는 이러한 반복을 막고 싶었기에 테스트 코드 작성은 꼭 필요하다고 생각했습니다.
테스트를 위해 접근 제어자를 수정하는 것 역시 실제 코드의 안정성을 저해할 수 있어 바람직하지 않았습니다.
그래서 저는 기능을 점진적으로 분리하고, 분리된 기능에 대한 테스트 코드를 작성하기로 결정했습니다.
구체적인 예를 들어보겠습니다. 기존에는 홈 화면의 ViewController에서 A, B, C 데이터를 모두 가져오고 표시하며 관리했습니다.
이를 개선하여 A 데이터를 관리하는 객체를 새로 만들어 역할을 분리했고, 이 객체의 데이터 처리 로직에 대한 테스트 코드를 작성했습니다.
ViewController와 ViewModel처럼 UI와 로직을 완벽하게 분리하지는 못했지만, 각 역할을 적절히 나누는 데는 성공했습니다.
역할이 분리되자 자연스럽게 테스트 대상이 명확해졌고, 많은 책임을 떠안고 있던 ViewController의 코드도 점차 개선되고 있습니다. 더 나은 해결책이 있을 수 있겠지만, 현재로서는 좋은 선택이었다고 느끼고 있습니다.
어떤 테스트를 해야할까
무엇을 테스트하고 어떻게 테스트 코드를 작성할지에 대해서도 깊이 고민하고 있습니다.
특히 어떤걸 테스트 해야 하는지 많이 고민하고 있습니다.
"모든 것"을 테스트해야 하는지, 그렇다면 "모든"이라는 범위는 어디까지인지 고민이 많았습니다. 또한 테스트 코드 작성 문화가 없는 조직에서 이에 많은 시간을 투자하는 것을 긍정적으로 볼까 하는 현실적인 고민도 있었습니다.
이러한 선택의 기준을 잡기 위해 "테스트 코드를 통해 얻고 싶은 것은 무엇인가?"에 집중했습니다.
테스트 코드의 목적이 명확해진다면 테스트 대상을 결정하는 것도 수월해질 것이라 생각했기 때문입니다.
현재는 기능의 주요 시나리오부터 테스트하고 점진적으로 케이스를 확장해나가기로 했습니다.
이는 제가 테스트 코드를 통해 얻고자 하는 것이 리팩토링 시 로직의 정확성 보장과 그로 인한 심리적 부담 감소였기 때문입니다.
모든 케이스를 테스트하는 것도 좋은 방향이겠지만, 배포 일정도 빠듯한 현실에서는 적절하지 않았습니다.
따라서 주요 시나리오에 대한 테스트를 우선적으로 작성하여 리팩토링 시의 부담을 실질적으로 줄이는 방향으로 결정했습니다.
테스트 코드가 유효했던 경험
테스트 코드를 작성하는 과정에서 실제 누락된 처리를 발견한 경험도 있습니다.
테스트 코드 도입 초기임에도 이미 놓친 부분을 찾았다는 것이 창피하기도 하지만, 매우 값진 경험이었습니다.
구체적으로는 학습 완료 API 관련 케이스였습니다.
기존에는 API가 항상 200 status code를 반환하고, 응답 데이터 내부의 code 속성으로 에러 여부를 판단했습니다.
백엔드 개발자의 제안으로 새로 추가되는 에러는 status code로 표현하기로 결정되었습니다(기존 에러는 하위 호환성 유지를 위해 그대로 유지...😭)
이에 따라 API 완료 completionHandler에 status code를 확인하는 로직을 추가하고, 성공 여부를 반환했습니다.
이 completionHandler는 총 3개의 화면에서 사용되고 있었는데, 테스트 코드를 작성하는 과정에서 한 곳의 처리가 누락된 것을 발견했습니다.
만약 이를 발견하지 못하고 배포했다면 에러 상황에서도 정상 동작하는 것처럼 보여서 학습 데이터가 제대로 기록되지 않았을 것입니다.
부끄러우면서도 테스트 코드 작성으로 이를 사전에 발견할 수 있어서 다행이었습니다.
마무리
앞으로의 작업 우선순위에 대해서도 고민해보았습니다.
배포 아이템 구현, 테스트 코드 작성, 코드 및 구조 개선이라는 세 가지 작업의 우선순위를 어떻게 가져갈지 생각했습니다.
하지만 이는 상황에 따라 달라질 수밖에 없고, 각 요소들이 서로 밀접하게 연관되어 있다는 것을 깨달았습니다.
일정이 촉박한 상황이라면 당연히 배포 아이템 구현에 우선순위를 두어야 할 것이고, 여유가 있다면 부족한 테스트 코드를 보완하거나 유지보수를 용이하게 하는 구조 개선에 집중할 수 있을 것입니다.
현재는 배포 아이템을 구현하면서도 테스트 시나리오를 미리 고려하여 테스트 코드를 효율적으로 작성할 수 있도록 노력하고 있습니다. (잦은 기획 변경으로 인해 TDD 도입은 아직 시기상조라고 생각됩니다 😅)
테스트 코드의 필요성을 이론이 아닌 실제 경험을 통해 깨달았으니, 이제는 이를 잘 실천해 나가는 일만 남았습니다.
감사합니다.
아직은 초보 개발자입니다.
더 효율적인 코드 훈수 환영합니다!
공감과 댓글 부탁드립니다.