1. TDD를 적용하게 된 계기

TDD에 대한 이야기를 처음 들은 후 부터, TDD를 처음 적용했던 LG-SH150 DMB Player 프로젝트 이래로 TDD 에 대한 나의 관심은 주욱 지속되어왔다.

처음에는 다소 번거로운 작업이지만 어느 정도 test가 쌓이게 되면 그 후부터는 마음 놓고 코드를 수정할 수 있다. 그래서 다양하고 실험적인 refactoring이 가능하며, 코드의 중심 로직을 대거 뜯어 고친 후에도 코드 검증에 걸리는 시간이 매우 짧다는 점, 무엇보다도 코드의 품질이 보장된다는(이 부분은 test 가 어느 정도의 coverage를 가지고 있느냐에 따라 다를 수 있다) 것 등...TDD는 거부할 수 없는 매력을 가지고 있다고 생각한다.

iPhone 개발을 시작하고 OCUnit에 대해 알게 되면서, iPhone 어플 개발시에도 TDD를 적용해 보리라 하고 생각하고 있었다.
그러던 중에 개발중인 어플리케이션이 DB(sqlite3)를 사용하게 되었고, view 부분과 data 부분을 어떻게 하면 효과적으로 분리할까 고민하던 중에 Ruby On RailsActiveRecord와 비슷한 일종의 DB framework 비스무레 한 것을 하나 만들어 보자는 생각을 하였고, 이 프레임웍을  TDD로 구현해 보기로 했다.


2. Ruby On Rails의 ActiveRecord를 참고하여 만든 iRecord

올해 초(3~4월)에 Ruby On Rails(이하 RoR)를 처음 접하고, RoR을 사용하여 고민상담 서비스를 만들면서 RoR의 매력에 흠뻑 빠져 들었다.
RoR은 다양한 framework을 제공하는데, 그 중에 관심을 제일 많이 끈 것은 DB framework인 ActiveRecord 였다.

RoR의 DB쪽 구현하는 부분의 구조를 간단하게 설명하면,
우선, DB migration을 생성한다. 개발자는 migration을 통해서 테이블 생성/삭제/변경 등을 수행할 수 있다. 생성된 DB migration을 실행하면 DB에 table이 생성되고, 간단한 model class가 생성된다.


















이와 같이 생성된 model class는 보기에는 별 것 없어 보이지만, 이 클래스가 상속받고 있는 ActiveRecord::Base 클래스를 통하여 DB에 access할 수 있는 많은 method를 제공한다. 몇 가지 예를 들어보면...



















위의 controller에서 find 메소드는, 'select * from event where id = params[:id]' 를 호출하여 해당 record를 @event 라는 인스턴스 변수에 저장하는 역할을 수행한다. 밑의 view에서는 '@event.name, @event.budget' 처럼 property에 접근하는 형식으로 record의 각 column의 값을 얻어올 수 있다.

ActiveRecord는 find 외에도 find_by_id, save, update 등의 메소드를 제공하여서, SQL을 거의 사용하지 않고서도 DB 관련 기능을 수행할 수 있도록 해 준다. 덕분에 DB관련 작업을 너무나도 쉽게 했던 기억이 있다.

iPhone 어플리케이션에서도 이러한 framework을 사용할 수 있으면 좋겠다는 생각이 들었다. SI 프로젝트와는 달리 iPhone 어플에서는 table 1~2개 정도만을 사용하는 소규모 db를 사용할 것이기 때문에, ActiveRecord의 기능 중 일부만을 구현해서 사용하는 것을 목표로 하였다. 이름은 야심차게 'iActiveRecord'로 하려다가, 왠지 조금 겸손해지고 싶은 마음이 들어서 'iRecord' 라고 바꾸었다. ^^;

이와 같은 framework(까지는 아니고...framework 비스무레한 거..ㅎㅎ) 이야말로 코드 검증 / 지속적인 refactoring이 필요할 것이므로 TDD로 개발하기에 딱 좋을것이라는 생각이 들어, OCUnit을 사용하여 개발을 시작하였다!


3. TDD 진행
우선, test 모드와 일반 mode를 설정해 줄 수 있어야 할 것 같아서. 다음과 같이 test 초기화 메소드를 작성하였다. setUp / tearDown 메소드는 테스트 실행 전/실행 후에 각각 호출되는 메소드이다.













테스트를 실행(OCUnit에서는 빌드. 이하 '실행' 이라고 표기함)시키면 에러 메시지가 뜬다. DBManager 클래스를 singleton으로 구현하고, dbManager 메소드를 통해 singleton instance에 접근하도록 하였다. 그리고 dbTestMode라는 property를 추가하여 test 모드 여부를 체크하였다. 테스트를 실행시키면, (테스트 메소드가 하나도 없긴 하지만) 빌드 성공이다. 이제 다음 단계로 넘어갈 수 있다!

다음으로, 모델 클래스에 property를 설정하고 save 하는 동작을 구현하려고 하였다. 관련 테스트를 다음과 같이 작성하였다.








property를 설정하고 save하면 DB에 insert가 되도록 하는 것이 궁극적인 구현 목표이다. 그러나 TDD에서는 일단 현재의 test를 통과하도록 하는 것이 급선무이고, 추가 구현은 그 다음에 생각해 볼 일이다. CheckListModel이라는 클래스를 만들고, 위의 테스트에 나온 property를 정의한 후에, save라는 instance method를 만든 후 다음과 같이 구현하였다.






우선 이번의 테스트는 통과하였다. 이제 다음 테스트를 작성할 차례이다.

save 메소드를 하나 더 추가하여 test를 구현하였다. test는 다음과 같다.





checkList2라는 model instance를 하나 더 생성한 후에 save하면 결과는 둘 다 YES를 반환해야 하며, save한 후에 각 instance의 key property는 적절한 값으로 설정되도록 하는 구현이다. key는 모든 model class가 가지게 될 기본 property이며, DB에서는 key 컬럼으로 쓰이며, 자동 증가하는 필드이다. (RoR의 id 컬럼에 해당한다)

이 테스트를 통과시키기 위해서는, DB와 관련된 구현을 해야만 한다. 드디어 본격적인 구현이 시작되는 순간...

우선, model class는 iRecord 라는 클래스를 상속받도록 구현하였고, model class에는 table meta data를 넣도록 하였다. iRecord framework의 model은  RoR의 migration + model 이라고 생각할 수 있다.













다음으로, iRecord 클래스에는 key property를 추가한 후, save 메소드를 다음과 같이 구현하였다.
































save method에 대하여 간단하게 설명하면, model에서 정의한 table meta data와 설정된 property의 값을 바탕으로 하여 insert sql 문을 생성하고, 그것을 db에 실행시킨 후, db로부터 last_inserted_rowid를 받아 와서 return하는 내용이다.

구현하는 시간이 조금 오래 걸렸지만, test가 통과되었다.

그 다음으로는, 전체 테이블의 record count 및 특정 조건 하에서의 record count가 필요할 것 같아서 다음과 같이 테스트를 작성하였다(클릭하면 크게 보입니다).












count 메소드는 'select count(*) from CheckList' 가 될 것이므로, record 관련 메소드가 아니라 table에 적용되는 메소드라고 생각되어 instance 메소드가 아닌 class method로 구현하였다.

save 메소드 구현 시에 db 관련 기반 private method 들을 만들어 놓은 상태이었으므로, 구현 시간은 비교적 짧았다.

다음에 작성한 테스트는 findwithID - key값으로 하나의 record를 얻어오는 메소드- 관련 테스트이다(클릭하면 크게 보입니다).









얻어온 record가 null 값인지 아닌지 체크를 한 후에, checkListAssertwithCheckList 메소드에서 각 record의 값을 검증하는 테스트이다.

다음에는 find 메소드 관련 테스트를 작성하였다. find 메소드는 findwithID와는 다르게 table에 있는 모든 record를 가져오는 메소드이다. 따라서 NSArray형을 return하도록 테스트를 만들었다(클릭하면 크게 보입니다).












결과값이 NSArray이므로 count 관련 테스트 코드도 작성하였다.

이와 같이 test 작성 - test 통과 - 다음 test 작성 ... 등의 과정을 거쳐 구현한 메소드들은 다음과 같다(클릭하면 크게 보입니다).













4. 회고

1) 좋았던 점
- test 빽(?)을 믿고 refactoring 을 과감하게 할 수 있어서 좋았음
- test 작성 시에, 결과물을 사용할 유저(여기서는 프레임웍을 사용할 개발자라고 할 수 있음)의 관점에서 생각을 할 수 있어서 좋았음
- 블로그에 글 쓰는 것은 빡세지만 재미있고 뭔가 남은 것 같은 기분이 들게 하는 작업임 ㅋㅋㅋ

2) 아쉬웠던 점
- save 메소드 구현 시에 private 메소드에 대한 TDD를 하지 않아서, 테스트를 통과하기 위한 구현 시간이 너무 길었음. private 메소드도 TDD를 해 주는 것이 어땠을까 하는 아쉬움이 남음
- 날짜 관련 쿼리를 해 오는 메소드를 구현할 때에 벌어진 일...
3가지 case가 있었는데 구현한 테스트는 2가지만을 cover 할 수 있었음. 부랴부라 나머지 case를 테스트에 추가하였는데, 소 잃고 외양간 고치는 기분 절반에 테스트의 검증력이 훨씬 강해졌다는 위안 절반...이상야릇한 기분이었음 @_@
- 블로그에 글 쓰는것은 빡센 작업임 ㅋㅋㅋ


3) 재미있었던 점
- 중간에 구조를 바꾸어야 할 일이 있었음. table meta data를 하나의 class 변수에 저장하는 방법을 썼는데 테이블을 2개 이상 사용할 때에 이전 data가 겹쳐져서 오작동을 하는 사례가 발생하였음. 그래서 NSDictionary를 사용하도록 구조를 바꾸었는데, 구조를 바꾼 후에 바꾼 구조에 대한 검증을 테스트 실행 한 번으로 끝낸 사실 자체가 너무 재미있었음 ㅋㅋㅋㅋ

OCUnit의 대안

iPhone 2009/06/28 22:00
http://code.google.com/p/google-toolbox-for-mac/wiki/iPhoneUnitTesting

OCUnit이 마음에 들지 않는 사람은,
위의 링크에 들어가 보면 OCUnit과 비스무레한 Unit test용 framework이 있으니
다운받아 설치하여 사용하면 된다.

Unit Test를 돌리다 보면 테스트 통과여부를 아는 것 만으로는 부족할 때가 있다.

테스트가 fail이 되었는데 도대체 왜 그랬는지 도통 감이 오지 않을 때에는,
디버깅을 해 보는 것이 많은 도움이 된다.

OCUnit에서 디버깅을 하려면, 다음과 같이 설정해 주면 된다.

1. Project - New Custom Executable을 클릭한다.






















2. 다음과 같이 입력하고 Finish 버튼을 누르면 'otest' 라는 이름의 Executable이 하나 만들어 진다.























3. 새로 만든 Executable 'otest' 의 Info 창을 띄우고 다음과 같이 Arguments와 Variables 를 설정한다. 2번째 Argument인 "Unit Test.octest" 에서 "Unit Test" 는 앞서 추가한 UnitTest bundle의 이름이다. Xcode 최신 버젼(3.1.2)에서는 세 번째 variable인 OBJC_DISABLE_GC를 YES로 설정해 주어야 한다.






































4. Active Executable을 방금 추가한 otest로 바꾼다.











5. break point를 설정하고, Debug 버튼을 눌러 마음껏 디버깅을 해 보자!



















P.S)
1. 참고사이트
http://beatworm.co.uk/blog/computers/tracing-unit-tests-with-the-xcode-3-debugger/
http://chanson.livejournal.com/119578.html

2. iPhone OS 2.2 / 2.2.1 에서는 몇몇 Framework( ex)UIKit.framework )를 추가하면 디버깅이 되지 않는다. 다행히도, Foundation.framework은 잘 돌아간다. UIKit을 사용하는 부분을 테스트 할 일은 그리 많지 않을 것이니 사용하는 데 큰 지장은 없을 것이다(하지만 찝찝한 건 어쩔 수 없다...) iPhone OS 3.0에서는 이 문제가 해결되었기를...

OCUnit은 Cocoa/Cocoa touch용 유닛 테스트 도구이다.
unit test용 bundle을 추가한 후, test 메소드를 몇 개 추가하여 간단하게 유닛테스트를 수행할 수 있다.

Cocoa에서는 간단하게 사용할 수 있으나 Cocoa touch(iPhone OS 2.2 / 2.2.1)에서는 제대로 동작하지 않아서 설정값 몇 개를 변경해 주어야 한다. iPhone OS 3.0에서는 제대로 동작하기를...

OCUnit을 사용하는 법은 다음과 같다.

1. 새로운 target을 추가한다.













2. Cocoa / Unit Test Bundle 을 선택하여 추가한다. 적절한 Target Name을 입력한 후 Finish 버튼을 누르면 Unit Test Bundle이 추가된다.






















3. 새로 추가된 Unit Test Bundle의 Info 창을 연다.(iPhone OS 2.2 / 2.2.1(이하 iPhone OS)관련 내용)













4. Build tab에서, User-Defined Settings’ 과 ‘Other Linker Flags' 항목을 삭제한다.(iPhone OS 관련 내용)



































5. General 탭의 'Linked Library' 란에 다음 경로의 프레임웍을 추가한다: /Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator2.2.sdk/Developer/Library/Frameworks/SenTestingKit.framework  (iPhone OS 관련내용)



















6. 기존 Application target의 Info 창을 연 후에, 'Direct dependencies' 란에 생성한 Unit test bundle을 추가한다. Application 빌드 시에 Unit Test bundle도 같이 빌드되도록 하여 , test bundle을 빌드하기 위해 Active Target을 바꿀 필요가 없도록 하기 위한 작업!




















7. Unit Test target에 다음과 같이 필요한 framework을 추가한다. Foundation.framework은 기본적으로 추가하는 것이 좋다.

















8. Unit Test target에 다음과 같이 Objective-C test class 를 추가한다.










































9. 추가한 testcase class(여기서는 UnitTest.m)를 열고 다음과 같이 testSample 메소드를 추가한 후 빌드한다. 모든 유닛 테스트 메소드(testSample과 같은)가 성공한 경우에는  'Build succeeded' 메시지가 나온다. OCUnit에서는 테스트 케이스 실행 = 빌드 인 것이다.

10. test가 실패한 경우에는 다음과 같이 에러메시지가 뜬다. 유닛 테스트 실패 = 빌드 에러 인 것이다. 빌드만으로 유닛 테스트를 실행시킬 수 있어서 킹왕짱 편리하다.


















11. test case에 다음과 같이 추가하여 시뮬레이터 상에서만 테스트가 실행되도록 한다.




















p.s)
1. 참고 URL
http://www.sente.ch/s/?p=535&lang=en

2. OCUnit 관련 정보. 사용할 수 있는 assert 목록 등의 유용한 정보가 들어 있다.
http://developer.apple.com/mac/articles/tools/unittestingwithxcode3.html

1. 당신이 처한 상황...

당신은 보일러 회사에 고용된 프로그래머다. 당신이 작성할 것은 '보일러 제어' 프로그램이다. 방에 달려 있는 보일러를 끄고, 켜고, 온도를 조절하는 제어기에 들어갈 프로그램이라고 보면 되겠다. 실제로 돌아가는 보일러와 소켓 통신을 통해 이런 저런 제어 정보를 주고 받는다. 주고 받을 정보의 형식은 다음과 같다.












command 앞에 command의 length를 붙여서 서버로 날리면 된다.

당신이 처해 있는 상황은 매우 열악하다. 사용할 수 있는 command가 보일러 끄기/보일러 켜기/보일러 온도 알아오기 정도 뿐이다. 이 정도면 충분하다고? 천만에! 보일러 켜기, 보일러 끄기, 보일러 온도 표시, 보일러 온도 조절, 타이머 켜기, 타이머 끄기 기능을 구현해야 한단 말이다. 게다가 보일러 회사 사장은 매우 깐깐하여 테스트용 보일러를 구동해 달라는 요구에도 꿈쩍 하지 않을 경우가 많으며, 회사의 사정이 열악한 나머지 테스트용 보일러 조차도 잘 돌지 않을 때가 많다. 이정도면 안습 상황이다...

2. 첫 걸음...

그나마 다행인 것은, 소켓 통신만 할 수 있으면 어떤 언어든 사용할 수 있다는 것이다. 가장 익숙한 언어인 C를 선택하였다. 테스트 프레임웍은 자주 쓰던 CUnit을 사용하기로 했다.

우선 주어진 환경이 환경이니만큼, 보일러와 소켓 통신을 통해 정보를 주고 받기 위해서는 소켓 초기화 및 종료가 필요할 것 같아서 다음의 테스트를 추가하였다.
void SetupTestFunction(CuTest* test)
{
  CuAssert( test, "SetupTest", client_setup() > 0 );  
  CuAssert( test, "TeardownTest", client_teardown() > 0 );
}


테스트를 추가한 후 빌드...당연히 에러 발생...우선 client_setup() 과 client_teardown() 함수를 만든 후 무조건 1을 return 하도록 하였다. 일단 테스트 통과...

수단과 방법을 가리지 않고 테스트를 통과시킨 후, 실제로 돌아가는 코드를 작성하는 식으로 개발을 진행해 나가기로 하였다. 그에 따라서 client_setup()과 client_teardown() 함수에 소켓 접속 / 접속 해제 코드를 넣었다. 약 한시간의 삽질과 테스트 fail 후에 일단은 성공...휴...

3. 벽에 부딛혀...

"소켓 통신은 되었으니 좀 후련한 것 같군. 그런데 이제 어떤 테스트를 추가해야 하지?"
일단 온도를 얻어 오는 테스트를 하나 추가해 보았다. 온도를 얻어오는 command를 보일러에 날리고, 날아온 온도 정보가 0에서 90 사이인지를 테스트하는 것이다.

시작부터 문제가 있었다. 우선 command를 제대로 날리지 못하였고, 그에 따라 온도 정보가 제대로 날아오지 않았다. 하지만 command가 제대로 구성되었는지의 여부를 체크할 수 있는 테스트 코드가 없었기 때문에 이 상태에서 테스트 코드를 디버깅하고, 로직을 디버깅 하는 등의 추태(?)를 보였다.

위에 작성한 테스트는 QA에서 테스트하는 것과 다를 바 없다. 유닛테스트는 QA 테스트와는 다르며, 보다 작은 단위로 나누어서 생각해 볼 필요가 있다는 조언을 얻었다.

4. 이렇게 하면 어떨까?

문제가 안 풀려서 책상에서 한참 골머리를 썩이고 있다가 밖으로 나와서 잠시 바람을 쐬고 있다보면 실마리가 불현듯 생각 나는 경우가 있다. Sample로 작성된 TDD 코드를 참고삼아 보면서 곰곰히 생각해 보다가 답이 안 나와서, 집을 나와서 약속장소로 가기 위해 버스를 잡아 타고, 자리에 앉아 창밖을 물끄러미 바라 보다가 문득 이런 생각이 들었다.

"작동하는 보일러에 무언가 날려서 얻은 결과만을 테스트하지 말고, 정보를 날리기 위한 과정 과정을 자세히 나누고, 보일러 작동을 시뮬레이션하는 방식으로 TDD를 적용해 보면 어떨까?"

다시 책상에 앉은 후, 첫 번째로 한 일은 네트워크 관련 함수 구현을 멈추고 network_stub.c 를 사용하여 구현하기로 한 것이다. 다음과 같이...

#include <windows.h>
#include <stdio.h>

#include "network.h"

/* private members */
static SOCKET sock;
static const int port = 7799;
static const char* ipAddr = "127.0.0.1";

int ConnectInit(void)
{
return 1;
}

int Send(const char *buf, size_t len, int flags)
{
  return 1;
}

int Recv(void *buf, size_t len, int flags)
{
  return 1;
}

int Disconnect(void)
{
  return 1;
}


위와 같이 네트워킹 관련 함수는 모두 1을 return 하도록 하고, TDD를 이용한 개발이 완료된 후에 실제로 네트워킹 관련 내용을 채워서 완성하기로 하였다.

그리고, command를 구성할 buffer를 만들고(여기서 command는 "\x02""78" 의 형식을 띰), 그 command의 length가 제대로 설정되었는지를 확인하는 작은 test로 시작하였다.
void BufferLengthTestFunction(CuTest* test)
{
  CuAssertTrue( test, client_setup() > 0 );
  SetServerSendBuf("\x02""78");
  CuAssertIntEquals(test, 3, strlen(GetServerSendBuf())/*read_length(sockfd)*/);
  CuAssertTrue( test, client_teardown() > 0 );
}

드디어 실제로 사용되는 command를 구성하고, 구성된 buffer를 체크하는 test를 추가해 보았다. 다음은 보일러의 온도를 얻어오는 "getboilertemp" command 버퍼를 구성한 후, 보낼 command 버퍼가 제대로 구성되었는지를 체크하는 테스트이다.
void GetTempCommandTestFunction(CuTest* test)
{
  CuAssertTrue( test, client_setup() > 0 );
  CuAssert(test, 13 == BuildCommandToPacket("getboilertemp"));
  CuAssertStrEquals_Msg(test, "\x0d""getboilertemp", GetServerSendBuf());
  CuAssertTrue( test, client_teardown() > 0 );
}

이와 같이 다른 command를 test 하는 코드를 추가한 후, 실제로 메시지를 보일러 서버에 보내는 test를 수행하였다. 여기서 사용된 SendCommandToServer() 함수는 실제로 보일러에command를 보내는 것이 아니라 각 command에 따른 적당한 값(예를 들면 getboilertemp 명령어에 대해서는 21도 라는 답을 주는 식으로)을 반환한다. 즉, 서버의 동작을 흉내낸 코드이다. test 코드는 다음과 같다.

void GetTempTCPSendTestFunction(CuTest* test)
{
CuAssertTrue( test, client_setup() > 0 );
CuAssert(test, 13 == BuildCommandToPacket("getboilertemp"));
CuAssertStrEquals_Msg(test, "\x0d""getboilertemp", GetServerSendBuf());
CuAssert(test, SendCommandToServer());
CuAssertTrue( test, client_teardown() > 0 );
}

이런 식으로 다른 command에 대한 test도 추가하였고, 받은 값에 대한 test도 추가하였다.

드디어 온도 설정 기능을 구현해야 한다! 우선 온도를 설정하는 함수가 있어야 할 것 같고, 적절한 온도를 설정하였는지 여부를 검사해 보아야 할 것 같아서 다음과 같은 test를 추가하였다. 온도는 0도에서 90도까지 설정할 수 있다.

void SetTempTestFunction(CuTest *test)
{
CuAssertTrue( test, client_setup() > 0 );
CuAssert(test, setTemperature(34) == 1);
CuAssert(test, setTemperature(1) == 1);
CuAssert(test, setTemperature(90) == 1);
CuAssert(test, setTemperature(0) == 1);  // 온도 설정 OFF
CuAssert(test, setTemperature(-1) < 0);
CuAssert(test, setTemperature(91) < 0);
CuAssertTrue( test, client_teardown() > 0 );
}

추가로, 온도 설정과 관련된 여러 상황들을 test 내에서 시뮬레이션 하여 보았다. 현재 온도와 설정한 온도를 비교하여 보일러가 해야 할 일(보일러를 켤 것인지, 끌 것인지, 아니면 적당한 온도이므로 그대로 놔 둘 것인지)을 return하도록 한 getBoilerActionWithCurrSettingTemp 함수를 추가하여 test를 작성하였다. test에서는 24도로 온도를 설정해 둔 후에, 현재 온도에 따른 보일러의 행동을 체크하였다.

예를 들면, 온도를 24도로 설정하였는데 현재 온도가 34도이면 보일러를 꺼야 할 것이고, 14도이면 보일러를 켜야 할 것이다.

참고로 action 관련된 반환값은 #define을 사용하지 않고 enum 타입을 사용하였다. Effective C++ 를 보면, #define 보다는 enum을 사용하는 것이 컴파일 타임에 예외사항을 찾기에 더 수월하다고 한다.

void SetTempBoilerActionTestFunction(CuTest *test)
{
CuAssertTrue( test, client_setup() > 0 );

CuAssert(test, getBoilerActionWithCurrSettingTemp(24) == BOILERACTION_NOSETTINGTEMPERATURE);

CuAssert(test, setTemperature(34) == 1);
CuAssert(test, getBoilerActionWithCurrSettingTemp(24) == BOILERACTION_TURNON);
CuAssert(test, getBoilerActionWithCurrSettingTemp(27) == BOILERACTION_NOTHING); // 온도가 올라간다 ㅇㅋ
CuAssert(test, getBoilerActionWithCurrSettingTemp(30) == BOILERACTION_NOTHING);
CuAssert(test, getBoilerActionWithCurrSettingTemp(34) == BOILERACTION_NOTHING);
CuAssert(test, getBoilerActionWithCurrSettingTemp(37) == BOILERACTION_NOTHING);
CuAssert(test, getBoilerActionWithCurrSettingTemp(39) == BOILERACTION_NOTHING);
CuAssert(test, getBoilerActionWithCurrSettingTemp(40) == BOILERACTION_TURNOFF); // 5도보다 높이 올라가면 끈다
CuAssert(test, getBoilerActionWithCurrSettingTemp(36) == BOILERACTION_NOTHING);
CuAssert(test, getBoilerActionWithCurrSettingTemp(35) == BOILERACTION_NOTHING);
CuAssert(test, getBoilerActionWithCurrSettingTemp(32) == BOILERACTION_NOTHING);
CuAssert(test, getBoilerActionWithCurrSettingTemp(29) == BOILERACTION_NOTHING);
CuAssert(test, getBoilerActionWithCurrSettingTemp(28) == BOILERACTION_TURNON); // 5도보다 낮게 떨어지면 켠다

  .... // 중략

CuAssertTrue( test, client_teardown() > 0 );
}


마지막으로 구현해야 할 기능은 보일러 타이머 켜기/끄기 기능이다.

타이머를 설정할 setBoilerTimer 함수를 만들고, 파라미터를 체크하는 테스트를 만들었다. 첫 번째 인자는 지금으로부터 보일러가 켜지거나 꺼지도록 설정할 타이머 시간(단위는 분), 두 번째와 세 번째 인자는 현재 시간과 현재 분, 네 번째 인자는 보일러를 켤 것인지 아니면 끌 것인지를 표시하는 인자이다.

위에서와 마찬가지로, action 관련된 인자는 #define을 사용하지 않고 enum 타입을 사용하였다.

void TimerSettingTestFunction(CuTest *test)
{
CuAssert( test, client_setup() > 0 );

CuAssert(test, setBoilerTimer( 60, 0, 23, BOILERTIMER_TURNON ) == 1);
CuAssert(test, setBoilerTimer( 1, 0, 23, BOILERTIMER_TURNON ) == 1);
CuAssert(test, setBoilerTimer( 0, 0, 23, BOILERTIMER_TURNON ) < 0);  // 0보다 작거나 같은 숫자는 타이머로 둘 수 없음
CuAssert(test, setBoilerTimer( -3, 0, 23, BOILERTIMER_TURNON ) < 0);

CuAssert(test, setBoilerTimer( 60, -1, 23, BOILERTIMER_TURNON ) < 0);  // 현재 시간은 0~23 이어야 함
CuAssert(test, setBoilerTimer( 60, 8, 23, BOILERTIMER_TURNON ) == 1);
CuAssert(test, setBoilerTimer( 60, 9, 0, BOILERTIMER_TURNON ) == 1);
CuAssert(test, setBoilerTimer( 60, 23, 23, BOILERTIMER_TURNON ) == 1);
CuAssert(test, setBoilerTimer( 60, 24, 23, BOILERTIMER_TURNON ) < 0);  // 현재 시간은 0~23 이어야 함

.... // 중략

CuAssertTrue( test, client_teardown() > 0 );
}


개발할 때 테스트하기 제일 짜증나는 것이 타이머 관련된 기능이다. 5분 후에 켜지는 기능을 테스트하려면 5분을 기다려야 하고, 알람 설정 시 화면을 테스트하려고 하면 매번 알람을 맞추고 최대 1분 정도를 기다려야 할 수 있다.

보일러 클라이언트 프로젝트에서도 가장 고민했던 것이 타이머 관련 기능이었다. 결국, 시간 관련된 인자는 모두 파라미터로 넣어 주는 방식을 사용하여 고민을 해결하였다. 현재 시간을 파라미터로 넣어서 타이머를 설정하고, 타이머 체크 또한 일정 시간이 지난 시점의 시간을 파라미터로 넣어서 체크(getBoilerTimerAction 함수 참고)하였다. 내용은 다음과 같다.

void TimerSettingTestFunction(CuTest *test)
{
CuAssertTrue( test, client_setup() > 0 );

CuAssert(test, setBoilerTimer( 60, 6, 20, BOILERTIMER_TURNON ) == 1); // 6시 20분부터 60분 후에 켠다
CuAssert(test, getBoilerTimerAction( -1, 59) < 0); // 시간 세팅 잘못
CuAssert(test, getBoilerTimerAction( 24, 59) < 0); // 시간 세팅 잘못
CuAssert(test, getBoilerTimerAction( 6, 60) < 0); // 분 세팅 잘못
CuAssert(test, getBoilerTimerAction( 6, -1) < 0); // 분 세팅 잘못

CuAssert(test, getBoilerTimerAction( 6, 30) == BOILERTIMER_NOTHING);
CuAssert(test, getBoilerTimerAction( 6, 50) == BOILERTIMER_NOTHING);
CuAssert(test, getBoilerTimerAction( 6, 59) == BOILERTIMER_NOTHING);
CuAssert(test, getBoilerTimerAction( 7, 0) == BOILERTIMER_NOTHING);
CuAssert(test, getBoilerTimerAction( 7, 15) == BOILERTIMER_NOTHING);
CuAssert(test, getBoilerTimerAction( 7, 20) == BOILERTIMER_TURNON); // 시간이 되었으니 킵시다

CuAssert(test, setBoilerTimer( 30, 8, 15, BOILERTIMER_TURNON ) == 1); // 8시 15분부터 30분 후에 켠다
CuAssert(test, getBoilerTimerAction( 8, 20) == BOILERTIMER_NOTHING);
CuAssert(test, getBoilerTimerAction( 8, 25) == BOILERTIMER_NOTHING);
CuAssert(test, getBoilerTimerAction( 8, 44) == BOILERTIMER_NOTHING);
CuAssert(test, getBoilerTimerAction( 8, 46) == BOILERTIMER_TURNON); // 시간이 지나 버렸네. 재빨리 킵시다

.... // 중략

CuAssertTrue( test, client_teardown() > 0 );
}


5. 더 나은 보일러를 위하여...

이렇게 하여 보일러 제어 프로그램, 보다 정확히 말하면 보일러 제어 프로그램에 필요한 API 를 완성하였다. 나름 고심하여 완성하였고 만족스러운 부분도 없지는 않지만, 고쳐야 할 부분이 이곳 저곳에 보인다. 고쳐야 할 점을 정리해 보면 다음과 같다.

- network_stub 부분을 사용하지 않았다. 보일러 부분에 모든 로직을 모아 놓고, 서버를 흉내낸 부분(특정 command를 해석해서, 그에 해당하는 적당한 예제 답을 자동으로 주는 함수)또한 보일러 부분에 들어 있었다. 이와 같은 모의 서버를 network_stub부분에 넣었으면 어땠을까 하는 생각이 든다.

- 보일러에는 실제 프로그램에서는 사용되지 않을 로직이 많이 들어 있다. 이 부분을 따로 빼야 한다.

- Assert 문 사용과 관련된 점. CuAssert 만을 사용하고, CuAssertIntEquals 을 사용하지 않은 것이다. 예를 들면
  CuAssert(tc, 3 == 3); 대신에
  CuAssertIntEquals(tc, 3, 3); 이런 식으로 하면 보다 깔끔하다.

- 함수의 naming 문제. GetServerSendBuffer 함수는 보내기 전의 버퍼(SendBuffer)의 포인터를 가져오는 함수인데, 서버로 보낸 후의 값을 받아오는 함수로 오해되기 쉽다.


보일러 TDD를 통하여 얻은 교훈은 다음과 같다.

- 지금까지는 실제로 작동하는 로직만을 테스트 할 수 있다고 생각하였는데, 이와 같이 서버가 작동하지 않더라도 클라이언트에서 mock server를 간단하게 작성해 두고 사용하게 될 API를 테스트하는 식으로 구현할 수 있다는 것을 알게 되었다.

- 유닛 테스트의 범위를 잘게 쪼개는 방법에 대한 감을 찾게 되었다.

- 예외사항을 테스트에 몰아 넣음으로써 코드의 정확성을 높이는 법을 훈련하였다.

- 위에서도 언급하였지만, 타이머 방식으로 작동하는 어플리케이션을 TDD로 개발하는 법을 익히게 되었다.

- 작성된 테스트들의 빽을 믿고, 리팩토링을 거침 없이 할 수 있었다.

아직 TDD 내공이 부족하다는 것을 다시 한번 느끼게 되었고, 다양한 상황에 다양한 방법으로 적용하는 법을 익혀야겠다고 생각했다. 각 상황에서 얻은 교훈을 기록해 두고, TDD 사전으로 사용하면 다른 프로젝트에 적용할 때 도움이 많이 될 것 같다(타이머에 적용하는 TDD와 같이). TDD 적용에 관한 자신감도 얻게 되었다. 무엇보다도, TDD 적용은 역시 재미있다. ㅎㅎㅎ

PS) 이상의 프로젝트는, 애자일 컨설팅에서 진행한 TDD 교육의 리허설 방식으로 진행되었습니다. 조언해주신 김창준님께 감사드립니다.
혹시 삭제해야 하면 알려주세요.

참고 URL : http://www.yes24.com/Goods/FTGoodsView.aspx?goodsNo=267290&CategoryNumber=001001003005006

넥슨에서 근무하며 게임 포털 사이트를 만들고 있을 때, 선배 개발자 형으로부터 한 책을 추천받았다. '리팩토링' 이라는 책이었으며, 간단하게 '리팩토링이란 소스를 다듬는 것이다...' 라고 들었던 것을 기억한다.

이 당시 Java로 커뮤니티 사이트(http://club.nexon.com)를 만들고 있었다. 개발 초창기에 다른 사람(알바)이 한달 정도 끄적거리던 소스를 넘겨받아 보았더니 JSP 파일에 DB 커넥션 하는 부분 및 business 로직까지 몽창 다 들어있던...소싯적 ASP 하던 때와 똑같은 분위기로 코딩되어 있던 것을 보고 경악했던 기억이 있다 -_-. 결국 DB 로직은 따로 빼고, business 로직 부분은 좀 귀찮아서(일정도 있고, 이런 저런 핑계로) 일부은 따로 뺐지만 일부는 걍 JSP 파일에 남겨두었던 것 같다. 다른 사람에 의해 구현되었던 부분이 별로 없었기에 가능했던 일이었을 지도 모르겠다.

암튼 사정이 이러다 보니 다듬는다고 다듬었는데 소스도 꽤 지저분하고, 개념도 덜 탑재되어 있던 상황이라서 소스는 중복에 중복 투성이(문자열 인코딩하는 부분의 소스가 특히 아주 지저분했던 것 같다. 만들었던 메소드 또 만들고 또 만들고 이름만 바꿔 추가하고 등등 -_-;) 였던 것 같다. 그럼에도 불구하고 나름대로 회사 다니면서 학교를 다니던 상황이라서 시간이 빡빡했던 것도 사실이었고(지하철에서 예습복습하고 회사에 늦게 가서 11시 넘어 퇴근하고 다음날 아침에 또 학교가는 생활의 연속 ㅠㅠ) 이러 저러한 핑계로 그 형의 제안을 묵살 아닌 묵살을 해 버리고 말았다.

LG전자에 들어와서 임베디드 S/W 개발자로 커리어 패스를 전환한 후에 이런 저런 스펙(WAP, MMS 관련) 스터디도 하고, XP 관련 세미나도 기웃거리고 하면서 느낀 점은 내가 전에 웹 개발을 하면서 너무 기본을 소홀히 하고 구현에만 급급했다는 생각이었다. 웹 개발자가 웹 스펙조차 본 적이 없었다는 점과 소스 리뷰를 소홀히 했다는 생각이 나를 자책하게 만들었다.

그래서 예전 선배 형에게 심정적인 사죄도 하고, 새로 옮길 팀에서도 리팩토링을 수행하고 있다고 하기에 대비도 할 겸 겸사겸사 책을 구해서 읽게 되었다.

서두가 너무 길었다.

책 내용은 리팩토링을 예제를 들어서 설명하고, 리팩토링의 원리에 대해서 간단히 설명한 후, 리팩토링을 해야 할 징후가 나타나는 코드 속의 징후에 대해 설명한 후에, 리팩토링 시 반드시 필요한 TDD 에서도 사용되는 자동 테스트 프레임워크인 JUnit에 대해 설명한 후, 여러 리팩토링 카탈로그(리팩토링 방법을 사전식으로 죽 나열한 것)에 대한 설명과 예제 나열로 구성되어 있다.

리팩토링을 간단하게 말하면 '코드 정리' 이겠지만, 보다 중요한 개념은 '코드의 의미 명확화' 이다. 저자는 코드 자체가 주석을 필요로 하지 않아도 될 정도로 의미가 명확하게 되는 것을 궁극적인 목적으로 하고 있다. '6. 메소드 정리' 의 내용을 보자면, 긴 메소드에서 짧은 메소드로 추출해 낼 때에도, 아무렇게나 추출하지 않고 적절한 기능을 가진 코드 라인을 그 기능에 걸맞는 명확한 이름을 부여하여 추출해 내야 한다고 말하고 있다. 코드에 임시 지역 변수가 남발되는 것 또한 경계하고 있는데 그렇게 되면 지역 변수가 어떤 역할을 하는지 의미가 모호해져 코드를 분석할 수 없게 되기 때문이다.

JUnit에 대한 설명이 나와 있는 것을 보고 알 수 있듯이, 리팩토링 또한 XP 방법론이 따라가는 철학의 일부라는 생각이 든다. 리팩토링을 완전무결한 Design을 통한 S/W 개발이 아니라, Design은 어느 정도만 하고 추가할 점이 발견되면 그때 그때 개선하는 S/W 개발 방법론(?)을 보조하기 위한 필수 단계로 설정해 놓은 점이 이채롭다.

리팩토링은 퍼포먼스 튜닝과는 대비되는 개념이지만, 퍼포먼스 튜닝을 보조할 수 있는 수단이라는 점도 흥미로왔다. 소스를 정리하고, 복잡한 메소드를 나누며, 복잡한 클래스를 서브클래싱 하는 등 소스를 리팩토링 하게 되면 인스턴스를 많이 생성하게 되는 등 퍼포먼스가 일시적으로는 떨어질 수 있지만, 리팩토링이 완성된 후에는 소스를 이해하기 쉬워지고 퍼포먼스 튜닝하기도 용이해지므로 우선 리팩토링을 한 후에 퍼포먼스 튜닝을 하라는 내용이 정말 공감이 갔다.
사실, 디버깅이나 튜닝이나 실제 디버깅 or 튜닝하는데 드는 시간보다는 디버깅 or 튜닝을 해야 하는 곳을 찾는데 시간이 90% 이상 드는 것이 사실이다. (현재 Trace32 같은 디버거도 없이 폰에 생긴 버그를 수정해야 하는 상황에 있는데 미쳐 버릴 것 같은 것이 현실이다. 더군다나 지금 쓰는 GSM 솔루션은 매우 지저분해서 하나의 함수가 1000 라인이 넘어가는 경우도 허다하다 -_-) 리팩토링은 그 찾는 시간을 줄여줄 수 있는 좋은 수단이 될 수 있을 것 같다.


팀을 무사히 옮기게 되면, 옮긴 팀에서는 리팩토링, TDD 등을 사용해 볼 수 있을 것 같기도 하다. 구슬이 서말이라도 꿰어야 보배라고...실무에 적용해 보고 이 내용에 깊이 공감해 볼 수 있게 되기를 바란다. 리팩토링은 현재 만들어 지고 있는 이론이고, 전산학의 전공 필수 과목처럼 정착된 분야가 아니기 때문에 책 내용은 따분한 이론의 나열이 아닌, 저자의 경험 중심으로 이해하기 쉽고 재미있게 되어 있다. 한 번 읽어 보면 소스 리팩토링할 때에는 물론 코드를 작성할 때의 나쁜 버릇을 고치는 데에도 도움이 많이 될 것 같다.

CUnit

Agile Story 2006/03/02 10:22
http://cunit.sourceforge.net/

cUnit이라는 툴은 자동화 테스팅을 위한 c 단위 테스트 툴이라고 한다.

테스트를 자동화하기 위해서 XP 방법론 책에서 추천한 툴이다. 기타 CppUnit, PerlUnit, PyUnit, JUnit 등등이 있다고 한다.