알라딘MGG와이드바


바쁠수록 돌아가기 : 테스트 주도 개발로 더 좋은 게임 만들기 개발 이야기

출처 : http://www.gamesfromwithin.com/articles/0603/000107.html
시작 : http://betterways.tistory.com/96

바쁠수록 돌아가기 : 테스트 주도 개발로 더 좋은 게임 만들기
번역 : 박일.
http://ParkPD.egloos.com
rigmania@naver.com

Game Developers Conference 2006

Noel Llopis llopis@convexhull.com
Sean Houghton sean.houghton@gmail.com

High Moon 스튜디오

프로그래머는 거의 대부분 정신적인 작업을 한다는 점에서 시인과 비슷합니다. 프로그래머는 공중에, 공기로부터 상상력을 발휘해서 자신의 성을 만들어 나갑니다. 이렇게 유연하고 쉽게 다듬고 고쳐서 거대한 구조를 만들어낼 수 있는 창작 매체를 찾아보기란 쉽지 않습니다.
Frederick P. Brooks, Jr.

1. 소개
프로그래밍의 큰 매력 중 하나는, 브룩스가 지적한 대로, 우리가 무로부터 공중에 떠 있는 아름다운 성을 만들 수 있다는 점입니다. 대상 플랫폼의 메모리와 파워가 발전함에 따라, 우리의 성도 더욱 크고 복잡해져 갑니다.

하지만, 희박한 공기 중으로부터 코드를 구현하기란 정말 위험하고 어려운 일입니다. 코드는 쉽게 처음 예상을 깨고 우리가 이해할 수 없을 정도로 복잡해져 갑니다. 우리가 만들어 놓은 공중의 성은 금새 눈덩이 같이 커져서 원하는 모습으로 바꾸기 힘들게 되어 버립니다.

프로젝트가 진행되는 동안 개발이 점점 느려져 막판에는 기어가는 것은 누구나 경험해 보았을 것입니다. 교묘하게 발생하는 여러 얼굴의 벌레(버그)들을 박살내는 것은 정말 힘든 일입니다. 다른 사람이 작업한 코드를 가지고 계속 씨름하다가 결국은 포기하고는 절망 속에서 새로 다 짜 보기도 했습니다. 게임이 빌드가 안 되는 바람이 몇 시간 동안이나 아무 것도 못 하고 투덜투덜하면서 기다린 적도 있습니다. 오랜 시간 동안 작업한 코드가 다음 프로젝트에서 사용되지 못하고 버려진 적도 있습니다. 물론 처음부터 새로 짜야 했습니다.

코드는 그 자체로도 쉽게 너무 복잡해집니다. 마일스톤의 압박, 요동치는 게임 산업, 팀원과 비용의 증가, 따라가기 힘든 하드웨어의 변화들은 이 힘든 상황을 더욱 더 어렵게 만듭니다.

이런 것들을 해결해 보기 위해 테스트 주도 개발이 시작되었습니다.

2. 테스트 주도 개발은 무엇일까요?

전통적인 개발 과정을 생각해 봅시다. 어떤 기능을 개발하려면, 먼저 이 문제를 작은 단위의 문제들로 나눈 후 하나씩 구현하기 시작합니다. 구현한 뒤에는 문제가 없는지 보기 위해 컴파일 합니다. 한참 작업한 후에 게임을 돌려볼 만큼 충분한 기능을 구현했다면, 직접 게임을 돌려보면서 잘 돌아가는지 확인해 봅니다. 레벨을 돌아다니면서 기능을 하나씩 테스트 해 보고, 이상하게 깨지는 곳은 없는지 확인해 봅니다. 가끔은 원하는 대로 돌아가는지 보기 위해 디버깅 포인터를 찍어서 코드 안으로 들어가 볼 수도 있을 겁니다. 잘 돌아간다면 소스 저장소에 코드를 집어넣은 후 다음 작업으로 갈 수 있겠죠.

테스트 주도 개발(TDD) 는 이런 개발 과정을 반대로 합니다. 기능을 작은 단위로 나누는 것까지는 똑같습니다. 하지만, 그 다음에 단위 테스트 코드를 먼저 만들고 컴파일 해서 에러가 나는 것을 확인합니다.(에러가 나는 게 당연하죠. 하나도 구현해 놓은 게 없잖아요) 이제, 테스트를 통과할 수 있도록 코드를 작성합니다. 이런 과정을 계속 반복하다 보면, 아주 작은 기능들이 모여서 원하는 전체 기능을 구현할 수 있습니다. 주기는 매주 짧아서 한 번에 1-2 분도 채 안 걸립니다.

TDD 주기에 대해서 좀 더 알아봅시다.
- 아주 작은 단위의 기능에 대해 하나의 단위 테스트를 구현한다.
- 돌려서 ‘실패’하는지를 확인한다. (C++ 에서는 아예 컴파일이 안 되는 경우도 있습니다.)
- 최소한의 코드로 컴파일 되고 테스트가 돌아갈 수 있도록 만든다.
- 코드(와 테스트)를 리펙토링 한 후 돌려서 ‘통과’하는지 확인한다.

‘단위 테스트’(개발자 테스트라고도 불립니다.) 란 시스템의 단순한 기능 하나가 잘 돌아가는 것을 보장하는 테스트입니다. 특별히 여기에서는 1분 안에 끝날 수 있는 정말 작은 기능들만 단위 테스트에서 다루도록 하겠습니다. 예를 들어, 단위 테스트에서는 플레이어의 생명치가 최대일 때 HP 아이템을 먹어도 더 이상 생명이 늘어나지 않는다던지, 특수한 이펙트는 '깊이 쓰기'를 무시한다던지 하는 것을 테스트 할 수 있습니다. 다음 장에서 간단한 예를 보도록 합시다.

TDD 는 소프트웨어의 복잡성에 대해서 우리를 어떻게 도와줄 수 있을까요? 그리고, 앞에서 소개했던 문제들을 해결하는 데 어떤 도움을 줄 수 있을까요? 저희가 생각한 중요도 순으로 TDD 가 해 줄 수 있는 것들을 한 번 나열해 보았습니다.

. 더 나은 코드 디자인(1/4)
TDD로 코딩을 할 때 가장 먼저 해야 하는 일은 바로 테스트 코드를 짜는 것입니다. 그 덕분에 모든 코드는 어떻게 구현 할 것인가 이전에 어떻게 사용될 수 있나를 생각하고 작성하게 됩니다. 결과적으로 작성된 코드는 훨씬 협업하기 쉬운 형태가 되고, 문제를 쉽게 해결할 수 있게 합니다. 또한 테스트를 위해서 만들어진 코드는 나머지 코드와 격리된 상태로 개발되기 때문에, 간결하고 규격화될 수 있습니다.

. 안전망 (2/4)
TDD를 사용하는 경우, 대부분의 코드는 연관된 테스트를 가지게 됩니다. 덕분에 우리는 가차없는 리펙토링을 할 수 있습니다. 또한 테스트를 전부 다 통과하는 것을 보고, 리펙토링한 코드가 별 문제없이 잘 돌아가겠구나 라는 걸 알 수 있게 됩니다. 리펙토링은 개발 프로세스 중에서 진짜 중요한 부분입니다만, TDD 같은 안전망이 그 이전에 선행되어야 합니다. 극강의 최적화를 위해서 코드를 막 꼬아놓더라도 TDD 덕분에 별 문제가 없다는 걸 확인할 수 있습니다. 마감 막바지에도 훨씬 맘이 편하게 새 기능을 추가하거나 수정할 수 있습니다.

. 즉각적인 피드백(3/4)
단위 테스트는 우리에게 1분 단위로 몇 번 씩이나 즉각적인 피드백을 제공합니다. 덕분에 어떤 테스트가 실패하기 시작했을 때 우리는 1시간 전도, 10분 전도 아닌 바로 1분 전에 작업한 무언가를 잘 못 되었다는 것을 알 수 있게 됩니다. 최악의 경우에는 그냥 1분 전 코드로 롤백한 후에 다시 작업하면 됩니다. 꽤나 주관적인 것이지만, 이런 계속적인 피드백은 사람들의 코드에 대한 윤리 의식을 놀랄만큼 향상시켜 줍니다. 이렇게 한 걸음 한 걸음 조금씩 작업해 나가면서 피드백을 받는다면 큰 문제가 생길 수 없습니다. TDD 덕분에 저희들은 8 비트 시절에 처음 느꼈던 프로그래밍의 재미를 다시 느낄 수 있었습니다.

. 문서화(4/4)
주석 없는 코드보다 더 최악인 것은? 옛날 주석이 남아있는 코드입니다. 코드를 수정할 때 주석은 그대로 두는 경우가 많습니다만, 어떻게 하기가 어렵습니다. 단위 테스트는 효과적인 문서화 방법을 제공합니다. 우리는 단위 테스트들을 쭉 훑어보면서 클래스를 어떻게 사용할 수 있는지, 함수를 실행시킬 때 파라메타에 대해서 어떤 가정을 하는지 등을 알 수 있습니다. 심지어, 필요 없어 보이는 코드를 제거한 후 테스트를 돌려서 어떤 문제가 생기는지를 확인할 수도 있습니다. TDD 가 문서화로서 가장 좋은 점은 항상 최신 상태를 유지한다는 점입니다. TDD로 개발된 코드를 살펴보았더니 주석은 거의 다 사라지고 왜 이렇게 구현했는지 의도를 설명하거나, 어떤 논문의 알고리즘을 사용했는지 정도만 남아있는 것을 볼 수 있었습니다.

무엇보다 가장 강조하고 싶은 점은 TDD 가 단위 테스트만을 의미하지 않는다는 것입니다. TDD 는 무언가를 테스트 하는 방법이라기 보단 개발 방법론 그 자체입니다. 그렇기 때문에 TDD 가 더 좋은 코드 디자인과 구조를 만들어 주고, 리펙토링을 쉽게 할 수 있게 하는 것입니다. TDD 은 코드가 정확하게 동작하는지를 보장하기 보다는, 여러분이 그 코드로 하고 싶은 것을 할 수 있도록 보장해 줍니다.

3. TDD 구현하기

이제 직접 간단한 첫 번째 테스트를 작성해 보면서 TDD를 시작해 봅시다. 게임이 시작하고 돌아갈 수 있을 정도로 구현되어 있다고 생각해 봅시다. 지금 해야 할 일은 'HP 아이템' 기능입니다. 플레이어가 돌아다니다가 HP 아이템 위를 지나가면 HP 가 늘어나는 기능을 만들어야 합니다. 처음 시작치고는 좀 어려우니까 좀 더 간단하게 테스트를 만들어 봅시다. 플레이어와 HP 아이템이 있지만, 둘은 서로 떨어져 있는 경우를 생각해 봅시다. 이 때 플레이어의 HP 는 아무런 변화가 없어야 합니다. 간단하지 않나요?

자, 어떻게 테스트 코드를 만들어 볼까요? 이런 식의 코드가 나오지 않을까요?

World world;
const initialHealth = 60;
Player player(initialHealth);
world.Add(&player, Transform(AxisY, 0, Vector3(10,0,10));
HealthPowerup powerup;
world.Add(&powerup, Transform(AxisY, 0, Vector3(-10,0,20);
world.Update(0.1f);
CHECK_EQUAL(initialHealth, player.GetHealth());

원하는 테스트 코드는 다 들어갔습니다. 이제 매크로를 이용해서 이게 어떤 테스트인지를 기록하도록 해 봅시다. 전체 코드는 다음과 같습니다.

TEST (PlayersHealtDoesNotIncreaseWhileFarFromHealthPowerup)
{
World world;
const initialHealth = 60;
Player player(initialHealth);
world.Add(&player, Transform(AxisY, 0, Vector3(10,0,10));
HealthPowerup powerup;
world.Add(&powerup, Transform(AxisY, 0, Vector3(-10,0,20);
world.Update(0.1f);
CHECK_EQUAL(initialHealth, player.GetHealth());
}

TEST 와 CHECK_EQUAL 매크로는 단위 테스트 프레임워크의 일부입니다. 이 프레임워크는 테스트를 쉽게 작성하고 실행할 수 있도록 만들어 졌습니다. 어떻게 더 쉬울 수 있겠어요? C++ 와 다른 언어용으로 만들어진 단위 테스트용 프레임워크는 다양하고 공짜로 사용할 수 있습니다. 저희가 추천하는 UnitTest++ 는 여러 플랫폼을 지원하는 프레임워크입니다. 이 프레임워크는 가벼워서 쉽게 적용하고 porting 이 쉬우며 TDD 가 게임 산업에 사용되기 시작한지 몇 년이 지난 후에 작성되었습니다.

이제 아까의 코드를 컴파일하고 결과가 어떻게 나오는지 봅시다.
Running unit tests...
1 tests run
There were no test failures.
Test time: 0 seconds.

CHECK_EQUAL 매크로는 실제로 테스트를 확인하는 부분입니다. 이것은 기대값과 실제값을 입력으로 받습니다. 만약 둘이 다르다면 테스트를 '실패'로 표시하고 추가 정보와 함께 결과에 알려줍니다. 보너스로, UnitTest++ 은 '실패' 포멧을 Visual Studio 에 맞게 보여줌으로서 쉽게 어디에 에러가 있는지 알 수 있게 해 줍니다.(역자 : 에러문을 더블클릭하면 실패한 코드 위치로 이동한다는 뜻인 듯)

왜 CHECK_EQUAL 같은 매크로를 사용할까요? 단위테스트를 완전 자동화 시키기 위해서 입니다. 눈으로 확인해야 하는 검사라던지, 몇 줄의 글을 읽어야 할 필요는 없습니다. 테스트 프로그램은 테스트가 실패했는지 여부를 확실하게 알 필요가 있고, 이런 방법은 쉽게 빌드 프로세스에 들어갈 수 있습니다.

4. 어떻게 테스트 해야 하나?
단위 테스트를 작성할 때는, 여러분의 코드를 테스트 하는 3가지 방법이 있습니다.

. 리턴값(1/3)

함수를 호출하고 리턴값을 검사합니다. 가장 직접적인 검사방법이고, 계산관련 함수들을 테스트하는 최고의 방법입니다. 가장 쉬우면서도 명확한 방법이어서 가능하면 이 방법을 최대한 자주 사용해야 합니다.

예를 들어, GetNearestEnemy() 같은 함수가 딱 이 경우에 맞겠네요. 몇 몇의 악당들을 월드에 놓은 후에 이 함수를 호출한 후, 우리가 원하는 결과값인지 여부를 확인할 수 있습니다.

함수 호출 결과를 불린 값으로 리턴하는 함수를 테스트할 때는 주의하세요. 우리는 이 함수가 진짜 제대로 돌아가고 있는지를 테스트하고 싶은거지, 함수가 '나 잘 돌아가고 있어요'라고 말하고 있는지를 검사하고 싶은 건 아닙니다.

. 객체 상태(2/3)

함수를 호출한 후에, 객체나 시스템의 상태가 올바르게 변경되었는지를 검사합니다. 이 테스트는 상태가 올바르게 변했는지를 테스트하기 때문에 직관적인 테스트입니다.

예를 들어, '한가한' 상태의 인공지능 주변에다가 '인기척' 이벤트를 보냈을 때, 상태가 '경계' 상태로 변하는지를 검사해 볼 수 있을 것입니다.

이런 테스트를 위해서 때때로 private 으로 정의되어 있는 상태값에 대한 getter 함수를 만들 필요가 있습니다. 하지만, 사용자 입장에서 테스트 하기 위해서 이런 게 필요하다면 public 으로 만드는 게 그리 나쁜 생각은 아닐 수 있습니다. 또한 인터페이스가 필요 이상으로 커질 수 있습니다만, 이 경우에는 그 클래스가 두 개로 나뉘어져서 하나를 포함해야 한다는 신호라고 생각할 수 있습니다.

. 객체 상호작용 (Object interaction) (3/3)
가장 어려운 방법입니다. 어떤 함수를 호출할 때, 테스트 중인 객체가 다른 객체와 순차적으로 특정 작업을 진행하게 하고 싶을 수 있습니다. 우리는 객체의 상태는 관심이 없고, 원하는 함수들이 잘 호출되는지만 알고 싶을 수 있습니다. 이런 경우 가장 일반적인 테스트 패턴은 mock 객체입니다. mock 객체란 특정 객체와 똑같은 인터페이스를 가집니다. 단, 하는 일이라고는 테스트를 도와주는 것 뿐입니다. 예를 들어, mock 객체는 어떤 함수들이 호출되었는지, 어떤 값들이 넘어왔는지를 기록할 수 있습니다. 또한 멤버함수가 호출되었을 때 하드코딩되어 있는 값을 리턴하게 할 수도 있습니다.

이 방법은 테스트 방법 중 가장 고난이도입니다. 앞의 1, 2번 방법으로 해결 안 되는 경우에만 사용하기를 권장합니다. mock 객체를 사용해야 한다는 것 자체가 코드가 무거운 객체와의 복잡한 상호작용히 필요하다는 신호(혹은 냄세)가 될 수 있습니다. 이 경우, 서로 연관이 적은, 작은 여러 객체들에 의존하도록 코드를 수정하는 게 좋습니다.

mock 객체를 사용할만한 좋은 경우가 HUD 랜더링 테스트입니다. HUD 안의 내용들이 특정 순서대로 잘 그려지는 지를 검증하고 싶다고 합시다. 랜더링 캔버스를 위한 mock 객체를 만들어서 요소들을 전달받은 순서대로 저장하게 합니다. 이 가짜 (mock) 랜더링 캔버스를 HUD 랜더러에 넘긴후에 렌더링 함수를 호출해서 mock 캔버스 안의 리스트가 기대했던 순서대로 들어가 있는지를 확인해 볼 수 있습니다.

5. 가장 중요한 실천법들
저희가 TDD 로 개발하면서 정말 중요하다고 생각한 실천법들을 소개합니다.

. 테스트를 자주 돌려라. (1/5)

테스트를 쉽게 만들 수 있도록 만드는 것은 TDD 를 성공적으로 도입하기 위해 무엇보다 중요합니다. 어느 누구도 이번 마일스톤에 들어가야 하는 기능을 급하게 구현하는 동안에 시간을 더 써가면서 단위 테스트를 작성하고 싶지 않을 것입니다. 하지만 더 중요한 것은, 테스트는 정말 자주 실행되어야 하고, 테스트 '실패'(failed) 는 빌드 실패랑 마찬가지로 다루어져야 한다는 점입니다.

High Moon 에서는, 모든 라이브러리는 그 라이브러리와 링크되는 실행파일을 생성하는 테스트 프로젝트를 가지고, 이것으로 모든 테스트를 실행합니다. 저희는 Visual Studio 의 post-build 단계에 이 바이너리들을 실행시키도록 등록했습니다, (또는 make file 의 마지막 command 를 이용했습니다.) 덕분에 라이브러리에 작은 변경이라도 있을 경우 항상 테스트가 실행될 수 있도록 만들 수 있었습니다. 더해서, 테스트 실행파일은 '실패'한 테스트 갯수를 리턴하게 되어 있습니다. 덕분에, 1 이상의 테스트 실패 갯수를 출력했을 경우 빌드 체인에서 이것을 실패 빌드로 해석하도록 만들 수 있었습니다. 이런 환경은 모든 사람이 테스트 코드를 실행시키도록 만들었고, 빌드 안정화에 엄청난 도움을 주었습니다.

로컬에서 테스트하는 것 뿐만 아니라, 빌드 서버에서도 빌드때마다 단위 테스트들을 실행하게 했습니다. 저희 경우에는 postbuild 단계에서 테스트를 돌렸기 때문에 빌드 서버에 따로 더 설정을 만들 필요가 없었습니다. 실패 테스트는 컴파일 실패와 똑같이 처리되었습니다.

. 테스트 중인 코드만 테스트 하기 (2/5)
이것은 좋은 단위 테스트를 만들기 위한 중요한 실천법입니다. 테스트와 관련된 코드의 양을 최소화 해야 하는 가장 중요한 이유는 테스트를 간단하게 유지하기 위함입니다. 어떤 것이 잘 돌아가지 않을 때 실패하는 몇 개의 테스트만 집중적으로 분석해서 문제를 해결할 수 있다면 더 이상 바랄 것이 없습니다. 만약 어떤 테스트들이 여러 코드에 전반적으로 연관되어 있다면, 매번 문제가 생길 때마다 아무 이유 없이 같이 실패할 수 있습니다.

또한, 테스트 코드를 라이브러리나 다른 코드와의 의존성이 최소가 되도록 작성할 수 있다면, 그 코드는 매우 모듈화가 잘 된, 독립적인 코드일 것입니다. 이런 코드는 전반적인 디자인을 깔끔하게 만드는 데 도움이 됩니다. 마지막으로, 테스트 코드가 다른 코드와의 연관을 줄여야 하는 이유는, 이렇게 했을 때 테스트 초기화와 종료를 따르게 만들 수 있기 때문입니다.

'HP 아이템' 의 예를 생각해봅시다. 이 테스트를 위해서는 월드 객체와 플레이어 객체, 그리고 HP 아이템 객체가 필요합니다. 그래픽 시스템은 초기화할 필요가 없고, DB 등도 마찬가지 입니다. 이렇게 생각해 보면 월드 객체는 다른 어떤 의존도 필요없는 가벼운 컨테이너라는 것을 알 수 있습니다. 이런 것을 여러분이 지금 사용하고 있는 게임 엔진에 적용해 보려고 시도해 보십시요. 얼마나 많은 '암시적'(implicit) 의존이 필요한지를 알게 되면 깜짝 놀랄 것입니다.

많은 시스템과 연결되어 있는 코드 전반에 대해서 하는 테스트를 일반적으로 '기능 테스트 functional test' (혹은 소비자 테스트) 라고 합니다. 기능 테스트도 매우 유용하며, 특히 자동화 할 수 있다면 더욱 그렇습니다. 다만, 기능 테스트는 단위 테스트와는 다른 영역을 테스트 한다는 점을 기억해야 합니다.

. 테스트를 간단하게 유지하기(3/5)
이번 내용은 앞에서 한 얘기와 관련 있습니다. 즉, 우리는 하나의 테스트가 하나의 기능만 테스트 하도록 만들어야 합니다. 이렇게 만들 수 있다면, 뭔가 잘못되었을 때 어떤 녀석이 문제를 일으키는지 금방 찾아낼 수 있습니다.

가장 먼저 할 일은 테스트가 어떤 의미를 가지는지를 정확하게 써 두는 것입니다. 예를 들어, 'PlayerHealth' 라는 이름은 그다지 큰 도움이 되지 않습니다. 'PlayerHealthGoesUpWhenRunningOverHealthPowerup' 정도는 써 주세요.

각 단위 테스트 코드 라인을 몇 줄 안 되게 유지하면 한 눈에 이해하기가 쉬워집니다. 저희 경우에는 테스트가 15 라인 이상인 경우가 거의 없었습니다. 테스트 코드 라인이 길다는 것 자체가 뭔가 많은 것과 연관되어 있다는 신호입니다. 분명 좀 더 좋게 다시 코딩할 방법이 있을 겁니다.

또한, 한 단위 테스트당 1~2개 이하의 체크문만 가지도록 하면, 코드를 쉽게 이해 할 수 있습니다. 이렇게 하려면, 가끔은 테스트를 위해 중복 코드를 짜야 할 경우가 있습니다만, 이건 테스트를 간단하게 만들어서 얻는 이득에 비하면 아무 것도 아닙니다.

만약, 여러 번 중복 코드를 만들어야 한다면 fixture 라는 걸 사용할 수 있습니다. fixture 는 테스트 시작/종료 때 자동으로 실행되는 공통코드입니다. fixture 를 이용하면 우리가 직접 코딩해야 하는 작업량을 훨씬 줄일 수 있습니다. 괜찮은 단위 테스트 프레임워크는 모두 fixture 를 지원합니다. 가능하면 최대한 활용하도록 하십시요.

. 테스트를 독립적으로 유지하세요(4/5)
단위 테스트는 서로 독립적이어야 합니다. 한 테스트에서 객체를 만들고, 다음 테스트에서 그 객체를 재사용 할 경우에 앞서 보았듯이 문제를 자초할 수 있습니다.
우리는 테스트가 실패할 때 실패한 테스트만을 집중해서 살펴보고 거기에서 테스트를 실패하게 만든 문제점을 찾고 싶은 것입니다. 그러나 테스트가 다른 테스트와 연관되어 있다면 하나의 테스트가 실패할 때 다른 테스트도 줄줄이 실패해서 어떤 게 진짜 문제가 되는지 찾기 어렵게 될 것입니다.

. 테스트를 빠르게 유지하세요 (5/5)
저희가 작성했던 단위 테스트는 postbuild 단계에서 컴파일되고 실행되었습니다. 라이브러리 별로 수 백에서 수 천의 테스트를 만들어 놨기 때문에, 테스트가 빠르게 실행되지 않는다면 테스트 자체가 방해가 되었을 것입니다. 단위 테스트가 최소한의 코드만을 다룬다면 정말 빠르게 돌릴 수 있을 것입니다. 즉, 단위 테스트는 하드웨어를 무시해야 하고, 시작/종료가 오래 걸리는 시스템이나 파일 I/O 가 필요 없도록 작성되어야 합니다.

저희가 만든 단위 테스트는 모두 다 제한 시간이 걸려 있고 (UnitTest++ 의 기능 중 하나입니다.) 전체 테스트의 시간이 출력됩니다. 일반적으로 한 단위 테스트가 2초 이상 걸린다면 무엇인가 잘못되었고 수정되어야 합니다.(사실 단위테스트 하나가 1ms 가 걸린다면 - 그래도 단위 테스트로서는 엄청 오래 걸리는 것입니다만 - 1000 개의 테스트를 1 초 안에 돌릴 수 있다라고 생각한다면 그리 큰 것은 아닙니다.)

6. TDD 와 게임 개발
TDD 를 게임 개발에 적용하기 위해서는 다른 분야에서는 찾아볼 수 없는 난제들이 숨어 있습니다.

. 다른 플렛폼들(1/5)

요즘 대부분의 게임 개발자들은 다양한 플렛폼에서 개발해야 합니다. PCs(윈도우즈, 맥, 리눅스), 게임 콘솔, 핸드폰이나 휴대용 게임기 등. High Moon 에서는 콘솔용 게임을 개발하고 있습니다만, 좋은 개발툴을 제공하고 빠르게 개발 주기를 반복할 수 있는 윈도우즈를 주 개발 환경으로 이용하고 있습니다. 이러기 위해 항상 엔진과 툴의 윈도우용 버전을 가지고 있고, 덕분에 단위 테스트를 쉽고 편리하게 할 수 있었습니다.

단위 테스트를 다른 플렛폼에서도 돌려보고 싶었습니다만, 이러기 위해서 각 플렛폼들의 단점을 메우기 위해 몇 가지 타협안이 필요했습니다. 필요한 최소한의 기능은 command line 을 통해서 실행파일을 실행하고, 출력값과 - 할 수만 있다면 - 리턴 코드를 얻는 것이었습니다. 놀랍게도, 저희가 개발하던 게임 콘솔 환경 중 어떤 것도 이런 간단한 기능을 제공하는 것이 없었습니다. 결국, 저희가 직접 시스템 API 를 이용해서 저희가 원하는 기능들을 구현해야만 했습니다.

이런 유틸리티 프로그램들을 작성하고 나서야, 콘솔에서도 윈도우즈에서 하듯이 단위 테스트를 돌릴 수 있게 되었습니다. 딱 하나 차이점은 너무 느린 시작 시간이었습니다. 단지 몇 초 정도가 걸리긴 했지만, 이것은 매번 빌드할 때마다 postbuild 단계에서 하기에는 너무 오래 걸리는 것이었습니다. 게다가 모든 개발자 자리에 콘솔용 개발 툴팃을 가지고 있는 것도 아니어서, 언제나 콘솔 테스트를 할 수 있는 것도 아니었습니다. 이런 이유로 해서, 윈도우즈 외의 다른 플렛폼 용 단위 테스트는 빌드 서버에서만 수동으로 실행시키도록 했습니다. (역자 : 개발 툴팃이 한 개 밖에 없었나 봅니다. :) ) 이것은 이상적이진 않았지만, 대부분의 문제들은 윈도우즈 테스트 환경에서 다 잡혔기 때문에 큰 문제가 없었습니다.

. 그래픽스, 미들웨어와 다른 API 들 (2/5)
게임 개발에 TDD 을 적용하기에 가장 힘든 부분 중의 하나가 그래픽스를 어떻게 할 것인가 일 것입니다. TDD 를 소개하는 책에는 보기 힘들겠지만, 충분히 가능한 주제입니다.

먼저 기억해야 하는 점은 그래픽스는 단순히 게임의 한 요소일 뿐이라는 점입니다. 그래픽스에 관련된 모든 코드를 하나의 라이브러리에 모아놓고, 다른 코드는 하드웨어나 그래픽스 API 에 독립적으로 개발하는 것은 좋은 개발 방법입니다. 일단 이런 구조를 만들고 나면, 우리는 그래픽스에 상관없이 다른 부분들을 테스트 할 수 있게 됩니다.

사실, 저희는 그래픽스 렌더링 라이브러리도 TDD 로 개발하고 싶었습니다만, 특정 플렛폼 용 그래픽스 함수를 호출하는 것은 어쩔 도리가 없었습니다. 다음 3 가지 방법을 시도해 보았는데, 복잡한 순으로 소개해 드리겠습니다.

. 전체 그래픽스 함수를 추적하기 (1/3)
우리는 그래픽스 렌더러와 그래픽스 API 사이에 중간 레이어를 하나 두었습니다. 덕분에 아래 하드웨어가 어떤 것이든지 상관없이 원하는 그래픽스 API 를 호출할 수 있게 되었습니다. 보너스로 이 레이어는 어떤 함수가 어떤 파라메타와 함께 호출되었는지를 알려줄 수도 있었습니다. 이 방법은 테스트하기에 정말로 적합합니다. 테스트 레이어는 최종 빌드때에는 제거할 수 있으므로, 성능에도 아무런 문제가 없었습니다. 그러나, 이 방법은 꽤나 노동이 필요했습니다. 특히나 Direct3D API 같은 경우에는 OpenGL 과는 달리 클래스를 사용하고 복잡한 함수가 많아서 더욱 더 어려웠습니다.

. 상태 확인하기 (2/3)
다른 방법으로는 그래픽스 하드웨어에 직접 어떤 작업을 한 후에 API 를 이용해서 하드웨어 상태를 쿼리해 와서 결과를 비교해 보는 것입니다. 예를 들어, 특정 메시를 그리기 위해서는 버텍스 정의가 셋팅되어야 하는 경우가 그렇습니다. 이 방법은 훨씬 쉽습니다만, 테스트 할 수 없는 것들이 있습니다. 메시 하나를 그리고 싶을 때 얼마나 많은 삼각형들이 하드웨어에 전달되는지를 알 방법이 없습니다. 또한 OpenGL 은 꽤나 상태기반(state-based) 적이어서 대부분의 함수는 호출된 후에 OpenGL 의 상태를 초기화 시켜주어야 하므로, 테스트에서 상태를 확인하기가 쉽지 않습니다. 그래픽스 API 내부가 많이 공개되어 있는 플렛폼에서는 그래픽스 command buffer 를 검사해 봄으로서 더 많은 상태를 검사해 볼 수 있습니다.

. 그래픽스 함수를 분리 (3/3)
이것 저것 다 안 된다면, 외부 API 호출부분을 처리할 수 있는 좋은 방법이 있습니다. API 함수들을 하나의 함수에 다 몰아넣은 후에, 여러분의 함수가 제때 호출되는지 테스트 하십시요. 그래픽스 API 를 호출하는 함수는 테스트되지 않을 테지만, 그 함수가 하는 일이라고는 API 를 직접 호출하는 것 밖에 없습니다. 진짜로 테스트 하고 싶은 것은 우리 코드가 제대로 돌아가는지 확인하는 것이지, 그래픽스 API 가 문서대로 돌아가는지를 보고 싶은 건 아니라는 걸 기억하세요.

처음에 우리는 사용하는 모든 API 를 감싸는 첫 번째 방법을 시도해 보았습니다. 하지만, Direct3D 나 OpenGL 같이 복잡한 API 를 감싸기 위해서 해야 할 일들은 끝이 없었습니다. 아마도 상용 그래픽스 엔진 위에서 작업하는 경우에서는 이런 작업이 의미가 있었을 수도 있습니다. 저희 경우에는 몇 주 정도 해 본 후에, 들이는 시간에 비해 얻는 게 적다는 것을 깨달았습니다. 또한 wrapping 코드를 작성해야 한다는 것 때문에 새로운 API 를 쓰지 않으려 하는 부작용까지 있었습니다.

결국 우리는 2 번째와 3 번째 방법을 조합해서 사용하는 것에 만족해야 했습니다. 즉, 최대한 상태를 테스트하고, 나머지 것들은 API 호출 자체를 격리시키는 것이었습니다. 이 방식의 단점 중 하나는 하드웨어를 직접 제어하기 때문에 매 테스트마다 그래픽스 하드웨어 시스템을 올렸다 내렸다 (그래픽스 시스템을 한 번만 초기화 할 수 있는 플렛폼은 제외하고) 해야 했고, 이것은 엄청난 시간 낭비였습니다. 장점을 꼽자면, 테스트들이 그래픽스 API 와 하드웨어를 직접 제어했기 때문에 첫 번째 방법에서 잡을 수 없는 문제점들을 찾을 수 있다는 점입니다.(함수에다가 잘못된 파라메타를 넘긴다던지 하는 것들이죠)

이 3 가지 방법은 어떤 미들웨어나 외부 API 용 테스트에 사용될 수 있습니다. 꼭 기억해야 할 점은 우리는 API 가 아닌, 우리 코드를 테스트 한다는 점입니다. 이 점만 기억하고 있으면 API 을 위한 함수 테스트(functional tests) 가 아닌 진정한 단위 테스트를 계속 작성할 수 있습니다.

이런 방법을 사용한 좋은 예가 TDD 로 구현한 입력 시스템입니다. 입력 시스템은 게임 패드나 다른 컨트롤러로부터 입력값을 받습니다. 그냥 보기에 해야 할 일이라고는 시스템 콜을 받아서 데이타를 던져주는 것 밖에 없어 보임니다만, 사실 플렛폼 독립적으로 할 수 있는 공용 코드가 엄청나게 많이 있습니다. 예를 들면 버튼 맵핑, edge 검출, 필터링, 컨트롤러 연결 / 해체같은 게 그렇습니다.

저희는 GameController 라는 인터페이스에 Sample 이라는 함수를 만들었습니다. 각 플렛폼에서는 이 인터페이스를 상속받아 각자 플렛폼에 맞게 구현해 놓았습니다.(예 : D3DController) 여기에서는 전체 버튼의 raw 샘플링이나 축 에 대한 API 호출을 담당합니다. 이 부분은 전혀 테스트되지 않습니다. 입력 시스템의 나머지 부분은 GameController 인터페이스를 통해서 작업됩니다만, 테스트를 위해서 우리는 MockGameController 를 이용해서 테스트에 전달해야 하는 입력 값을 지정할 수 있게 만들었습니다.

저희는 TDD 가 게임 산업에 좀 더 널리 퍼졌으면 합니다. 그렇게 된다면, 미들웨어 제공업체에서도 그쪽 API 를 좀 더 TDD스럽게 만들어 줄테고 심지어 미들웨어를 제공할 때 단위 테스트도 같이 제공해 주지 않을까 기대합니다.

. 써드 파티 게임 엔진 (3/5)

API 를 다루는 게 어려워 보인다면, TDD 가 전혀 적용되지 않은 서드 파티 게임 엔진으로 작업하는 것은 훨씬 더 어려워 보일 것입니다. API 야 그냥 클래스나 함수들의 모임이기 때문에 여러분들은 쉽게 언제, 어떻게 호출되어야 할지를 쉽게 제어할 수 있습니다. 전적으로 게임 엔진을 이용해서 작업할 경우에는, 게임 엔진 코드에 둘러쌓인 약간의 모듈을 개발하는 것이 전부일 수도 있습니다. 엔진이 TDD 로 개발되지 않았다면, 여러분의 코드를 엔진과 분리시키기 매우 어려울 수 있고, 테스트 하기 위해서 어떻게 코드를 나누어야 할지 결정하기 힘들 수 있습니다.

High Moon 에서는 여러 개의 프로젝트가 Unreal Engine 3 을 이용해서 진행중이고, 그것들은 모두 TDD 가 적용되어 있습니다. 엔진이 전혀 TDD 스럽지 않게 만들어져 있다고 해도, TDD 를 적용하는 것이 훨씬 더 낫다는 것을 알게 되었습니다. 그렇다면 엔진이 처음부터 TDD 로 개발되었다면 얼마나 굉장하겠습니까?

저희 경우에 가장 중요한 작업은 우리의 코드를 최대한 엔진 코드와 분리해 놓은 것이었습니다. 이렇게 하면 우리 코드를 독자적으로 단위 테스트 할 수 있을 뿐 아니라, Epic 의 코드와 merge 하는 과정도 최대한 쉽게 유지할 수 있습니다. 하지만, 이렇게 테스트 가능한 상태를 유지해야 한다는 점은 대규모 리펙토링을 하기 힘들게 제약했습니다. 뭐 일종의 어쩔 수 없는 절충안 이랄까요.

Unreal 엔진의 큰 특징 중 하나는 UnrealScript 라는 스크립트 언어를 엄청 사용한다는 것입니다. 게임 엔진의 윗단의 많은 부분이 UnrealScript 로 작성되어서 저희도 어쩔 수 없이 많은 코드를 UnrealScript 로 작성해야 했습니다. 그래서 처음 했었던 일은 UnrealScript 용 단위테스트 프레임워크를 만드는 것이었습니다. 그 결과가 UnUnit 이라는 것이고, 이건 UDN(Unreal Develper Network) 에서 공짜로 받아서 쓰실 수 있습니다. 이미 UnrealScript 를 사용하는 여러 회사들이 그들의 프로젝트에 UnUnit 을 사용하고 있습니다.

UnrealScript 는 단위테스트 하기에 놀랍도록 적합한 언어입니다. 기본적으로 모든 함수가 가상함수(virtual) 이므로, 오버라이딩하고 mock 객체를 만들고 하는 것이 C++ 보다 쉽습니다.
또한 컴파일도 빨라서 개발 주기를 빠르게 유지시켜 줍니다. UnrealScript 로 TDD 만들기가 완전하진 않지만 매우 효과적입니다.

. 임의성(randomness)과 게임 (4/5)
대부분의 게임은 많은 부분이 무작위(random)하게 돌아갑니다. 한 걸음 걸을 때마다 여러분은 몇 가지 소리들 중 하나를 듣게 됩니다. 파티클 에미터는 최소/최대값 사이의 임의의 속도로 움직입니다. 테스트에서 random 는 일반적으로 제거되는게 좋습니다. 처음에는 함수를 반복문 안에서 많이 돌린 다음에 결과값을 가지고 단위 테스트를 진행했었습니다만, 잘못된 방향이었습니다. 단위 테스트는 쉽고 빨라야 하므로, 테스트 안에 반복문이 들어가는 것은 영 꺼림찍합니다.

더 좋은 방법은 임의 결정과 그걸 사용하는 코드를 서로 분리하는 것입니다. 그냥 PlayFootstep() 이라는 함수 안에서 random 계산까지 하고 있다고 합시다, 이것을 random 을 계산하는 ComputeNextFootstep() 라는 함수와 그 값을 이용하는 PlayFootstep(int index) 라고 분리한다면 훨씬 쉽게 테스트 할 수 있습니다.

다른 방법은 테스트 하기 전에 난수 생성기를 조작해서, 어떤 값이 나올지 미리 예상할 수 있게 하는 것입니다. 가짜 난수 생성기를 이용하거나 난수 생성기의 전역 상태(역자 : random seed) 를 조작하세요. 이제 테스트는 난수값이 어떤 순서로 나올지를 알 수 있기 때문에 그에 따른 결과를 확인할 수 있게 됩니다.

. 고수준 게임 스크립트(5/5)

TDD 는 어디까지 적용해야 하는 걸까요? 코드 한줄 한줄에 대해 전부 다 해야 할까요? 게임 개발용 스트립트 코드에도? 이것은 여러분의 우선순위와 게임에 달려있습니다.

일반적으로는, 어떤 부분이라도 우리가 직접 작성한 코드에 의존한다면, 대부분 TDD 로 작업하고 단위 테스트 집합을 만들어 두는 게 좋습니다. 만약, 게임이 고급 언어로 구현된 간단한 거라면 TDD 를 쓰지 않아도 괜찮습니다. 레벨 디자이너가 게임용 스크립트 언어로 작성하는 경우라면 TDD 를 쓰는 게 힘들 수도 있겠죠.

TDD 를 쓰지 않을만한 코드의 예로 trigger 코드를 들 수 있습니다. 플레이어가 코너를 돌 때 두 개의 인공지능을 동작 시키면서 배경음악을 바꾸는 거 같은 것이죠.

기능 테스트(Functional tests) 는 단위 테스트와 함께 이루어 져야 한다는 점을 잊지 마세요. 자동화된 기능 테스트는 고수준의 문제를 잡아내는 데 너무나도 유용합니다. 또한 성능 측정이나 데이타 수집을 도와주고, QA 팀이 기계적으로 작업해야 하는 것들을 대신 해 주기 때문에 그들이 정말 중요한 게임 밸런싱이나 흐름 같은 것에 집중할 수 있도록 도와줄 수 있습니다.

7. 배운 것들

. 디자인과 TDD (1/7)
TDD 로 부터 얻을 수 있는 가장 좋은 것은 TDD 가 만들어 내는 더 좋은 코드 디자인입니다. 이것을 위해서는 꼭 테스트가 코딩을 주도하게 만들어야만 합니다. (역자 : 테스트를 먼저 작성한 후 코드를 작성하라는 뜻) 눈앞의 목표에 맞춰서 '일단 돌아가도록 만들기' 방식으로 코딩하라는 얘기를 들을 때는 '이게 뭐야' 싶겠지만, 해 보면 정말 결과가 좋다는 것을 알 수 있습니다. TDD 로 작업할 때의 비결은 바로 리펙토링이 개발 프로세스의 필수 요소라는 점을 깨닫는 것입니다. 항상 몇 개의 테스트를 추가한 후에는 다시 리펙토링을 해야합니다. 좋은 디자인은 테스트를 작성하고, 그에 맞는 코드를 구현하고, 그것들을 리펙토링하면서 만들어져 갑니다.

그렇다면 코딩 이전에 디자인 할 필요는 없는 걸까요? 이것은 여러분의 상황과 경험, 그리고 어떤 게임을 개발하느냐에 따라 다릅니다. 잘 모르거나 쉽게 바뀔만한 것이라면, 미리 디자인 하기 힘듭니다. 그러나, 만약 여러분이 유명한 스포츠 게임의 10 번째 버전 (역자 : 위닝 일레븐 9 이나 10 같은 시리즈) 을 익숙한 플렛폼에서 돌아가도록 구현해야 한다면, 무엇을 해야할지 너무나 잘 알고 있을 겁니다. 이럴 때는 미리 디자인 하기가 도움이 될 수 있습니다.

저희 경우에는, 무엇을 해야할지, 어떻게 나아가야 할지에 대해서 대략적인 개념과 필요할 경우 간단한 프로그램 구조를 가지고 논의하는 걸 좋아했습니다. 클래스니 칠판에 UML 같은 걸 그린다던지 하는 건 생각조차 해 보지 않았습니다. 이런 것들이 구현을 어떻게 할 지를 너무 많이 결정지을 수 있었기 때문이었습니다. 테스트가 어떻게 구현해야 할지를 이끌어주는 걸 더 선호했습니다. 처음 방향이랑 좀 벗어나는 느낌이 들 때면 언제든지 멈춘 후에 제대로 가고 있는지 확인할 수 있었습니다. 대부분의 경우 아무 문제가 없었습니다. 처음 시작과 다른 경우에도 대부분 시작할 때 성급하게 (혹은 너무 길게) 생각하느라 문제가 있었던 것임을 알 수 있었습니다.

. TDD 와 고수준 코드 (2/7)

처음 TDD 를 시작할 때 가졌던 의문은, 이것이 고수준 코드에도 잘 쓰여질 것인가 하는 것이었습니다. 저번 프로젝트 때 TDD 를 써 보았기 때문에, 저수준 혹은 중간 수준의 라이브러리(수학, 충돌, 메시지 등)를 만드는데 TDD 를 쓰는 것은 문제 없었습니다. 하지만, 저수준 코드로부터 만들어진 고수준 코드에 대해서도 TDD 가 작동할까요?

결과는 100% 문제 없었습니다. 저희는 전체 코드를 시작부터 TDD 로 작성했고, 고수준 코드를 TDD 로 작성할 때도 아무런 어려움이 없었습니다. 캐릭터 상태 머신, 게임 flow 나, 특수한 게임 객체(entities) 같은 것들도 TDD 로 어렵지 않게 개발되었고, 오히려 많은 이득을 얻었습니다.

TDD 를 고수준 코드에 적용하는 데에는 다음 두 가지 비법이 도움이 되었습니다.

. 테스트를 진정한 단위 테스트 형태로 유지하세요. 고수준 코드로 작업하다보면, 단위 테스트를 게임 엔진 전반적인 내용을 테스트 하는 '함수 테스트'형태로 변질시키고 싶다는 유혹에 근질근질할 것입니다. 절대 그렇게 하지 마세요. 심지어, 적 캐릭터를 테스트 하기 위해 코드 격리시키는 작업 때문에 시간이 몇 분 더 걸리더래도, 길게 보면 훨씬 더 이득이 될 것입니다.

. 엔진 구조를 최대한 단조롭게 유지하세요. 이것은 다른 이유에서도 맞는 말이지만 특히 TDD 를 위해서는 더욱 더 그렇습니다. 쉽게 얘기해서, 여러분의 엔진이 하나의 거대하고 여기 저기에 걸쳐서 연결되어 있는 모듈 형태로 만들지 마세요. 이걸 몇 개의 라이브러리나 모듈로 나눌 때에도, 각각을 최대한 독립적으로 유지하세요. AI 모듈이 그래픽스나 사운드에 대해서 알 필요가 없습니다. 대신, 월드라던가 메시지 시스템에 대해서는 알 필요가 있을 것입니다.

. 작업 진척도를 알려주는 TDD (3/7)
소프트웨어 개발자는 오랜 시간 동안을 '어떻게 하면 작업 진척도를 잘 나타낼 수 있는가' 에 대해서 고민해 왔습니다. 코드의 라인 수를 이용하기도 하고, 개발 완료된 기능 갯수를 이용해 보기도 했습니다. 심지어, 얼마나 오랫동안 사무실에 있었나를 이용하기도 했죠. 전부 다 이상적인 방법은 아닙니다.

저희는 단위 테스트의 갯수야말로 진척도를 알 수 있는 좋은 척도라고 생각합니다. 테스트가 TDD 를 통해서 개발되었다면, 테스트들은 corner cases(역자 : 특이한 값을 넣었을 때 발생하는 예외들) 보다는 프로그램 기능에 대해서 더 다루고 있을 것입니다. 어떤 라이브러리가 500 개의 테스트를 가지고 있다고 한다면, 1000 개의 테스트를 가진 라이브러리보단 1/2 배 복잡하다고 생각할 수 있을 것입니다.

이런 장점에 더해서, 사람들은 어떻게 측정되는지에 따라서 생각하는 경향이 있다는 것도 말씀드리고 싶습니다. 이런 측정방법이 명확해질 경우, 사람들은 더욱 더 엄격하게 TDD 를 적용하고, 더 이상 테스트 없이는 어떤 코드도 작성하지 않으려 합니다. 저희는 테스트 도표라는 것을 두고 매일 매일 전체 테스트의 갯수를 갱신했습니다. 테스트 갯수가 늘어나지 않거나, 평소보다 느리게 증가한다면, 뭔가가 잘못되었고, 진행이 잘 안 되고 있다는 것을 알 수 있었습니다.

. 빌드 안정화(4/7)
예상하신 대로, 전체 코드에 대해서 단위 테스트를 둔 결과 코드의 안정성이 급격히 좋아졌습니다. 빌드 실패는 훨씬 덜 일어났고, 일어났을 때도, 파일을 빼먹었다던가, 컴파일 옵션을 잘 못 줬다던가 하는 게 대부분이었습니다. 더 놀라운 것은, 빌드가 깨졌을 때도 훨씬 쉽게 고칠 수 있었다는 것입니다. TDD 덕분에 어떤 게 잘 못 되었을 때 어느 부분인지 확실하게 들어나게 되었고, 우리는 그 부분만 바로 확인하면 되었습니다. 빌드가 깨졌을 때 많아봐야 몇 분 안에 고쳐졌기 때문에, 우리가 코드를 소스 컨트롤에서 어떻게 구성할지에 대한 것이 완전히 바뀌어 졌습니다.

. 코드의 양(5/7)
TDD 를 하게 될 경우에, 작성해야 할 코드의 양이 늘어난다는 것은 놀랄만한 일이 아닙니다. 때때로, 테스트 코드의 양은 그것에 테스트하는 코드의 양과 거의 같을 정도로 많아질 수도 있습니다. 이런 상황은 무시무시하게 보일 수도 있습니다만, 정말 아무런 문제가 되지 않습니다. 추가적인 코드는 작성하는데 오래 걸리지 않고, 일단 테스트 코드들이 모이고 나면 훨씬 빠르게 기능을 구현하고 리펙토링하고, 성능 최적화를 할 수 있게 해 줍니다.

코드의 양이 두 배가 된다고 해서 복잡도가 두 배가 되는 것은 아닙니다. 오히려 반대입니다. 테스트 코드는 워낙 간단하게 작성되기 때문에 복잡도는 무시해도 됩니다. 이것들이 하는 일이라고는 다른 코드들을 검사하는 것 뿐입니다. 우리 대신 무엇이 잘 못 되는지 지켜보고 문제가 생긴 곳을 알도록 도와줍니다. 또한, TDD 접근법은 우리의 코드를 간단하고, 결합도가 낮게 만들어 주므로, 결과적인 코드의 복잡도는 훨씬 낮아집니다.

TDD 와 비슷한 것으로 건축 현장의 비계(scaffolding)를 들 수 있습니다. 이건 사용자에게 제공해야 하는 것은 아닙니다만 절대적으로 필요한 것입니다. 이것 없이 복잡한 건물을 짓는다는 걸 상상해 보실 수 있으신가요?

. 개발 속도(6/7)
여기 백만 달러짜리 질문이 있습니다. TDD 가 개발 속도를 느리게 하나요? 저희는 TDD 가 단순히 '하면 좋은' 것이어야 한 게 아닙니다. 오히려 더 좋은 품질의 제품을 더 빨리, 더 값 싸게 제공하기 위해서였습니다. TDD 가 이런 점에서 도움이 되지 않았다면, 별 볼 일 없는 것이겠지요.(프로그래머들을 행복하게 만든다는 것도 있습니다.)

다른 소프트웨어 측면과 마찬가지로, TDD 가 다른 것에 비해 얼마 정도나 효과가 있는지를 객관적으로 연구하고 측정하기란 어렵습니다. 프로젝트와 팀에 따라서 많은 것들이 달라집니다. 그러나 몇 몇 초기 연구들이 - 그다지 엄격하지 않고, 샘플 갯수도 얼마 되지 않습니다만 - 흥비로운 발견을 해 놓았습니다.(http://collaboration.csc.ncsu.edu/laurie/Papers/TDDpaperv8.pdf)

저희 경험상 TDD 는 다른 최신 개발 방법과 마찬가지로, 처음 시작하고 익숙해지는 기간 동안에는 개발 속도가 느려집니다. 보통 여기에 2달 정도가 필요합니다. 일단 언덕을 넘고 나면, 그리고 적당한 도구와 개발 환경만 주어진다면, 개발 속도에 TDD 가 미치는 악영향은 거의 없습니다.

바로 코딩하는 게 테스트를 먼저 작성하는 것 보다는 좀 더 빠를 수도 있습니다. 하지만, 코드는 금방 리펙토링 되어야 하고, 디버깅에, 다른 개발자가 사용하기도 하고, 더 복잡해지기도 합니다. 이렇게 되면 테스트를 작성하지 않아서 절약한 시간은 금방 날아가버립니다. 팀이 커지고 문제가 복잡할 수록, TDD 는 더 많은 시간을 길게 봤을 때 절약하게 해 줍니다. 월말 마일스톤 같은 막판 달리기 기간에 TDD 를 무시하려는 유혹은 이겨내는 것은 그래서 중요합니다.

이상적으로는 TDD 와 리펙토링은 전통적인 "개발 기간에 따른 변경 비용의 증가" 곡선을 평평하게 만들 수 있습니다. 짧은 기간 동안 좀 천천히 개발하는 것 대신 뒤에 얻을 수 있는 엄청난 이득을 주목하세요.

. TDD 적용하기(7/7)

처음에는 다른 프로젝트에서 소수의 그룹 안에서 먼저 TDD 를 시도해 보았습니다. 저희 경우에는 단순히 TDD 만 한 게 아니라, 익스트림 프로그래밍 실천법(짝 프로그래밍, 지속적인 통합, 코드 공동 소유) 을 다 같이 시작했습니다. 이렇게 할 때에는, TDD 에 대한 경험이 있는 사람이 팀원 중에 있다면 훨씬 도움이 될 수 있습니다. 그는 과정을 이끌어주고, 환경을 잡아주며, 초기 실수들을 막아주는 데 크게 도움이 될 것입니다.

작은 규모의 팀에 먼저 적용해 보는 것은 여러 모로 유용합니다. 팀은 좀 더 안정적으로 TDD 을 받아들일 수 있게 되고, 이전 생산성의 단계에 이를 수 있게 합니다. 또한, 안 좋은 실천법을 좀 더 빨리 깨닫게 해 줍니다.(복잡하거나 기능 테스트에 어울리는 것들을 단위 테스트로 만들려고 한다던지 하는)

이렇게 하면 새로운 기술에 대한 전도자를 만드는 효과도 있습니다. 그 팀의 구성원들이 다른 팀으로 가게 될 경우, TDD 에 대한 상주 전문가 역할을 할 수 있습니다.

TDD 는 다른 애자일 실천법과 잘 어울립니다. 애자일 방식은 테스트를 통해서 디자인을 찾아나간다는 생각과 잘 어울립니다. 짝 프로그래밍은 특히 처음 TDD 를 도입해서 모든 팀원들이 TDD 에 익숙해지게 만드는 데 도움이 됩니다. 또한 짝 프로그래밍은 프로그래머가 처음에는 하기 싫어하는 '테스트 만들기'를 거르지 않게 도와줍니다.

TDD 에 관심은 있지만, 매니저나 팀장을 설득하지 못했다면, 여러분의 작업에 먼저 도입해 보세요. 가장 중요한 것은 여러분의 테스트가 자동으로 돌아가야 한다는 점입니다.(postbuild 을 잊지 마세요.) 천천히 다른 팀원들도 테스트가 얼마나 유용한지, 또는 리펙토링을 쉽게 만들어 주는게 알게 될 것입니다. 그 때가 TDD 를 전격적으로 도입할 수 있는 때가 되는 것이죠.

TDD 로 사고의 전환을 하는데 있어서 힘들어 하는 팀원들이 분명히 있을 수 있습니다. 하지만, TDD 를 최대한 활용하려면, 개발 초기에 디자인하기 보단, 테스트와 리펙토링으로부터 디자인을 만들어 내도록 해야 합니다. 재미있는 점은, High Moon 대부분의 개발자들이 금방 TDD 를 익혔고, 곧 TDD 열성당원이 되어서 모든 코드와 심지어 집에서 짜는 코드에까지 TDD 를 쓰기 시작했다는 점입니다.

처음 시작하는 프로젝트 코드에 TDD 를 적용하는 게 당연히 가장 쉽습니다만, 그런 사치를 모두가 누릴 수는 없습니다. 그래서, 어떻게 기존 코드에 TDD 를 잘 적용할 수 있는지 익히는 것도 중요합니다. Michael Feathers 의 책 'Working Effectively with Legacy Code' 는 딱 이런 상황에 대해서 쓴 책입니다. 그 책은 어떻게 하면 단위 테스트가 없는 기존 코드에 테스트를 잘 집어넣을 수 있는지에 대한 좋은 지침을 제공합니다. 어떨 때는 약간의 리펙토링 만으로도 훨씬 쉽게 테스트 코드를 추가할 수 있게 됩니다.

8. 결론

테스트 주도 개발은 매우 효과적인 개발 방법론입니다. 저희는 여러 개발 프로젝트에 TDD 를 성공적으로 도입했고, 덕분에 많은 것을 얻었다고 확신합니다. 이제는, 저희 대부분에게 테스트 없이 코딩한다는 생각 자체가 이상하게 느껴집니다. 저희는 TDD 를 건축 현장의 비계라고 생각합니다. 소비자에게 필요한 것은 아니지만, 개발 동안에 엄청한 도움을 주는 필수 도구같이 말이죠.

9. 자원

다음 것들은 여러분이 TDD 를 시작하기 전에 읽어보시면 좋습니다.

. Beck, Kent, Test Driven Development: By Example. Addison-Wesley, 2002

이런 책들도 매우 유용합니다.

. Fowler, Martin, Refactoring: Improving the Design of Existing Code. Addison-Wesley, 1999
. Astels, David, Test Driven Development: A Practical Guide. Prentice Hall, 2003
. Beck, Kent, and Cynthia Andres, Extreme Programming Explained: Embrace Change (2nd
Edition), Addison-Wesley, 2004

매우 유용한 메일링 리스트와 웹 사이트 입니다.

. TestDriven.com (http://www.testdriven.com)
. TDD mailing list (http://groups.yahoo.com/group/testdrivendevelopment)

TDD 와 애자일 게임 개발을 다루고 있는 곳입니다.

. Noel's blog, Games from Within (http://www.gamesfromwithin.com)
. Clinton Keith's blog, Agile Game Development
(http://www.agilegamedevelopment.com/blog.html)

C++ 단위 테스트 프레임워크입니다.

. UnitTest++ (http://unittest-cpp.sourceforge.net)
. CppUnit (http://cppunit.sourceforge.net/cppunit-wiki)
. Comparison of C++ unit-test frameworks
(http://www.gamesfromwithin.com/articles/0412/000061.html)

핑백

덧글

댓글 입력 영역


Yes24위대한게임의탄생3

위대한 게임의 탄생 3
예스24 | 애드온2