지속적인 통합은 단순히 생각하면 빌드 자동화, 컴파일 자동화에 불과하다. 자동화 자체를 꾀하는 개발팀이 의외로 적다는 점을 감안하면 ‘불과하다’고 폄하할 필요는 없지만, 더 많은 가능성이 있는데 이를 전부 활용하지 못하니 아쉬울 뿐이다. 지속적인 컴파일을 넘어 소프트웨어 코드베이스의 품질을 극적으로 개선하는 방법이 있는데, 이를 활용하지 못하는 현실이 안타깝다.
개발자 테스트(단위 테스트
등)는 도입하기 쉽지 않지만, 느긋하게 마음 먹고 익숙해지면 많은 게 변한다.
[필자 소개] 최재훈 SK 아이미디어의 게임 서버 팀에서 일한다. 요즘은 스크립트 엔진을 개발하는 데 전념하며, 새로운 도전을 즐긴다. 직업 외적인 측면에선 배철수의 음악 캠프를 15년째 즐겨 듣고, U2가 최고의 밴드라 생각한다.
[본문]
이번 칼럼이 단위 테스트를 다룬 세 번째 글이다. 여느 때와 마찬가지로 복습을 잊지 말자.
단위 테스트 첫 편에선 단위 테스트가 왜 중요한지, 어떤 단위 테스트 라이브러리를 골라야 하는지 알아봤다. 닷넷에선 64비트 환경을 지원하는 NUnit을 추천했으며, C++이나 C++/CLI 환경이라면 예외 처리가 잘 되고 XML 출력이 가능해
CruiseControl .NET과 연동하기 쉬운
UnitTest++을 권했다.
두 번째 칼럼에선 UnitTest++과 CruiseControl .NET을 연동해봤다. 개발자가 테스트할 땐 표준 입출력에 결과를 출력하고, 빌드 서버가 테스트를 돌릴 땐 파일에
XML 형식의 결과값을 기록한다.
테스트 코드에 표준입출력을 사용하는 코드가 있으면 CruiseControl .NET의 빌드 로그가 망가질 수 있으니, UnitTestExec.bat란 배치 파일을 만들어 이런 문제를 방지했다. 다음
칼럼부터 다룰 MSBuild의 경우에도 이렇게 배치 파일을 적극적으로 활용하게 된다. 꽤 유용한 기법이니 어렵지 않다고 대충 넘기지 말고 새겨두면 좋다.
두 칼럼에서 가장 중요한 내용은 역시 UnitTest++의 테스트 결과 보고서를 웹 대시보드를 통해 깔끔하게 정리해 보여주는 기법이다.
CruiseControl .NET이 UnitTest++을 공식적으로 지원하지 않기 때문에 직접 Unittests.xsl 파일을 편집했다. 개발팀마다 프로젝트마다 빌드 환경이
달라지기 때문에 이렇게 사람의 손길이 닿아야 할 일이 생긴다. 이번처럼 누군가 칼럼이나 웹 페이지를 통해 해결책을 공개한 경우라면 운이 좋은 것이지만, 항상 이렇게 일이 잘 풀리진 않으니
CruiseControl .NET의 동작 원리를 잘 알아두면 좋다.
자, 복습은 끝났다. 이제 본론, 즉 단위 테스트를 어떻게 도입하고 활용할 것인지에 대해 알아보자.
프로젝트 구성하기
거듭 말하지만 이 칼럼은 Visual
C++을 기준 삼는다. VC++ 프로젝트의 구성 유형은 크게 세 가지다. 응용프로그램(.exe), 동적 라이브러리(.dll),
정적 라이브러리(.lib) 말이다. 각각의 프로젝트마다 그에 해당하는 UnitTest++ 프로젝트를 구성하는 방법이 다르다. 이제부터 하나씩 알아보자.
정적 라이브러리
그림 1. 정적 라이브러리에 대한 종속성
우선 [그림 1]을 보자. 여기서 Core는 정적 라이브러리고, CuckooTest는 Core에 대한 단위 테스트 프로젝트다.
물론 CuckooTest는
콘솔 애플리케이션이다. 이 그림에서 CuckooTest는 Core에 대한 종속성이 있다. 이렇게 프로젝트 종속성을
설정해놓으면 CuckooTest를 빌드할 때 Core.lib 파일을 링크할 때 입력 값으로 쓴다. 물론 이 말엔 전제가 있다.
CuckooTest 프로젝트 구성 값이 [그림 2]와 같아야 한다.
그림 2. CuckooTest 프로젝트
“프로젝트 의존성 링크하기(Link Project Dependencies)”를 선택해야 이렇게 알아서 Core.lib을 링크한다. 하지만 프로젝트를 처음 만들 때 이 값이 기본적으로 선택되므로 신경 쓸 일이 거의 없다.
이게 전부다. 나머진 UnitTest++ 홈페이지에 있는 “Step,
a step by step example”(http://unittest-cpp.sourceforge.net/money_tutorial/)
문서를 따라 하면 끝이다.
동적 라이브러리
실은 동적 라이브러리를 직접 개발해 쓴 적이 없다. 아니, 그런 적이 있긴 하지만 직접 짠 라이브러리가 아니었고, 그 코드에 대해 단위 테스트를 해본 적이 없다는 게 정확하다. 그렇다면 동적 라이브러리인 경우는 설명할 능력이 없느냐? 또는 설명할 자격이 없느냐?
그렇기도 하고 아니기도 한다. 일반적인 동적 라이브러리에
대한 경험은 부족하지만, C++/CLI를 다룬 경험이 있다. 네이티브 코드(일반적인 C++)에 대한 관리되는 래퍼 라이브러리(간단히 말해 닷넷 기능이 들어간 C++ 코드)를 짠 경험은 풍부하다. 보통 C++/CLI 라이브러리는 동적 라이브러리의 형태를 띈다. 만약
정적 라이브러리로 빌드해 버리면, 외부의 관리되는 코드(C#이든 C++/CLI든)가 해당 라이브러리에 어떤 타입이 있는지 알
수 없게 되기 때문이다.
동적 라이브러리를 쓰면 외부에 공개된 인터페이스만 접근할 수 있다. 이는 단위 테스트도 마찬가지라 상당수의 코드를 테스트하지 못한다. 하지만 동적 라이브러리의 오브젝트 파일을 직접 링크하면 문제는 쉽게 해결된다.
[그림 3]을 보자. ScriptShell이란 동적 라이브러리가 있고, ScriptShellTest란 단위 테스트 프로젝트가 있다. 이때 단위 테스트 프로젝트의 구성 값 중 “추가 의존성”란이 핵심이다. 링크시 ScriptShell의 모든 오브젝트 파일을 입력 값으로 쓰라고 해놨다. 이렇게만 하면 된다.단, 이런 식으로 오브젝트 파일을 직접 링크하면 빌드 시간이 꽤 늘어난다. 사실, 사람들이 단위 테스트를 자주 실행하게 만들려면, 단위 테스트를 빌드하고 실행하는 데 걸리는 시간이 짧아야 하기 때문에 이는 문제의 소지가 있다.
그림 3. 외부 의존성 추가하기
응용프로그램
응용프로그램도 동적 라이브러리와 마찬가지로 “추가 종속성” 기능을 활용하면 된다. 하지만 더 좋은 방법은 프로젝트
구성 자체를 바꾸는 것이다. 약간만 수고하면 오브젝트 파일을 링크하는
데 소모되는 엄청난 시간을 아낄 수 있다.
보통 응용프로그램 프로젝트를 만들고 그 안에 소스 코드를 마구 추가해 넣기 시작한다.
하지만 방법을 달리하면 어떨까? 기존 응용프로그램 프로젝트를 정적 라이브러리로 바꾸고, 응용프로그램 프로젝트를 새로 추가한다. 그리고 새로
만든 응용프로그램 프로젝트엔 프로그램을 시작시키는 메인 함수만 둔다. 이렇게 하면 단위 테스트 프로젝트가 정적 라이브러리를 직접 참조해 쓸 수 있다. 당연히 빌드 시간도 줄어든다. 아주 조금 수고할
의욕만 있으면 된다.
테스트의 어려움
테스트 주도 개발이란 말을 많이 들어봤을 것이다. 코드를 먼저 짜고 테스트하는 게 일반적인 개발 순서라면, 테스트 주도 개발은 이와 반대로 운용된다. 간단히 말해 테스트 코드부터 짜고 실제 기능 구현이 이어진다. 이렇게 하면 코드의 추상화나 사용편의성을 좀더 생각하게 되고, 자연히 코드 품질이 향상된다는 이야기다.
하지만 테스트 주도 개발을 하는 개발자나 조직은 보기 힘들다.
왜 그럴까?
처음 테스트 주도 개발을 접하고 단위
테스트를 도입할 때였다. 학교를 벗어나 현업을 처음 접할 무렵의
일이다. 테스트 주도 개발을 도입하면 전문가답게 일하는 모양새가
날 거라 생각해서 NUnit을 설치하고 사용법을 알아봤다(C#을 다뤘다). 그리고 책에서 봤던 대로 테스트부터 짜려 했다. 그런데 잘 됐을까? 전혀 그렇지 않았다. 하루가 채 지나기도 전에 포기하고 말았다. 테스트 주도 개발을 직접 해보면 알겠지만, 이것은 상당히 극단적인(extreme)
기법이다. 실제 코드의 인터페이스가
어때야 할지, 사용자는 어떤 식으로 코드를 쓰려 들 것인지 판단하기가
쉽지 않다. 이런 판단을 금새 해내려면, 프로그래밍 언어(이 경우엔 C#)를 잘 아는 정도론 안 된다. 디자인 패턴 등을 익히고 설계 경험을 쌓은 후에야 가능하다.
테스트 주도 개발은 어렵다. 하지만 좌절할 일은 아니다. 테스트가 쉽다면 품질보증 팀이나 테스트 전문가의 존재 이유가 없지 않겠는가? 그러니 실망하지 말고 쉬운 일부터 하나씩 해나가자. 우선 다음의 “단위 테스트의 일상화”에서 제시하는 내용부터 실천해보자.
단위 테스트의 일상화
개발을 혼자 하면 편할지 모른다. 내가 의지만 있으면 빌드 서버를 도입하고 테스트 주도 개발을 할 수 있다. 하지만 학교 숙제가 아니고선 혼자 개발하는 일은 거의 없다. 조언을 얻을 사람이 생긴다는 장점이 있지만, 아무래도
뭔가 도입하고 시도해보긴 어려워진다. 개발자마다 성향이 무척 달라서
버전관리시스템조차 통제의 수단이라며 반발하는 경우가 적지 않다. 사정이 이렇다 보니 단위 테스트를 도입하기도 쉽지 않다. 강제하지 않으면 일년 내내 단위 테스트를 하나도 안 짜는 사람이 속출할 테고, 그렇다고 강제하자니 그것도 문제다. 예를 들어, 테스트 코드가 없는 클래스를 짜면 인사 평가에 반영하겠다고 해보자. 이러면 사람들이 열심히 테스트 코드를 짤까? 십중팔구 사람들은 회피 기술을 찾아낸다.
클래스가 많아질수록 귀찮은 일감이 늘어나니 아예 클래스를 안 만들기로 한다. 곧 몇 천 줄짜리 대형 클래스가 난무하게 된다.
이것은 누굴 비난할 일이 아니다. 중요하다고 생각하지 않는 일에 시간을 투자할 사람은 없다. 단위 테스트를 도입하려면 먼저 구성원들의 공감대를 이끌어내야 한다. 이는 팀장이나 관리자의 영역이다. 아무래도 일개 개발자로선 한계가 있다. 특히 관리자가
회의적일 땐 말이다. 그래서 이 방면에 대핸 더 언급하지 않겠다. 단, 관리자와 팀 구성원들의 공감대가 어느 정도 형성됐다면, 서두르지 말고 하나씩 실행해나가자. 이를테면, 하루에 단위 테스트를 하나만 짜보는 정책은 어떨까? 이 정도면 그리 힘들지 않을 것이다. 그리고 단위 테스트를 꼭 어렵게만 생각할 필요도 없다. 프로그래밍 언어에 대해 궁금한 것이 생겼을 때 활용해도 좋다.
[목록 1] 단위 테스트의 일상화
enum ACCOUNT_GENDER
{
AG_MALE = 1, //!< Male
AG_FEMALE = 2, //!< Female
AG_NOTDEFINED = 3, //!< Not Defined
AG_UNKNOWN = 4, //!< Unknown
AG_DEFAULT = AG_FEMALE,
AG_MAX
};
TEST(GenderValues)
{
DWORD expectedAgMax = AG_UNKNOWN + 1;
DWORD agMax = static_cast<DWORD>(AG_MAX);
CHECK(agMax == expectedAgMax);
}
[목록 1]은 C++의 enum이 어떤 식으로 처리되는지 궁금해서 짠테스트 코드다. 원래 AG_MAX의 값이 5가 아닐까 싶었는데, 정작 테스트해보니 6이 나왔다. 이런 식으로 단위 테스트를 여러 방면에 활용할 수 있다는 걸 보여주면 좋다. 특히 초심자일수록 이렇게 간단한 테스트 코드를 메인 함수에 넣고 돌려본다. 그리고 원하는 결과를 알아내면 테스트 코드를 지운다. 하지만 하루가 지나고, 한 달이 지난 후 어느 날
다시 똑 같은 코드를 짜는 일이 비일비재하다. 시간이 지나면 잊어먹기
마련이고, 그렇지 않더라도 또 다른 의문이 들 때가 있다. 똑 같은 일을 여러 번 반복하는 건
DIY(Don’t Repeat Yourself) 원칙에 어긋나는 일이니, 이렇게 단위 테스트를 활용하는 편이 좋다.
예외처리
그림 4. 충돌이 났을 때
코드에 문제가 있어서 단위 테스트가 실패해도 UnitTest++는 죽지 않는다. UnitTest++는 충돌이 난 코드 영역을 실패처리하고 다음 테스트를 실행한다. 이러한 안전성은 UnitTest++을 돋보이게 하는
장점이다. 하지만 테스트 자동화를 꾀할 땐 좀더 신경 쓸 일이
많다. 예외를 알리는 메시지 박스가 그 중 하나다. 충돌이 나면 보통 [그림 4]와 같은 메시지 박스가 뜬다. 단위 테스트 중에 충돌이 나도 마찬가지다. 차이가 있다면, “Don’t send”
버튼을 클릭했을 때 UnitTest++은 다음 테스트를 실행한다는 점이다. 문제는 “버튼을 클릭했을 때”란 대목이다. 다시 말해, 사람이 버튼을 눌러주지 않으면 다음 테스트가 실행되지 않는다는 말이다. 이런 문제 때문에 실제론 테스트가 실패했어도 그 사실을 모르는 경우가 잦다. 빌드 서버엔 오류 메시지 창이 떠 있고 빌드가 끝나지 않는다. 빌드가 끝나지 않으므로
CruiseControl .NET에 빌드의 성공 여부가 보고되지 않고, 사람들은 이전 빌드의 결과(성공)를 보고 안심하는 것이다.
다행히 이런 문제는 어느 정도 대처
가능하다. CHECK, VERIFY 같은 매크로를 (만들어) 쓰는데, 이때 crash 코드를 이용하곤 한다. 물론 crash 코드가 실행되면 오류 메시지 창이
뜬다. crash 대신 예외를 던진다면 어떨까? UnitTest++ 같은 단위 테스트 라이브러리엔 예외를 탐지해 대처하는 기능이 들어있다. 그러니 오류 창이 뜰까 걱정하지 않아도 된다. 물론
단위 테스트 상황이 아니라면 처리되지 않은 예외로 처리되어 종전과 같이 오류 창이 뜰 테니 이 점도 걱정하지 않아도 된다.
[목록 2] UnitTest++의 예외 매크로
struct TestException {};
CHECK_THROW(throw TestException(), TestException);
멀티 스레드에 대한 단위 테스트
애자일 코리아란 구글 그룹이 있다. 어쩌다 보니 요즘은 활동이 없는데 한때는 이런저런 의견을 활발히 교환하던 곳이다. 한번은 멀티스레드 코드를 어떻게 테스트해야 할까란 이야기가 오갔다. 이는 매우 흥미로운 토론이었는데, 그럴만한 게 멀티스레드
때문에 고생하지 않는 사람이 없기 때문이다. 더군다나 요즘엔 멀티스레드를
넘어 멀티코어, 멀티프로세서 시대가 되었기 때문에 문제가 더욱
심각해졌다.
이 논의는 다음과 같은 질문으로 시작됐다.
Multithread에 대한 Unit Test는 불가능할까요? 혹은 가능하더라도 별 효용이 없을까요?
레이스 컨디션과 같은 문제는 원인 파악이
힘들뿐더러 재현하기도 힘들다. 단위 테스트는 일종의 문제를 재현하는
과정인데, 그래서 이런 문제를 해결하기 위한 특별한 도구가 있다. 이 토론 때 언급된 도구로는 다음과 같은 게 있다.
- Multithreaded unit testing with ConTest
(http://www.ibm.com/developerworks/java/library/j-contest.html) - GroboUtils (http://groboutils.sourceforge.net/)
하지만 안타깝게도 이런 도구를 도입하기가 쉽지 않다. 도구 자체의 문제라기보단 사람의 문제가 크다. UnitTest++ 같은 간단한 도구조차 조직에 도입하기 힘든 판에 멀티스레드를 위한 단위 테스트라니!
결국 UnitTest++ 같이 지금 쓰는 도구를 최대한 이용하는 편이 좋다. 직접 lock이나 크리티컬 섹션 등을 활용해 레이스 컨디션 문제 등을 잡아내는 코드를 만들어야 한다. 결코 쉬운 일은 아니지만 참고할만한 지침은 있다.
xUnit
Test Patterns 에도 나오는 것 중에, Humble
object라는 것이 있습니다. ‘Test하기 어려운
환경적인 부분(thread scheduling)에 관련된 코드는 가급적이면 thin하게 만들어라’라는 것이죠.
참고. Humble Object (http://xunitpatterns.com/Humble%20Object.html)
로직을 테스트하기 쉬운 별도의 컴포넌트로 빼내어 환경과 분리시킨다.
그림 5. Humble Object
이 토론이 시작된 후 처음 달린 댓글이다. 결국 설계의 문제다. 최대한 싱글스레드 환경인 것처럼 짜고 멀티스레드를 지원하는 부분은 따로 빼낸다. 아마도 가장 본질적인 해결책일 것이다. 일단 인간의
사고능력 자체가 한번에 한가지만 집중하고 부수효과(side-effect)를 고려하지 않는 상황에 적합하기 때문에, 문제를
단순화시켜야 한다.
하지만 원칙적인 이야기는 상황이 급할
땐 적용하기 힘들다. 당장 레이스 컨디션 문제가 발생했는데, 설계를 바꾸라고 해도 난감하다. 정확히 뭐가 잘못됐는지 파악해야 설계를 바꿀 테고, 설사 원인 파악이 끝났다 해도 시급히 해결해야 할 문제라 설계를 바꾸는 건 미뤄야 할지도 모른다. 이런 경우엔 테스트를 대충 짜는 방법이 있다.
이상하게 들릴진 몰라도 “대충” 짜는 게 좋을 때가 있다. 레이스 컨디션 문제가
발생했고 그 문제를 재현할 코드가 필요하다고 해보자. 매번 정확히
문제를 짚어내는 코드를 짜려면 힘들다. 그래서 멀티스레드 프로그래밍이
어려운 것이다. 이럴 땐 확률적으로 접근해보자. 열 번 실행해서 한번 꼴로 실패하는 정도라면 어떨까? 대부분의 상황에선 그 정도면 충분하다.
당장 문제를 재현할 수도 있거니와 나중에 빌드 자동화시 문제를 재현하기에도 충분하다. 어차피 소스 코드를 커밋할 때마다 빌드하고 테스트할 테니, 하루에 열 번 이상 테스트가 실행된다.
그러면 똑 같은 문제가 재발하더라도 그 날 바로 알 수 있다.
이는 단순한 이론이 아니다. 실제로 내가 속한 개발팀은 이렇게 해서 문제를 짚어내고 풀어내왔다. 코드가 꽤 복잡해 제시하진 않았지만, 두 개의 스레드가 변수 값을 증가시키는 테스트 코드가 있다. 대충 이런 식이다. 초기값이 0인 변수를 1씩 증가시키는데, 각 스레드가 이런 일을 다섯 번
한다. 그러면 최종 값이 10이 나와야 한다. 물론 동기화에 문제가 있더라도 운 좋게 최종 값이 10이 나올 수 있다. 하지만 테스트를 열 번, 스무 번 실행시키면 어떨까? 한번 정도 깨진다면 그걸로 충분하다. 하루나 이틀
안에 문제가 재현될 테니 말이다.
부록. 지속적인 통합 팁 – Visual
UnitTest++
UnitTest++은 기본적으로 콘솔 애플리케이션이다. NUnit처럼 화려한 GUI를 제공하지 않는다. 그런데 여러분은 운이 좋다. 비주얼 스튜디오 애드온이 있기 때문이다.
더군다나 한국인 개발자의 성과물이라 더욱 인상적이다.
Visual Studio 2005와 2008을 지원하며, UnitTest++ 뿐 아니라 CppUnitLite, BoostTest 등도 지원한다. http://code.google.com/p/vutpp/에 방문해서 다운로드 받고 자세한 사양을 확인하면 된다.
끝마치는 말
이번 칼럼에선 단위 테스트를 일상화하자고 제안했다. 또한 단위 테스트를 적용하다 겪기 마련인 문제도 하나씩 알아봤다. 이런 정보가 여러분에게 도움이 되길 바란다. 다음 시간부턴 MSBuild를 이용하는 법에 대해 알아보겠다.
좋은 글 잘 읽고 있습니다.
본문중,
” 똑같은 일을 여러 번 반복하는 건 DIY(Don’t Repeat Yourself) 원칙에 어긋나는 일이니, 이렇게 단위 테스트를 활용하는 편이 좋다. “
중, DIY가 DRY로 변경되어야 할 것 같습니다.
옳은 지적이십니다. 방금 수정했습니다.
어떻게 UnitTest++을 잘 사용할 수 있는지 알려 주셔서 정말 고맙습니다.
최근에 진행 중인 프로젝트에선 Google Test를 씁니다. 사실 대부분의 경우엔 그리 큰 이점이 없긴 합니다만, 나중엔 이 라이브러리도 다뤄보겠습니다. ^^