전문가칼럼

DBMS, DB 구축 절차, 빅데이터 기술 칼럼, 사례연구 및 세미나 자료를 소개합니다.

테스트를 충분히 했는데도 릴리즈 후에 문제가 발생했어요

전문가칼럼
DBMS별 분류
DB일반
작성자
dataonair
작성일
2011-07-07 00:00
조회
6623





소프트웨어 잘 만들기

테스트를 충분히 했는데도 릴리즈 후에 문제가 발생했어요

소프트웨어는 사람이 만든다고 이야기한다. 우수한 인적 자원을 가진 대한민국이 전세계 소프트웨어 시장, 특히 패키지 소프트웨어 시장에서 그다지 큰 역할을 담당하지 못하는 이유는 무엇일까 여러 가지 이유가 있겠지만, 미국과 같은 소프트웨어 선진국에 비해 체계적인 소프트웨어 개발 방법이 정착되지 않았기 때문 아닐까 싶다. 소프트웨어 개발 방법이라는 방대한 주제를 글로 다룬다는 것은 현실적으로 쉽지 않기 때문에, 필자는 총 5회에 걸쳐 현업에서 겪은 10년간의 삽질()을 통해 체화한 엑기스() 노하우만을 추려 연재한다.

지난 시간에는 버그를 체계적으로 관리하는 방법에 대해 알아보았다. 이 글에서는 버그를 효과적으로 찾아내기 위해서 체계적으로 테스트하는 방법에 대해 알아볼 것이다. 또한 버그를 만들지 않기 위해 예방 차원에서의 테스트 중요성에 대해서도 살펴볼 것이다. 아울러, 이 글의 제목을 약간 정정해야 할 것 같다. 테스트를 충분히 하면 릴리즈 후에 문제가 발생하지 않는다. 테스트를 충분히 했다고 생각했지만, 실제로는 테스트를 충분히 하지 않았기 때문에 릴리즈 후에 문제가 발생한 것이다.

테스트의 중요성

테스트에 대한 대상과 방법을 이야기하기 전에, 테스트와 관련해 발생할 수 있는 문제점들을 알아보고 테스트가 왜 중요한지에 대해 공감할 필요가 있다. 한 회사의 개발 프로세스는 그 회사의 문화와도 같다.
그러한 문화는 쉽게 바꿀 수가 없는데, 특히 테스트에 대한 개발 프로세스는 쉽게 바꾸기가 어렵다는 것을 필자는 여러차례 깨달았다. 테스트 코드를 작성하지 않고도 잘 개발해 왔는데 왜 시간을 더 투입해서 테스트 코드를 작성해야 하느냐는 개발자와 개발자들이 테스트까지 하게 되면 프로젝트 기간에 테스트 코드 작성에 필요한 시간까지 투입되어 릴리즈가 늦춰질 수밖에 없다고 생각하는 관리자에 이르기까지 기존의 개발문화에 익숙한 사람들의 생각을 바꾸는 것은 정말 쉽지 않다.
그래서 테스트를 어떻게 할 것인가에 앞서 왜 테스트가 중요한지, 테스트를 소홀히 하면 어떠한 결과를 초래하게 되는지에 대해 공감대를 형성할 필요가 있다. 이제부터 테스트가 제대로 이뤄지지 않은 프로젝트의 경우 어떤 결과를 초래하는지를 열 가지 사례를 들어 간략히 기술해 보겠다.

첫째 - 개발만 진행하고 테스트하지 않는 경우

일정이 촉박한 프로젝트의 경우 개발만 진행하고 테스트를 하지 않는 경우를 종종 볼 수 있다. 사실 이보다 더 위험한 경우는 구현 기간에 비해 짧은 시간 동안 간단한 테스트만 진행하며 개발하는 경우다.
이 경우, 프로젝트 관리자는 테스트도 하면서 개발을 진행하고 있다고 생각하지만, 실제로는 테스트가 안 된 것이나 다름없기 때문에 버그가 발견조차 되지 않은 채 프로젝트는 계속 진행된다.
기능은 계속 추가되고 버그는 계속 늘어나게 되며, 릴리즈에 앞서 테스트를 수행했을 때 비로소 버그들이 발견된다. 하지만 너무 많은 버그가 발견돼 이를 감당하지 못하고 릴리즈를 계속 연기하게 된다.
혹은 고객에게 제품을 전달하기 전에 간단한 테스트만을 수행하고 전달해 다양한 문제를 고객이 발견하게 되고 고객은 다시는 그 제품은 물론, 그 회사가 만든 다른 제품들까지도 구매하지 않는다.
거기다가 개발사는 그 문제를 해결한 후, 고객사에 설치된 제품을 교체하고 기능이 잘 동작하는지 고객사에서 검증하기 위한 인력을 투입하는 추가적인 비용을 감수해야 한다.

둘째 - 코드만 테스트하고 매뉴얼은 테스트하지 않는 경우

패키지 개발사를 예로 들어보겠다. 패키지 안에는 릴리즈하는 제품을 담은 CD와 제품을 설명하는 매뉴얼이 포함된다. 매뉴얼을 테스트한다는 것은 매뉴얼에 나온 내용대로 제품을 실제로 사용해보고 매뉴얼에 틀린 부분은 없는지, 심지어 사소한 오타 하나까지 찾아내는 것을 의미한다.
매뉴얼을 테스트하지 않은 경우, 고객은 매뉴얼 안에 있는 절차대로 프로그램을 사용하려 시도하지만 실제로는 매뉴얼에 있는 대로 프로그램이 동작하지 않는 것을 보게 되면, 제품 개발사가 매뉴얼을 검증조차 하지 않고 고객에게 전달했다는 사실을 알게 될 것이다.
이로 인해 고객은 제품 자체뿐만 아니라 제품 개발사에 대해 좋지 않은 이미지를 가지게 되며, 그 개발사가 릴리즈하는 다른 제품들을 구매할 때에도 부정적인 영향을 미칠 수 있다.

셋째 - 테스트를 자동화하지 않는 경우

테스트를 하긴 하는데, 매번 수동으로 테스트하는 경우다. 테스트하는 데 시간이 많이 걸리기 때문에 테스트를 빈번하게 수행할 수 없고, 다양한 테스트를 수행하기보다는 단편적인 기능을 위주로 테스트하게 된다. 또한 방금 구현한 기능을 위주로 테스트가 이뤄지기 때문에, 오래전에 구현한 기능에 대해서는 테스트하지 않게 된다.
예전에 잘 작동했던 기능이 갑자기 작동하지 않는 문제가 발생되고, 개발 도중에 이를 디버깅하느라 시간을 허비한다. 필자의 경험상 이렇게 허비되는 시간은 테스트를 자동화하는 데 필요한 시간보다 훨씬 많기 때문에 프로젝트가 비효율적으로 진행되고 추가로 인력을 투입해도 일정은 이와 무관하게 연기된다.

넷째 - 테스트를 자주 하지 않는 경우

테스트하지 않고 구현만 진행하다가 나중에 몰아서 테스트를 하는 경우다. 일례로, 한 달 동안 테스트는 하지 않고 구현만 하다가 그 뒤 한달 동안 테스트를 하는 식으로 개발이 진행되면, 구현기간 내에 발견되지 않은 문제점들이 뒤늦게 발견되어 해결하기가 무척 곤란한 상황에 이를 수 있다.
심지어 코드를 걷어내고 다시 작성해야 하는 상황도 발생하는데, 새로 작성하는 코드도 마찬가지로 테스트하지 않고 구현만 하는 방법을 반복하는 악순환이 계속된다.
테스트를 주기적으로 하기 위한 한 가지 방법으로, SVN과 같은 버전 컨트롤 시스템(Version Control System. 이하 VCS)을 이용하고 개발자가 SVN COMMIT과 같은 명령어를 이용하여 코드 변경 내용을 코드 저장소(Repository)로 전송할 때마다 유닛테스트, 통합테스트, 회기테스트, 그리고 성능테스트를 수행하도록 자동화하는 방법을 추천한다. 또한 이러한 자동화를 돕는 허드슨(Hudson)과 같은 도구를 사용하는 것도 고려해 볼 수 있다. 허드슨에 대한 자세한 내용은 http://hudson-ci.org를 참고하길 바란다.

다섯째 - 장시간 테스트와 스트레스 테스트, 비정상 상황 테스트를 하지 않는 경우

서버 상에서 동작하는 소프트웨어의 경우, 제품을 장시간 실행해보고 메모리 누수와 같은 프로그램 오류는 없는지, 제품의 기능이 오작동하는 경우는 없는지 검증해야 한다. 하지만 이를 수행하지 않고 릴리즈한다면, 고객사에서 몇 주만에 문제가 발생할 것이다.
서버 상에서 동작하는 소프트웨어가 아니더라도 고객이 소프트웨어를 실행한 후 얼마나 오랫동안 프로세스를 종료하지 않고 유지하게 될지 그 누구도 예측할 수 없다. 때문에 장시간 테스트를 수행할 필요가 있다.
또한 소프트웨어가 감당할 수 있는 일반적인 처리량보다 훨씬 많은 처리량이 몰렸을 때도 오동작하지 않고 잘 동작하는지 검증한 후에 릴리즈가 이뤄져야 한다. 이러한 검증이 이뤄지지 않은 채로 릴리즈 된다면, 어느날 갑자기 고객사에서 처리량이 몰리는 날에 문제가 발생하게 될 것이고, 고객사는 해당 제품을 문제있는 제품으로 인식하기 시작할 것이다.
예를 들면 네트워크가 끊어진다던가, 데이터를 저장하는 하드 디스크가 가득 차거나, 갑자상 종료한다던가, 컴퓨터의 전원이 나간다던가 하는 비정상적인 상황을 포함해 다양한 부분에서 테스트를 수행하지 않고 릴리즈했다가 이러한 상황에서 제품이 오동작한다면 고객사에 돌이킬 수 없는 손실을 끼치게 될 것이다.

여섯째 - 성능을 주기적으로 모니터링하지 않는 경우

기능이 구현되는 중간중간에 성능을 측정하지 않고, 기능 구현이 완료된 후에 성능을 측정해보니 너무 느려서 도저히 릴리즈하기 힘든 수준임을 알게 된다. 하지만 너무나 많은 기능을 추가했기 때문에 어떤 기능 때문에 성능이 느려진 것인지 알 수 없다. 프로파일링 툴을 이용해 성능저하의 원인을 찾아보려 하지만, 시스템 설정 변경과 같은 이유로 성능이 저하된 경우 프로파일링 툴을 이용해 이를 찾아내기란 쉽지 않다. 결국 릴리즈는 연기된다. 해결책으로는 짧은 사이클로 제품의 성능을 측정하고 모니터링하여 성능저하가 발생한 것을 알게 된 즉시 원인을 찾아 해결하는 방법이 있다.

일곱째 - 테스트 자동화의 정도에 대해 측정하지 않는 경우

테스트를 자동화 하긴 했지만 어느 정도 자동화 되었는지 측정하지 않아서 지속적인 테스트 자동화가 이뤄지지 않는 경우다.
100줄의 코드 중 몇 줄이 테스트 되었는지 라인 커버리지를 측정하지 않아서 개발자는 테스트 코드를 작성했어도 어떤 코드가 여전히 테스트가 이뤄지지 않는지에 대해 모르는 채로 다음기능을 구현하게 된다.
또한 새롭게 구현한 기능에 대해서도 일부만 테스트되고 프로젝트가 계속 진행되다가 릴리즈에 임박해서야 테스트가 이뤄지지 않은 기능이 너무 많다는 것을 인지하고, 다양한 버그를 만나게 될 것이다. 이 때, 개발자에게는 뾰족한 해결책은 없고, 릴리즈 또한 연기되는 상황을 접하게 된다.
이때 필자가 추천하는 해결책으로는 gcov와 같은 테스트 커버리지 측정 툴을 이용하고 테스트 안된 코드를 테스트하기 위해 테스트 코드를 지속적으로 추가해 나가는 방법이다.

여덟째 - 고객과 동등한 입장에서 테스트하지 않는다

고객은 제품의 다양한 기능 중 몇 가지 주요기능만 간단하게 사용하려고 제품을 구매하지 않는다.
고객은 제품에 포함된 기능을 다양한 방법으로 조합해 사용한다. 따라서 고객처럼 다양한 방법으로 제품을 써보지 않은 상태에서 릴리즈가 된 경우, 고객은 너무 많은 문제를 발견하게 되어 놀라게 될 것이며, 제작사는 이러한 문제가 내부에서 발견되지 않은 채 릴리즈 되었다는 점에 놀라게 될 것이다. 하지만 제품을 고객이 사용하는 것과 동일하게 써보지 않고 릴리즈 했기 때문에 이러한 결과는 어찌보면 당연하다고 말할 수 있다.
마이크로소프트는 제품 개발기간 전반에 걸쳐 도그푸드(Dogfood), 즉 별도의 시스템에서 실제 고객과 동일한 환경으로 제품을 사용해보고 윈도우즈나 오피스와 같은 주요 제품의 주요 릴리즈 전에 직원들에게 이를 사용하도록 권유하는 과정을 거친다. 또한 실제 개발을 위해 사용되는 다양한 시스템에 적용하여 제품에 문제가 없는지 철저히 검증한다.
이렇게 신제품이 릴리즈 되기 전에는 대부분의 직원이 그 제품을 설치하고 회사에서 자신의 업무를 처리하는데 사용해본다.
여담이지만, 어떤 직원은 여러 동료가 모인 발표시간에 자신의 노트북을 프로젝터로 연결해서 벽에 뿌렸는데, 윈도우7이 릴리즈된 지 6개월이 지났는데도 윈도우XP를 쓰고 있는 모습을 회의에 참석한 여러 사람이 보게 되었다. 그중 김구라 같은 한 사람이 농담을 건네기도 했는데, 그 친구가 회사의 제품에 대해 그다지 애착이 없는 것이 아닌가 하는 뉘앙스를 풍기기도 했다. 회사의 제품이 출시되기 전에 관심을 가지고 써 보는 것은 마이크로소프트 문화의 중요한 일부분이며, 제품의 문제점을 찾아내는 효과적인 방법이기도 하다.

아홉째 - 통합테스트는 했으나, 유닛테스트를 하지 않는 경우

프로그램에 대한 통합테스트는 수행했지만, 제품을 구현하기 위해 작성한 함수나 클래스 각각에 대해 유닛 테스트를 수행하지 않는 경우를 종종 볼 수 있는데, 이는 모래위에 성을 쌓는 것과 같다.
하나의 프로그램을 구성하는 함수나 모듈에 대한 단위 테스트를 수행할 때는 함수나 모듈의 입력과 예상되는 출력 값을 이용해 함수가 정상적으로 동작하는지를 검증하게 된다. 그러나 이러한 단위 테스트를 수행하지 않으면, 일부 통합테스트에서 커버하지 못한 예외적인 범위의 데이터가 들어오는 경우 개별함수가 오동작해 예상치 못한 오류를 일으킬 수 있다. 또한 프로그램을 작성하다보면 구조를 개선하기 위해 리팩토링(Refactoring)을 실시하게 되는데, 유닛테스트가 작성되지 않은 프로그램의 경우 리팩토링 이후 문제가 발생할 가능성이 다분하다.
그 이유는 유닛테스트가 있다면 리팩토링 전후에 리팩토링된 함수가 예상대로 잘 동작함을 검증할 수 있지만, 유닛테스트가 없다면 이러한 테스트를 통합 테스트에 의존해야 하는 상황에서 리팩토링한 함수가 호출되도록 통합테스트를 수행하는 것이 쉽지 않은 경우가 다반사이기 때문이다.
리팩토링한 함수의 갯수가 많을 경우에는 이를 통합테스트로 모두 커버하기에는 역부족이며, 유닛테스트가 있어야 개별 함수에 대한 검증을 손쉽게 수행할 수 있다.

열 번째 - 체계적인 테스트 계획을 수립하지 않고 생각나는 대로 테스트를 수행하는 경우

테스트 작성의 중요성을 인지하고 테스트 코드를 작성하기는 하지만 계획 없이 생각나는 대로 테스트 케이스를 만드는 경우다.
프로그램 사용에 대한 주요 시나리오를 커버하지 못하고 소스코드의 일부만 커버하는 빈약한 테스트를 작성하지만, 정작 개발자는 테스트를 수행하며 개발했다고 착각하는 다소 위험한 상황이다.
이 경우, 테스트가 이뤄지지 않은 부분이 너무 많아서 테스트하지 않은 부분을 고객이 실행했다가 오류가 발견되는 문제가 발생하기도 한다.
해결책으로는 프로그램을 사용하는 시나리오를 전부 나열하고, 각각의 시나리오에 대해서 테스트 슈트 및 테스트 케이스를 나열한 후, 이들에 대해 우선순위를 부여하고 우선순위가 높은 주요 시나리오에 대해 테스트 케이스를 먼저 작성해 주요 기능이 잘 작동함을 먼저 검증해야 한다. 그 다음, 우선순위가 낮은 테스트 케이스들을 작성해 나가면서 테스트 커버리지를 만족스러운 수준까지 높여나간다.

열한 번째 - 앞서 언급된 열 가지 테스트의 중요성을 알면서도 귀찮아서 테스트하지 않는 경우

요즈음에는 인터넷이 발달하여 테스트가 왜 중요한지, 어떻게 수행하는지, 수십 년간 소프트웨어를 개발해온 메이저 개발사들이 테스트하는 방법은 어떠한 것들이 있는지에 대한 정보를 비교적 손쉽게 접할 수 있다. 그래서 상당수의 개발자들이 이와같은 테스트 방법론에 대해 알고는 있지만, 그간 몸에 익힌 주먹구구식 개발방식을 쉽게 버리지 못한다.
혹은 빠듯한 일정에 '테스트'라는 새로운 무언가를 하는 것에 대한 부담을 갖거나, 혹은 일정은 빠듯하지 않지만, 테스트 코드를 작성하기 싫은 귀찮음으로 인해 테스트하지 않고 개발이 진행되다가 결국 위의 열 가지 상황을 그대로 맞닥들이는 경우도 있다.
수동 테스트를 하기가 귀찮아서 자동 테스트를 위한 테스트 코드를 작성하는 개발자는 고수로 인정될 것이지만, 자동 테스트를 작성하기가 귀찮아서 간단히 수동 테스트만 진행한 후, 기능 구현만 열심히 하는 개발자는 하수중의 하수로 평가받을 것이다.

테스트 대상

고객에게 전달되는 제품이 바로 테스트의 대상이다. 따라서 프로그램 바이너리 뿐만 아니라 프로그램 사용법을 설명하는 매뉴얼도 테스트의 대상에 속한다.
매뉴얼 안에 캡처한 화면들이 고객에게 전달된 프로그램을 실제 실행했을 때의 화면과 일치한지 검증해야 할 것이며, 특정 기능을 수행하기까지의 단계를 실제로 실행했을 때, 매뉴얼에 기술된 대로 동작하는지는지도 꼼꼼히 리뷰해야 한다. 매뉴얼을 보면 그 회사가 제품을 얼마나 진지하게 개발하고 출시하는지 금방 알 수 있기 때문에, 고객에게 전달하는 바이너리 파일 뿐 아니라 매뉴얼도 완벽에 가까운 품질을 보장할 수 있어야 한다.
매뉴얼 외에도 프로그램을 로컬 라이즈, 글로벌 라이즈 한 경우 해당 프로그램이 다양한 언어에 대해 잘 동작하는지, 번역이 길게 되어 화면에서 잘리는 문자열은 없는지도 테스트해야 한다.

테스트 수행시점

테스트는 언제 수행되어야 할까 필자는 SVN과 같은 버전 컨트롤 시스템에 소스코드 변경분이 전송되면 <표 1>과 같은 테스트가 자동으로 수행되도록 개발 시스템을 설정하길 권고한다. 릴리즈할 플랫폼이 여럿(예를들어 윈도우와 리눅스)이라면, 모든 플랫폼에 대해 다음 테스트를 수행한다. 테스트의 진척상황과 수행결과는 웹으로 확인이 가능하며, 문제가 있을 경우 문제가 발견된 모듈의 담당자와 새로운 코드를 전송한 개발자에게 메일을 보내어 문제를 조기에 해결할 수 있도록 돕는다.

110706_ok1_img01.jpg

테스트계획서(Test Plan)

테스트를 수행하기에 앞서 테스트 계획서를 작성하는 것은 매우 중요하다. 테스트 계획서를 통해 제품이 어느 정도 테스트될 것인지를 프로젝트 참여자가 함께 리뷰하고 부족한 부분을 보강하여 제품을 체계적으로 테스트할 수 있기 때문이다.
테스트 계획서 안에는 고객이 제품을 사용하는 다양한 시나리오와 각 시나리오별로 사용하게 되는 제품의 기능을 우선순위별로 분류해 기술한다. 그리고 테스트를 자동화할 수 있는 방법을 모색하고, 자동화가 도저히 불가능할 경우에는 수동으로 테스트할 항목으로 만들어서 주기적으로 테스트한다.
테스트 시나리오나 테스트 케이스의 우선순위는 고객이 제품의 기능을 사용하는 빈도와 밀접하게 연관되어 있다. 고객이 자주 사용하는 기능에 대한 테스트일수록 우선순위가 높다.
또한 많은 수의 고객이 사용할 기능일수록 우선순위가 높다. 테스트 자동화는 우선순위에 입각해 높은 우선순위를 가진 테스트케이스부터 자동화한다.

개선하려면 측정하라

테스트 코드는 작성하면 끝이 아니라 그 테스트 코드가 얼마나 많은 기능 구현 코드를 테스트하는지 주기적으로 측정할 필요가 있다. 또한 기능 구현 코드의 어느 라인이 테스트 되었으며, 어느 라인이 테스트되지 않았는지 확인할 수 있다면 테스트되지 않은 코드를 테스트하기 위해 새로운 테스트 코드를 작성하는데 큰 도움이 된다.
이렇게 기능을 구현한 코드 중 얼마나 많은 부분이 테스트 되었는지를 측정하는 측정치가 바로 테스트 커버리지(Test Coverage)다.
테스트 커버리지는 여러 가지 기준으로 측정할 수 있는데, 그중 가장 많이 쓰이는 라인 커버리지(Line Coverage)는 소스코드 파일별로 주석이나 공백라인을 제외한 실제 코드가 존재하는 행 중 몇%의 행이 테스트 되었는지를 측정한다.
테스트 커버리지를 측정할 수 있는 무료 툴로 gcov가 있는데, gcov를 이용하면 <리스트 1>과 같이 소스코드의 행마다 테스트가 몇 회 수행되었는지를 알 수 있다.
######으로 표시된 행은 해당 행이 전혀 테스트 되지 않았음을 의미하며, 이 부분을 테스트하기 위해 추가적인 테스트코드 작성이 필요함을 보여준다.
gcov에 대한 자세한 사용방법은 http://korea.gnu.org/ manual/release/gcov/gcov_1.ko.html을 참고하길 바란다.

<리스트 1> gcov를 이용해 소스파일 안의 행 별로 테스트가 된 행과 그렇지 않은 행을 확인할 수 있다. (출처: korea.gnu.org )

main()
{
1 int i, total;

1 total = 0;

11 for (i = 0; i < 10; i++)
10 total += i;

1 if (total != 45)
###### printf ("Failure\n");
else
1 printf ("Success\n");
1 }

테스트 자동화(Test Automation)

테스트는 수동으로 수행할 수도 있고, 자동으로 수행할 수도 있다. 수동으로 수행하는 테스트는 즉시 수행할 수 있다는 장점이 있는 반면, 사람이 수동으로 테스트하다 보니 테스트 도중에 실수가 있거나, 중요한 테스트를 빼먹을 수 있으며, 여러번 반복하기에는 비 효율적이라는 단점이 있다.
자동으로 수행하는 테스트는 처음 테스트 코드를 작성하는데 노력이 필요하지만, 한번 작성된 자동화된 테스트 케이스는 이후 언제든지 실행해보고 프로그램이 정상적으로 작동하는지를 손쉽게 검증할 수 있다는 장점이 있다.
프로그래머들은 창조적인 작업을 하는 예술가임을 항상 잊지 말자. 단순 반복 수동 테스트보다는 어떻게 하면 테스트를 자동화 할 수 있을지 고민하고, 테스트 코드를 작성하면서 테스트 커버리지가 올라갈 때마다 묘한 쾌감을 맛보도록 하자.
테스트를 자동화하지 않으면 기능이 추가됨에 따라 매번 수동으로 테스트해야 할 기능이 점점 늘어난다. 시간이 지날수록 테스트에 필요한 시간이 점점 늘게되어 결국 테스트를 포기하고 개발만 하게 되는데, 이런 식으로 진행되는 프로젝트는 개발자가 막판에 수많은 버그들을 잡느라 밤을 새지만 새로 발견되는 버그가 잡히는 버그보다 많아서 릴리즈는 연기된다. 릴리즈만 연기되면 다행이겠지만, 이런식으로 개발된 제품은 고객에게 전달된 후에도 끊임없이 버그가 발견된다.

유닛 테스트와 통합테스트

유닛테스트(Unit Test)는 하나의 기능을 구현하는 여러개의 함수에 대해 다양한 입력값에 대한 출력값이 맞게 나오는지 검증하기 위한 테스트다. 객체지향 방법론을 사용하고 있다면, 각각의 클래스를 이용해 생성한 인스턴스 멤버 함수에 대한 입출력값이 맞는지를 검증한다. 유닛테스트를 위한 프레임워크는 여러 가지가 있는데, 닷넷 프레임워크의 경우 NUnit, Java의 경우 JUnit을, C++의 경우 Google Test Framework를 추천한다.
통합테스트(Integration Test, End-To-End Test)는 고객에게 전달되는 바이너리 자체를 테스트하며, 고객이 제품을 사용하는 것과 같은 방법으로 테스트한다.
통합테스트를 수행해서 기능을 검증했는데 왜 유닛테스트까지 수행해야 하는지에 대해 의문이 가는 독자는 앞서 테스트의 중요성을 설명한 부분에서 아홉 번째 항목으로 이에 대해 자세히 설명했으니 참고하길 바란다.

회귀테스트(Regression Test)

한 달 전에 작성했던 코드에 대해 대략적인 구조는 기억할 수 있을지 몰라도 코드의 상세한 내용은 이미 내 머릿속에서 사라진지 오래다. 지금 내 머릿속에 들어 있는 내용은 당장 이번주에 구현하고 있는 기능에 대한 코드일 뿐이다.
그런데 이번 주에 작성하는 코드는 한 달 전에 작성한 코드와 섞여서 컴파일되고 그와 어울려 잘 돌아가야 한다. 회귀테스트는 예전에 잘 작동했던 기능이 지금도 잘 작동함을 보장하기 위해서 기능을 추가할 때마다 해당 기능에 대해 작성하는 테스트 케이스다. 이후에 다른 기능을 추가할 때도 기존에 작성된 테현한 기능이 여전히 잘 작동함을 보장해준다.
심지어 필자가 일했던 한 회사에서는 버그를 하나 잡을 때마다 해당 버그의 재현 케이스를 테스트 케이스로 만들어서 자동화시키고, 버그를 수정한 코드를 버전 컨트롤 시스템으로 전송할 때 해당 테스트 코드까지 함께 전송하도록 프로세스를 운영한 팀도 있었다.
이와 같이 작성된 테스트 케이스는 자동화된 테스트 슈트의 일부로 실행되며, 이후에 같은 버그가 또 발생하지 않는다는 것을 보장한다.

장시간 테스트(Long Haul Test)

소프트웨어가 고객에게 한 번 전달되면 고객이 프로그램을 실행한 후, 얼마 동안 컴퓨터를 끄지 않고 계속 사용할 지 제작사 입장에서는 예측할 수 없다.
특히 서버에서 동작하는 소프트웨어의 경우 몇 주, 심지어는 몇 달 동안도 계속 실행될 수 있으며, 이러한 상황에서도 아무런 문제가 발생하지 않아야 한다.
따라서 릴리즈 전에는 제품을 최소 2주 이상 장시간 실행해서 아무런 문제가 발생하지 않는다는 것을 검증할 필요가 있다. 이러한 테스트가 바로 장시간 테스트다.
장시간 테스트 중에는 구현된 모든 기능을 골고루 테스트하는 자동화된 테스트 프로그램을 주기적으로 실행하면서 제품이 오랜 시간 실행했을 때 발생하는 문제점을 조기에 포착하고 해결한다. 테스트 중에 문제가 발견되면 해결하고 다시 장시간 테스트를 수행해야 하기 때문에, 미리 계획된 릴리즈 날짜보다 몇 달 전부터 장시간 테스트를 주기적으로 수행한다.

성능테스트(Performance Test)

코드가 변경됨에 따라 성능에 얼마나 영향을 미치는지를 확인하기 위해서는 SVN에 코드가 전송될 때마다 제품을 새로 빌드하고 성능을 측정해 데이터베이스에 저장한 후, 필요할 때 웹에서 코드 변경에 따른 성능의 변화를 그래프로 볼 수 있도록 개발 시스템을 구성할 필요가 있다.
성능을 주기적으로 모니터링하지 않는 경우, 릴리즈에 앞서서 최적화 기간을 두어 성능최적화를 하게 되는데, 최악의 경우에는 성능 최적화를 위해 설계 자체가 변경돼야 하는 것으로 판명난다. 하지만 릴리즈가 얼마 남지 않아서 설계를 변경하지 못하고 릴리즈를 하게 되고, 이후 고객이 성능에 대해 만족하지 못해 경쟁사에 뒤처지는 일이 발생할 수 있다.
따라서 성능은 최대한 짧은 사이클로 모니터링해 개발 중에 성능 저하가 발생했을 때 즉시 대응하고 해결할 수 있도록 할 필요가 있다.
아울러, 성능과는 별개로 확장성(scalability)이 중요한 소프트웨어의 경우에는 확장성 또한 주기적으로 측정할 필요가 있다.

도그푸드(Dogfood)

지금까지 기술한 어떠한 테스트보다도 가장 중요한 테스트가 바로 도그푸드다. 도그푸드는 제품을 테스트하는 것이 아니라 실제로 고객이 제품을 사용하는 것과 똑같은 방법으로 내가 고객이 되어 제품을 장시간 사용하는 것을 뜻한다. 이를 통해 릴리즈 후에 고객이 맞닥들일 문제를 릴리즈 전에 미리 발견하고 해결한다.
문제점을 최대한 많이 찾아내기 위해 개발자들은 상당한 시간을 도그푸드 환경에서 자신의 모듈이 정상적으로 동작하는지 검증한다. 또한 릴리즈에 앞서서 개발자들은 매일 최소한 두세 시간은 도그푸드 환경의 에러 로그파일 안에 기록된 자신의 모듈에 대한 로그를 면밀히 분석하고 문제가 없음을 확인한다.
예를 들어 웹서버를 릴리즈 하는 회사라면, 회사의 홈페이지를 릴리즈 할 버전의 웹서버로 교체해 운영할 수 있다. 새 버전의 웹서버에 문제가 생겨서 홈페이지가 다운되는 상황이 생겨서는 안되니, 일부 트래픽은 기존 버젼의 웹서버에서 처리할 수 있도록 설정해야 하는 것도 중요하다.
마이크로소프트는 효과적인 도그푸드 과정을 거치기 위해 별도의 예산을 편성하고, 실제 고객이 사용하는 것과 동일한 환경으로 해당 제품을 사용한다.
하나의 제품을 대상으로 도그푸드 과정을 거치는데 드는 비용은 벤처기업 하나를 새로 설립할 수 있을 만한 엄청난 금액이다. 돈 낭비가 아니냐고 반문하는 독자도 있을 것이다. 하지만 도그푸드의 효과는 그만큼 엄청나기 때문에, 비용을 들여서라도 정식 제품 출시 전까지는 장기간에 걸쳐 도그푸드를 실시하는 것이 좋다.
소프트웨어는 사람이 만드는 것이기 때문에, 아무리 우수한 프로세스를 적용한다고 하더라도 버그가 있을 수 있다. 필자는 감히 말할 수 있다.

성공하는 개발사와 실패하는 개발사의 차이는 이러한 문제를 제품출시 전에 찾아내고 해결할 수 있는지의 여부로 구분할 수 있다고 말이다.
제품출시 후에 고객들이 버그를 끊임없이 발견하는 제품은 반드시 그 원인을 분석하고 해결해야 치열한 경쟁 시장에서 살아남을 수 있을 것이다.
고객이 버그를 발견하게 되면 제품뿐만 아니라 회사의 이미지에도 치명적인 손상을 입게 되며, 해당 버그를 수정하고 다시 릴리즈 하느라 개발진이 다음 릴리즈에 집중할 수 없게 되는 문제가 발생한다.
마이크로소프트에서도 이러한 문제가 전혀 발생하지 않는 것은 아니지만, 그 횟수가 필자가 국내에서 경험한 것과는 비교할 수 없을 정도로 적었다.
지금까지 필자는 총 5회의 컬럼을 통해 개발 프로세스가 중요한 이유를 구체적인 사례를 들어 설명하고, 일정안에 정해진 인력으로 고품질의 소프트웨어를 개발해내기 위해 프로젝트 계획단계부터 릴리즈까지의 각 단계에서 프로젝트를 관리하는 방법, 그리고 체계적인 버그관리 및 테스트 방법에 대해 설명했다. 부족한 내용이지만 시간내어 읽어준 독자들에게 감사하며, 다섯 편의 지루한 컬럼 내용을 세 가지 법칙으로 요약해 보았다.

● 1법칙

무욕(無欲)의 법칙 - 욕심부리지 말고 기간 안에 할 수 있을 만큼으로 범위를 한정하라.
망하는 프로젝트의 첫 번째 이유는 정해진 기간과 인력에 비해 터무니없이 많은 기능을 구현한다는 것이다. 구현만으로도 일정 안에 완료하기가 버겁기 때문에, 개발자들이 테스트를 안 하게 되고 결국 프로젝트 후반에 버그를 감당하지 못해 일정이 연기된다.
필자는 이러한 문제로 인해 매 분기 릴리즈가 연기되다가 결국 최종릴리즈가 3년간이나 연기 되었고, 그래도 버그가 많아서 고객으로부터 외면당하는 경우도 보았다.
해보지도 않고 기간 안에 할 수 있는지를 어떻게 알 수 있는지, 그리고 개발 중에 예상치 못한 문제를 맞닥들일 수도 있는데 어떻게 기간안에 구현할 수 있는 만큼으로 기능을 한정할 수 있느냐고 반문하는 독자도 있을 수 있다.
그러한 독자에게는 세 번째 연재를 다시 보기를 권하고 싶다. 구현할 주요 기능에 대한 프로토타이핑과 기반기술 습득, 그리고 체계적인 플래닝을 수행하여 정해진 기간 안에 제품을 구현할 수 있도록 스펙을 한정지을 수 있기 때문이다.

● 2법칙

릴리즈 임박 - 코드 변경량 감소의 법칙, 릴리즈가 임박할수록 코드 변경량은 점점 줄어들어야 한다.
프로그래머가 작성하는 코드에는 버그가 있을 가능성이 항상 존재한다. 심지어 버그 해결을 위해 수정한 코드에 새로운 버그가 들어있을 가능성도 있다. 따라서 릴리즈가 다가올수록 코드 수정량을 서서히 줄여나가야 한다. 이를 위해 네 번째 연재에서 알아본 것과 같이 코드 프리즈(Code Freeze), 버그 분류 미팅(Bug Triage Meeting), 버그 잡기 그래프(Bug Glide Path)를 활용할 수 있다.

● 3법칙

즉시 검증의 법칙 - 한 방에 다 만들려 하지 말고 기능을 테스트해가면서 조금씩 추가하라.
몇 주 동안 컴파일조차 해보지 않고 기능을 구현한다던가, 테스트 코드를 작성하지 않고 수동으로 간단한 테스트만 수행하며 몇 달 동안 기능만 구현하는 경우를 종종 볼 수 있다.
인간의 뇌(특히 30대 중반 이후 프로그래머의 뇌)는 시간이 지날수록 기억한 내용이 자동으로 사라지기 때문에 구현하고 한참 지나서 예상했던 대로 동전에 구현했던 코드를 다시 분석해야 하는 상황이 발생한다.
따라서 이번 글에서 강조한 대로, 구현한 코드는 즉시 테스트한다. 즉시 테스트하면 버그의 현상을 알게 된 후, 머릿속에 있는 코드로 원인 분석까지 끝내는 보너스도 얻을 수 있다.
테스트는 자주하면 할수록 더 좋다. 물론, 빈번한 테스트를 손쉽게 수행할 수 있도록 테스트를 자동화 하는 것이 관건이다. 그래서 자동화할 수 없을 것 같은 테스트도 자동화할 수 있는 개발자가 진정한 고수다.

연재를 마치며, 개발 프로세스에 관해 독자들과 소통할 수 있는 필자의 페이스북 팬 페이지 ThankYouSoft를 개설하였으니 다음 QR코드를 통해 접속 바란다.

110706_ok1_img02.png