1. 개요
전통적인 공학론적 개발 프로세스는 사전에 철저히 검증된 계획 하에 장기간에 걸쳐 많은 인원과 비용을 투입하여 목표를 완수하는 방식을 따른다. 소프트웨어 개발 역시 초기에는 이러한 프로세스에 따라 폭포수 모델 등을 따랐으나, 구현하게 될 소프트웨어의 규모가 커지고 복잡해짐에 따라 소프트웨어 위기(Software crisis)라는 문제에 봉착하게 되고, 소프트웨어 공학에서는 소프트웨어 개발 과정이 다른 공학적 방식과 큰 차이가 있음을 인지하게 된다.소프트웨어는 유동적이고 예측하기 어렵다. 또한 소프트웨어가 발전하면서 점차 확장가능성, 개방적 구조를 요구하게 되었다. 따라서 구체적으로 개발이 언제 완료될 것인지 예측하는 것이 매우 어려울 뿐더러, 인원과 비용을 늘린다고 해서 개발 시간의 절감이나 질적인 성과를 보장할 수 없다. 소프트웨어의 규모가 커지고 복잡해질수록 기존의 폭포수 모델을 적용했을 때 다음과 같이 다양한 문제점이 발견되었다.
1. 개발에 적용할 수 있을 수준의 구체적인 요구사항을 작성하는 것이 매우 어려움. 불가능함.
2. 규모가 커질수록 설계에 요구되는 시간과 비용이 기하급수적으로 증대됨.
3. 실제로 개발에 들어가고나서 정해진 요구사항이 변경되거나, 다양한 문제점이 발견됨.
4. 위와 같은 문제로 인해 작업 난이도 및 개발일정을 예측하는 것이 어려움.
기존의 계획주도 개발방식에서는 세운 계획대로 개발이 흘러가지 않으며, 그것을 보완하기 위해 언제나 개발이 지연되었고, 개발자에게 주어지는 스트레스와 과중한 업무, 그 결과 떨어지는 생산성과 상품성 등에 직면하여 소프트웨어 공학자들은 전통적인 개발 프로세스에 큰 의문을 제기하였다. 이에 완전한 설계를 지향하는 폭포수 모델을 폐기하고, 초기의 설계 비용을 줄이고자 작은 규모의 소프트웨어를 완성한 다음 그것을 점차 보완해가며 복잡한 소프트웨어를 완성하는 나선형 모델(Spiral model)을 도입하는 등 산업에서는 보다 경험적 프로세스 제어 모델로 이행하게 된다.
애자일 프로그래밍(Agile programming) 방식은 계획과 문서에 의존하는 기존의 방식을 부정한다. 미래에 대한 예측을 차단하고 지속적인 프로토타입의 완성을 반복하여 그때그때 소단위 요구사항을 추가하고 기존의 문제점을 해결하여 점차 큰 규모의 소프트웨어를 완성하는 개발 방식이다. 익스트림 프로그래밍(eXtream Programming, XP)은 대표적인 애자일 프로그래밍 개발방법론 중 하나로, 고객이 원하는 소프트웨어를 빠른 시간 내에(약 2주) 프로토타입의 형태로 전달하고 이를 통해 고객이 원하는 소프트웨어를 이끌어내며, 수시로 발생하는 요구사항에 대처하는 것을 목표로 한다.
다른 애자일 방법론과 구별되는 XP의 특징은 테스팅이다. 구현과 테스트를 하나의 쌍으로 취급하여, 실제 구현과 동시에 테스트 코드를 작성하도록 하며, 이것에 기반한 프로젝트 발전 과정은 애자일 방법론의 기본 개념인 "반복적으로 프로토 타입을 고객에 전달함으로써 고객의 요구사항 변화에 민첩하게 대응한다"를 실천하는데에 큰 도움을 줄 수 있다. 왜냐하면 매번 프로토 타입을 고객에 전달함에 있어서 프로토 타입 자체로써 버그가 상대적으로 적은 완벽에 가까운 데모를 경험하게 해줄 수 있기 때문이다.
테스트 주도 개발(Test Driven Development, TDD)은 익스트림 프로그래밍 개발방법론의 실천 방안 중 하나이다. 개발이 이루어진 다음 그것이 계획대로 잘 완성되었는지 테스트 케이스를 작성하고 테스트하는 타 방식과는 달리, 테스트 케이스를 먼저 작성한 다음 테스트 케이스에 맞추어 실제 개발 단계로 이행하는 개발방법론을 말한다. 묵시적으로 잠재된 상황을 가정하지 않고 테스트 케이스만을 완벽하게 수행하는 것을 목표로 하기 때문에 매우 빠르게 목표를 완료할 수 있다. 한편, TDD 자체가 하나의 테스트가 완전하지 않다는 것을 가정하고 있기 때문에 1차 테스트를 완료한 다음에 새로운 테스트 케이스를 확장해서 작성하고 그것을 통과하기 위한 개발에 들어가는 과정을 끊임없이 반복하여 큰 규모의 프로젝트를 완성해가는 것이다.
2. 분류
테스트 주도 개발에서 작성되는 테스트는 단위 테스트가 대표적이다. 먼저 단위 테스트는 말 그대로 한 단위[1]만을 테스트하는 것이다. 단위 테스트를 작성하려면, 일단 테스트를 작성한 뒤 컴파일 에러가 일어나지 않도록 테스트에서 쓰이는 클래스와 그 메소드의 스텁을 만들어 둔다. 테스트가 실패하는 것을 확인하고(= 컴파일 에러는 없는 것을 확인하고) 클래스를 구현한다. 클래스에서 필요한 다른 클래스가 필요하면 일단 스텁으로 지금 작성중인 클래스의 테스트를 통과하게만 해 두고, 나중에 그 클래스의 테스트 케이스를 작성한 후 구현한다. 이런 식으로 프로젝트 전체를 완성해 나간다. 하지만 단위 테스트는 단위 하나하나에 대해서만 검증할 수 있으므로, 이를 해결하기 위해 인수 테스트를 작성한다. 사실 인수 테스트는 전통적으로 사람(QA)의 영역이었지만, 코드로도 약간 구현해두면 품질에 큰 도움이 된다[2].3. 장점
-
코드의 유지보수가 용이해진다
프로그래밍 개발에서는 처음 개발할 때보다 이미 개발한 코드의 버그를 수정하고, 최적화하고, 새 기능을 추가할 때 비용이 더 들어간다. 그런데 테스트를 작성하면 코드에 절대로 뒤떨어지지 않는 문서가 탄생하며, 다른 코드의 행위가 보증되므로 원하는 부분에만 신경을 쓸 수 있으며, 테스트하기 쉬운 코드는 자연히 품질이 높아지므로 다시 읽기도 편하다. 또한 테스트가 있으면 안심하고 코드를 리팩토링할 수 있다.
-
프로그래밍 시간이 단축된다
테스트를 작성하는 시간을 포함시키고도 오히려 전체 작업 시간은 줄어든다. 왜냐하면 프로그래밍에서 대부분의 시간이 디버깅에 투입되는데, 테스팅은 디버깅을 해야 할 범위를 단위 안으로 제한함으로써 디버깅에 들어가는 노고를 크게 줄여준다. 또한 유지보수 시에도 상술한 이유로 효율이 높아진다. 특히 JavaScript나 Python 등의 동적 타입 언어로 개발을 할 경우 TDD는 필수적이라고 할 수 있다.
-
뛰어난 프로그램 소스코드 기록
테스트를 작성하는거 자체가 훌륭한 소스 코드의 기록이다. 소스 코드 중간중간의 주석은 왜 코드가 이렇게 짜여져 있는지를 기록한다면, 유닛 테스트는 코드가 어떻게 행동해야 하는지를 기록한다. 따라서 다른 프로그래머들이 쓴 (또는 과거의 자신이 쓴) 코드를 파악하고 프로그램을 수정, 확장하는데 시간과 비용이 크게 단축된다.
4. 단점
-
인간은 실수를 미리 인지하지 못한다
테스트를 미리 작성하고 코드를 작성한다는 것은 이미 해당 실수가 일어날수 있다고 인지하고 있는 상황에서나 적용 가능한 이야기다. 대표적으로 결과가 확정되어 있는 수학 라이브러리 코드 등이 이에 속한다. 루트를 계산할때 뉴턴-랩슨법보다 빠른 방법을 구현하기 위해 코드를 작성한다면, 그 결과 값은 항상 기존의 정답과 같아야 한다. 그렇기 때문에 몇몇의 예외 케이스들을 테스트한다면 해당 문제를 잡을수 있다. 하지만 이러한 수학문제가 아닌 대부분의 프로그래밍 사례에서는 미리 작성된 테스트가 개발자의 실수를 답습하기만 한다. 즉 변명을 하기 위한 완벽한 변명거리가 된다. 기본적으로 인간의 실수는 코드를 작성할때 인지 할수 없다. 코드를 쓰고 문제가 발생 했을 때 테스트를 작성하나, 테스트를 작성하고 코드를 작성하고 문제를 발견하나 결국 발생할 실수는 똑같이 발생하기 때문이다. 장점으로 알려진 디버깅 시간 단축은 그저 이론적인 환상에 가깝다.
-
테스트가 모두 통과하므로 버그가 없다고 생각하는 함정에 빠질 수 있다
녹색 불이 들어오면서 통과하는 정돈되고 멋진 전체 테스트 목록은 개발자에게 높은 쾌감과 확신을 준다. 이 때문에 꽤 잘 갖추어진 테스트 목록이 완성되면, 개발자는 섣불리 코드를 확신에 차서 배포하다가 테스트에서는 고려하지 못한 문제를 만나 버그를 만들고 당황하기도 한다. 그러나 테스트의 존재는 1. 구현 코드에 버그가 없고, 2. 구현 코드가 고객의 비즈니스 요구사항을 완벽히 만족시킨다는 것을 증명하지 않는다. 개발자는 구현할 로직의 모든 스펙과 부작용을 완벽히 예측할 수 없고, 비즈니스 요구사항은 계속 변화한다. 어제까지 완벽했던 코드는 오늘 협력사의 업데이트로 인해 무너질 수 있다. 오늘까지 맞았던 로직은 내일 회사의 목표나 고객의 변심에 의해 틀릴 수 있다. 테스트와 테스트 주도 개발은 "이 구현 코드가 문제없이 완벽함"을 보증하는 것이 아니라 "이 구현 코드가 여기까지는 문제없음을 확인함"을 보증하는 것이다. "여기까지는 괜찮음"을 확인한 것만으로도, TDD는 제 역할을 한 것이다. 테스트를 중심으로 개발한 덕분에 개발자는 새로운 변경을 만들 때 전체 소프트웨어가 문제가 없는지를 처음으로 돌아가서 점검할 수고를 덜기 때문이다.
-
추가적인 기록물의 증가
주석의 치명적인 단점은 주석이 항상 코드와 동일한 내용을 보장하지 못한다는 것이다. 쉽게 말하면 소스코드에 쓰여진 주석의 내용과 TDD용으로 개발된 테스트와 실제 라이브 서비스에서 돌아가는 코드와 개발문서가 전부 다를수도 있다. 추가적인 기록물이 늘어나는 만큼 추후에 읽고 검증해야할 기록물 또한 늘어난다.의미 없는 문서를 읽으며 하루종일 월급루팡 ㄷㄷ
5. Unit Testing 프레임워크
-
xUnit
뒤에 -Unit이란 접두사가 붙는 여러 단위(unit) 테스트 프레임워크의 통칭이다. Java의 사실상 표준 테스트 프레임워크인 JUnit이 가장 유명하다. 최초의 xUnit 프레임워크는 SUnit인데, 켄트 벡이 Smalltalk용으로 개발했다. 이후 켄트 벡과 에릭 감마가 함께 이를 Java용인 JUnit으로 포팅했다. 비행기를 같이 타고 가다가 JUnit을 만들었다고 한다. 이 외에도 C++용의 CppUnit, .NET Framework용의 NUnit, XUnit 등이 있다. -
Jest
자바스크립트용 단위 테스트 프레임워크. Facebook에서 만들었다.
6. Code Coverage
테스트가 실제로 거쳐가는 코드가 몇 줄인지 확인하는 용도로 쓴다. 예외 처리 같은 것이 제대로 테스팅 되는지 확인하거나, 아니면 다른 개발자들이 제대로 테스팅 코드를 작성하는지 확인하려면 쓰는 것이 좋다. 프로파일러처럼 사실 유닛 테스트와는 독립적으로 동작한다. 그래서 본인이 어떤 코드를 써 놓고 코드가 어떻게 흘러 가는지 파악하는 데 사용할 수도 있다.다만, 각종 프로파일러나 벤치마크 툴을 돌릴 때, 자체적으로 잡아먹는 성능 때문에 실제보다 성능이 낮게 측정되는 현상처럼, Coverage를 적용시키고 나면 테스트 속도가 느려질 수 있으므로 대형 프로젝트에서는 신중히 적용하자.
-
JaCoCo
Java 코드에 대하여 Code Coverage를 관리할 수 있는 툴이다. Code Coverage를 지원하는 오픈 소스 툴 중에 가장 활성화된 커뮤니티를 가진다.[3] Eclipse, Jenkins, SonarQube에 플러그인 형태로 사용할 수 있으며 NetBeans, IntelliJ IDEA 등의 IDE는 기본적으로 지원하고 있다. Ant, Maven, Gradle 등의 빌드 툴을 사용한다.
7. CI
위와 같은 프레임워크들을 활용해서 테스팅 케이스를 작성하고 나면 직접 돌려 보아야 하는데, 사람이 일일이 수동적으로 돌리려면 노동력이 들어가게 되고, 자동으로 돌린다 할지라도 서버를 구매할 여력이 되지 않는다면 클라우드 서버를 빌려야 한다.보통 특별히 클라우드 서버를 빌려 쓰기 보다 CI를 사용하는 편이다.
개인 사용자나 오픈 소스 프로젝트의 경우 아예 무료로 쓸 수 있게 해주는 경우가 많으므로 잘 찾아보고 공부해 보도록 하자.
- Travis CI
- Azure Pipelines
- CircleCI
-
등등
[1]
일반적으로 클래스
[2]
현재 코드가 완전히 사람을 대체하는 것은 불가능한데, 실제 응용 프로그램에서는 가능한 경우의 수가 무한히 많기 때문이다.
[3]
그래도 영어 커뮤니티가 전부이다...