낡은 코드 (Legacy Code) 로  효과적으로 일하기 박일 (parkpd.egloos.com)
낡은 코드 (Legacy Code)?
낡은 코드 (Legacy Code) 가 뭐죠 ? <ul><li>낡은 코드 ( 낡은 프로젝트 ) </li></ul><ul><ul><li>(1)  자동화 테스트가 되어 있지 않고 ,  리팩토링 도 안 되어 있는 프로젝트 <...
' 소프트웨어 변경 ' 의 어려움 <ul><li>어떻게 보면 ,  소프트웨어를 변경하는 건 매우 쉽다 </li></ul><ul><li>그게 ,  우리 프로그래머의 저주의 시작일지도 ? </li></ul>
배경 <ul><li>이런 지저분한 코드에 새로운 기능을 추가해야 한다면 </li></ul>if (m_nCriticalError) return; m_nCriticalError=nErrorCode; switch (nEven...
소프트웨어 변경의 두 가지 방법 <ul><li>시스템을 변경하는 두 가지 방법 .  </li></ul><ul><ul><li>기존 코드를 그냥 수정한다 </li></ul></ul><ul><ul><li>새로운 클래스나 함수...
낡은 코드의 위험성 <ul><li>원래 있던 코드를 바로 고치는 게 낡은 코드를 수정하는 가장 안전한 방법일까 ? </li></ul><ul><ul><li>테스트 할 방법이 없다면 ,  기존 코드를 수정하는 도중에 버그를...
코드 변경 <ul><li>코드 변경은 버그를 만들어내지만 ,  코드를 고치지 않으면  ( 결국에는 )  더 많은 버그가 생길 수 있다 </li></ul><ul><li>한 번도 리팩토링하지 않은 프로젝트라면 </li></...
리팩토링이 필요하다
변경의 모순 (Paradox of Change) <ul><li>코드를 변경하다 보면 ,  버그가 생긴다 </li></ul><ul><li>코드를 변경 ( 리팩토링 ) 하지 않으면 ,  코드가 점점 복잡해져 ,  버그가 생...
UnitTest  가 필요하다
경고 <ul><li>UnitTest  를 추가하게 되면 </li></ul><ul><ul><li>여러분은 코드를 더 작성하게 됩니다 . </li></ul></ul><ul><ul><li>UnitTest  를 위한  fram...
작업의 모순 (Paradox of Work) <ul><li>일을 많이 하면 ,  코드를 많이 고치게 된다 </li></ul><ul><ul><li>코드를 많이 고치면 ,  버그도 많이 만들게 된다 </li></ul></u...
억울하지 않은지 ? <ul><li>홈런 타자는 삼진도 가장 많이 당하지만 ,  사람들은 홈런만 기억한다 </li></ul><ul><li>프로그래머는 기능을 많이 구현하지만 ,  사람들은 프로그래머의 버그를 더 기억한다 ...
낡은 코드에 테스트 추가하기
낡은 코드 변경 알고리즘 <ul><li>변경 지점 찾기 </li></ul><ul><li>테스트 할 곳 찾기 </li></ul><ul><li>의존 제거하기 </li></ul><ul><li>테스트 작성 </li></ul><...
변경 지점 찾기 <ul><li>2007 년  8 월  16 일 ( 목 ) ' 리니지 II  혼돈의왕좌  - 1st Throne :  더 카마엘‘ 업데이트 내용 </li></ul><ul><ul><li>PC  관련 변경  ...
변경 지점 찾기 <ul><li>기존 코드 ( 절대로  fake code  입니다 !!!) </li></ul>class CMonster : public CActor { public: void Attack(CActor*);...
테스트 할 곳 찾기 <ul><li>무엇을 테스트할 수 있을까 ? </li></ul><ul><ul><li>70  랩  vs 70  랩의 명중 / 회비 확률 비교 </li></ul></ul><ul><ul><li>70  랩 ...
의존 제거하기 <ul><li>대부분의 프로그램은  결합되어 (glued)  있다  </li></ul><ul><li>각 부분별로  독립적으로  테스트하려고  시도해 보기 전에는 ,  이런 사실을 실감하지 못한다 . </l...
결합의 종류 <ul><li>singleton (=  전역 변수 ) </li></ul><ul><li>내부에서 다른 클래스를 초기화해서 사용 </li></ul><ul><li>구현 클래스와의 의존  </li></ul><ul>...
의존 제거하기 <ul><li>IDE  에서  extract method, extract interface  같은 리팩토링을 잘 지원한다면 최대한 많이 활용하자 . </li></ul><ul><li>( 그러나  MSVC++...
테스트 작성 TEST_FIXTURE(FixtureActor, 70CantHit70) { pActor1->m_Level = 70; pActor2->m_Level = 70; CHECK(!pActor1->CanHit(pAct...
리팩토링 혹은 변경 <ul><li>이제 리팩토링을 하는 도중에 ,  이전에 잘 돌아가던 기능이 잘못되지 않는다는 것을 보장받기 때문에 마음 편하게 리팩토링 할 수 있다 . </li></ul><ul><li>( 사실 )  ...
의존 제거하기
의존 제거하기  0  단계 인터페이스 추출 (Extract Interface) 구현 추출 (Extract Implementor)
인터페이스 추출 <ul><li>낡은 코드에 가장 필요한 리팩토링 </li></ul><ul><li>안전하다 . </li></ul><ul><ul><li>몇 가지 수칙만 기억하고 있다면 ,  전혀 위험하지 않다 . </li>...
인터페이스 추출 <ul><li>인터페이스 </li></ul><ul><ul><li>순수 가상 함수만 있는 클래스 </li></ul></ul><ul><ul><li>안전하게 다중 상속해서 ,  사용자에게 다양한 모습으로 보여...
인터페이스 추출 순서 <ul><li>해당 클래스의 자식 클래스에서 이 멤버 함수를 일반 함수 (non-virtual)  로 만들어 쓰고 있는 건 없는지 확인한다 . </li></ul><ul><li>인터페이스 클래스를 하...
일반 함수 (Non-Virtual) 오버라이딩 문제 <ul><li>C++  에서 인터페이스 추출할 때 ,  실수하기 쉬운 부분 </li></ul>class CMonster : public CActor { public: ...
일반 함수 (Non-Virtual) 오버라이딩 문제 <ul><li>가상 함수로 만들면 ,  자식 클래스의 똑같은 (same signature) 의 함수도 자동으로 가상 함수가 된다 . </li></ul>class CMo...
일반 함수 (Non-Virtual) 오버라이딩 문제 <ul><li>해결방법 </li></ul><ul><ul><li>인터페이스를 추출하기 전에 ,  해당 클래스를 상속받는 클래스가 있는지 본다 . </li></ul></u...
구현 추출 (Extract Implementor) <ul><li>인터페이스 추출이랑 완전 같은데 ,  끌어 올리는 거 (pull up)  대신 끌어 내린다 (push down) 는 것만 다르다 . </li></ul>
의존 제거하기  1  단계 함수에 매개변수 추가 (Parameterize Method) 함수 추출 후 오버라이딩 (Extract and Override Call)
함수에 매개변수 추가 <ul><li>어떤 함수가 내부에서 다른 클래스를 생성하는 숨은 의존관계 (hidden dependency) 가 있다면 ,  함수를 새로 만들어서 그 클래스를 인자로 받을 수 있게 하자 . </li...
예  :  몬스터가 공격 당할 때 void CMonster ::Attack(CActor* pTarger) { double angerBonus = 1.0; if (m_VeryAngry) { CAngryData* pAngr...
함수에 매개변수 추가  -  단계 <ul><li>새로운 함수를 만들어 ,  내부에서 생성하던 클래스를 인자로 만든다 . </li></ul><ul><li>원래 함수로부터 코드를 잘라내 붙이고 ,  클래스 생성 부분은 제거...
함수 추출 후 오버라이딩 <ul><li>함수 내에서 다른 것을 호출하는 좋지 않은 의존관계가 있다면 ,  이 부분을 함수로 추출한 후 ,  테스트용 자식 클래스에서 이 함수를 오버라이딩한다 </li></ul>
함수 추출 후 오버라이딩  -  단계 <ul><li>다른 함수 호출 부분을 새로운 멤버 함수로 추출한다 .( 형태는 그대로 유지하자 ) </li></ul><ul><li>새로운 멤버 함수를 가상 함수로 만든다 . </li...
함수 추출 후 오버라이딩  -  예 void CPlayer::Attack(CActor* pTarger) { ... WSASend(m_Handle...); //  공격과 함께 데미지 표시 //  변경 후 void CPla...
더 할 수 있다 class CMockSocket : public CSocket { void Send(int packetType) { m_PacketRecorder.Push(packetType); TEST_FIXTURE(...
의존 제거하기  2  단계 <ul><li>Seams  이용하기 </li></ul><ul><ul><li>1.  가상함수 이용 </li></ul></ul><ul><ul><li>2. define  을 이용한 코드 변경 </l...
Seams? <ul><li>뜻  :  솔기 ,  이은 자리 ,  흉터 </li></ul><ul><li>Seam  은 외부에서 행동을 변경할 수 있는 프로그램의 한 지점이다 </li></ul>
Seam  예제 void CPlayer::Logout() { g_Database.SavePlayerStatus(this); ... } CDatabase::SavePlayerStatus()  를 하기 위해서는 꼭  DB ...
가상함수 이용  -  예 CPlayer::CPlayer(CDatabase* pDatabase) : m_pDatabase(pDatabase) {} void CPlayer::Logout() { m_pDatabase->Sav...
가상함수 이용 <ul><li>장점 </li></ul><ul><ul><li>언어가 제공하는 기능으로 원하는 작업을 할 수 있다 </li></ul></ul><ul><ul><li>가상 함수로 만들면 ,  디자인 적으로 다른 ...
define  을 이용한 코드 변경  -  예 void CPlayer::Logout() { CDatabase::Inst().SavePlayerStatus(this);  // singleton  사용 ... } class...
define  을 이용한 코드 변경 <ul><li>장점 </li></ul><ul><ul><li>일반 프로그래밍 기법으로 할 수 없는 것을 할 수 있다 </li></ul></ul><ul><ul><li>성능 저하를 막고 ,...
링크  -  예 class CDatabase { void SavePlayerStatus(CActor* pActor) { sql_query(“update ...”); #ifndef __UnitTest__ #pragma c...
링크 <ul><li>장점 </li></ul><ul><ul><li>기존 코드를 하나도 안 고쳐도 된다 </li></ul></ul><ul><ul><li>외부 소프트웨어와의 의존을 제거할 수 있다 </li></ul></ul>...
의존 제거하기  3  단계 정적 함수 노출하기  (Expose Static Method)
예  : CPlayer  의 공격 코드 class CPlayer : public CActor { bool CanHit(CActor*); void Attack(CActor* pTarget); ... void CPlayer...
어려운 점 <ul><li>CPlayer  는  TCP  소켓 연결을 하지 않으면 객체를 생성하기 어렵게 되어 있다 . </li></ul><ul><li>new CPlayer  를 하지 않고도 ,  명중 여부를 확인할 방법...
CPlayer  의 공격 코드  -  수정판 class CPlayer : public CActor { static bool CanHit (CActor* pAttacker, CActor* pTarget); void Att...
코드가 이상하지 않나 ? <ul><li>CanHit  을 굳이  static  으로 만들고 , this  를 넘기는 게 디자인을 망치는 건 아닐지 ? </li></ul><ul><ul><li>멤버 함수도 내부적으로  st...
의존 제거하기  4  단계 템플릿 재정의 (Template Redefinition)
예  : test  코드 실행 중  dead lock  이 걸려요 . class CPlayer : public CActor { CMutexLock m_Lock;  // recursive lock  아님 void BeAt...
해결 방법 class CMutexLockImpl : public ILock { void Lock() { WaitForSingleObject(...); class CDummyLockImpl  : public ILock {...
더 할 수 있다 ! class CDummyLockImpl : public ILock { void Lock() { if (CDummyLockImpl::Inst().IsDeadLock()) { throw CException...
그 외 임의성 테스트
임의성 테스트 <ul><li>타격 크리티컬 같이  random  값이 들어가는 계산은 어떻게 테스트 할 수 있을까 ? </li></ul>int GetRand() const { #if defined(_DEBUG) && d...
다른 것들 <ul><li>Sprout Method / Class </li></ul><ul><li>Interception Points </li></ul><ul><li>Pinch Point Traps </li></ul><u...
테스트의 종류
테스트의 종류 스토리 테스트 비즈니스 의도 ( 제품 설계 ) 사용성 테스팅 탐색적 테스팅 단위 테스트 개발자 의도 ( 코드 설계 ) 특성 테스팅 보안 테스팅 부하 테스팅 조합 테스팅 … 자동 자동 수동 도구
특성 테스트 <ul><li>일종의  Regression Test </li></ul><ul><li>변경하려는 클래스를 위해 가장 먼저 만드는 테스트 </li></ul><ul><li>이 테스트는 현재 상태의 특성을 나타낸다...
특성 테스트 <ul><li>어떤 종류의 테스트가 필요할까 ? </li></ul><ul><ul><li>필요한 만큼 많이 만들수록 좋다 . </li></ul></ul><ul><ul><li>클래스에 대해 자신감이 생기고 </...
예  :  공격 코드에 특성 테스트를 붙인다면 ? void CPlayer::BeAttacked(CActor* pAttacker) { int damage = GetDamageFromAttacker(pAttacker); T...
더 할 수 있다 . TEST_FIXTURE(FixtureAttack, GetDamage) { struct TestData { int m_Level; int m_Expect; }; TestData data[] = { {4...
테스트 코드의 리팩토링 <ul><li>테스트 코드가 넘치면 관리가 안 됨 </li></ul><ul><li>팀원들이  #define  을 임의로 고치거나 해서 테스트를 하지 않으려 할 수 있음 . </li></ul>
처음 테스트 붙이는 요령
처음 테스트 붙이는 요령 <ul><li>정적 함수 노출하기  (Expose Static Method)   을 이용해서 쉬운 테스트부터 붙여본다 . </li></ul><ul><li>테스트 코드 안에서 여러 객체를 생성해 ...
처음 테스트 붙이는 요령 <ul><li>변경 요청이 들어오는 기능부터 테스트를 살살 붙여본다 . </li></ul><ul><ul><li>변경 요청이 들어온 기능은 작업이 끝나면  QA  팀이 한 번 더 검사해 주기 때문...
처음 테스트 붙이는 요령 <ul><li>낡은 코드에  UnitTest 를 붙이다 보면 문제가 생길 수 있으므로 가능하면 그나마 덜 심각한 부분부터  Test  를 붙여나간다 . </li></ul><ul><ul><li>스...
처음 테스트 붙이는 요령 <ul><li>private method 를  public method  로 바꾼다 . </li></ul><ul><li>너무너무 그러는 게 싫다면 다음과 같이 해 보자 . </li></ul>
class CTest{ protected: int m_Test; void Test() {} }; class CMockTest : public CTest { public: using CTest::m_Test; //  부모...
아니 ?  은닉화 (encapsulation)  은 ? <ul><li>private  과  protected  를 해야 안전한 거 아닌지 ? </li></ul><ul><ul><li>테스트 없는  private  보다 ,...
UnitTest  가 아닐 조건 <ul><li>Database  를  access  한다 . </li></ul><ul><li>네트워크 전송을 한다 . </li></ul><ul><li>파일을 읽고 쓴다 . </li></u...
프로그래머를 위한  UnitTest <ul><li>개발 속도 증가 </li></ul><ul><li>화성 탐사선  spirit </li></ul><ul><ul><li>응답 속도  14 분 ! </li></ul></ul>
프로그래머를 위한  UnitTest <ul><li>빠른  feedback! </li></ul><ul><li>실수를 금방 알기 ! </li></ul><ul><ul><li>1 분 전에 만든 버그를 고치는 것과 </li></...
Test Driven Debugging? <ul><li>일반적인 디버깅 방법은 ? </li></ul><ul><li>버그 리포트 시스템에 새로운 버그 추가 </li></ul><ul><li>게임 스크립트 데이타 받아서 컴파...
Test Driven Debugging!! <ul><li>TDD  를 이용할 때 </li></ul><ul><ul><li>디버그 관리자에 새로운 버그 추가 </li></ul></ul><ul><ul><li>게임 스크립트 데...
Branch & Merge <ul><li>Branch  후  Merge  작업 </li></ul><ul><li>Merge  하면서 다른 팀원이 바꾸어 놓은 코드 때문에 버그 발생 </li></ul><ul><ul><li>...
UnitTest  는 극히 일부분입니다 <ul><li>이는 남을 탓할 것이 아니라 세상 사물에 관심을 가진 이들이 저 좋은 것 보기에 급급한 나머지 사람 보기를 게을리 했기 때문이다 ... 언제나 모든 것의 마지막 조각...
다른 팀과의 협조 <ul><li>QA  팀과의 협조 </li></ul><ul><ul><li>원하는 기능 리스트 얻어서 시간 날 때마다 작업해 주기 </li></ul></ul><ul><li>기획팀과의 협조 </li></u...
UnitTest  결과를  email  로 축적 <ul><li>다른 팀원이 내가 설치한 테스트를 실패해서 고생하고 있을 때 바로 이메일로 확인할 수 있음 </li></ul><ul><li>테스트가 늘어남에 따라 ,  테스...
References <ul><li>Michael Feathers </li></ul><ul><ul><li>[email_address] </li></ul></ul><ul><ul><li>http://www.xpnl.org/h...
References <ul><li>TDD, UnitTest for games in KGC 2007 </li></ul><ul><li>http://unittest-cpp.sourceforge.net/ </li></ul><u...
Books
감사합니다 .
Upcoming SlideShare
Loading in...5
×

카사 공개세미나1회 W.E.L.C.

1,600

Published on

Published in: Technology
0 Comments
3 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total Views
1,600
On Slideshare
0
From Embeds
0
Number of Embeds
2
Actions
Shares
0
Downloads
0
Comments
0
Likes
3
Embeds 0
No embeds

No notes for slide
  • 폭탄 돌리기처럼 , 어느 순간 너무 복잡해져서 리팩토링 하지 않고서는 코드를 추가할 수 없는 그 상황이 되기 전까지는 내 일이 아닌 것이다 .
  • 카사 공개세미나1회 W.E.L.C.

    1. 1. 낡은 코드 (Legacy Code) 로 효과적으로 일하기 박일 (parkpd.egloos.com)
    2. 2. 낡은 코드 (Legacy Code)?
    3. 3. 낡은 코드 (Legacy Code) 가 뭐죠 ? <ul><li>낡은 코드 ( 낡은 프로젝트 ) </li></ul><ul><ul><li>(1) 자동화 테스트가 되어 있지 않고 , 리팩토링 도 안 되어 있는 프로젝트 </li></ul></ul><ul><ul><li>(2) 인수인계 받은 프로젝트 . 코드도 방대하고 , 실행은 되지만 , 조금만 고쳐도 여기저기에서 에러가 나서 , 수정이 거의 불가능한 프로젝트 </li></ul></ul><ul><ul><li>(3) 이력서 쓰는 데 아무런 도움을 주지 않는 언어나 플랫폼으로 만들어진 프로젝트 </li></ul></ul><ul><li>지금 당신도 낡은 코드를 작성하고 있을지 모른다 ! </li></ul>
    4. 4. ' 소프트웨어 변경 ' 의 어려움 <ul><li>어떻게 보면 , 소프트웨어를 변경하는 건 매우 쉽다 </li></ul><ul><li>그게 , 우리 프로그래머의 저주의 시작일지도 ? </li></ul>
    5. 5. 배경 <ul><li>이런 지저분한 코드에 새로운 기능을 추가해야 한다면 </li></ul>if (m_nCriticalError) return; m_nCriticalError=nErrorCode; switch (nEvent) { case FD_READ: case FD_FORCEREAD: if (GetLayerState()==connecting && !nErrorCode)
    6. 6. 소프트웨어 변경의 두 가지 방법 <ul><li>시스템을 변경하는 두 가지 방법 . </li></ul><ul><ul><li>기존 코드를 그냥 수정한다 </li></ul></ul><ul><ul><li>새로운 클래스나 함수를 만든다 </li></ul></ul><ul><li>많은 프로그래머가 낡은 코드에서는 기존 코드를 그냥 수정한다 . 이유는 , </li></ul><ul><ul><li>가장 빨리 할 수 있고 </li></ul></ul><ul><ul><li>가장 안전해 보이기 때문 </li></ul></ul><ul><li>젠가 , 폭탄 돌리기 </li></ul>
    7. 7. 낡은 코드의 위험성 <ul><li>원래 있던 코드를 바로 고치는 게 낡은 코드를 수정하는 가장 안전한 방법일까 ? </li></ul><ul><ul><li>테스트 할 방법이 없다면 , 기존 코드를 수정하는 도중에 버그를 만들기 쉽다 </li></ul></ul><ul><ul><li>변경한 부분이 시스템에 어떤 영향을 미칠지 , 뭔가 바뀐 부분은 없는지를 전부 다 파악해야 한다 </li></ul></ul><ul><li>가장 안전한 방법은 코드를 아예 수정하지 않는 것이다 ! </li></ul>
    8. 8. 코드 변경 <ul><li>코드 변경은 버그를 만들어내지만 , 코드를 고치지 않으면 ( 결국에는 ) 더 많은 버그가 생길 수 있다 </li></ul><ul><li>한 번도 리팩토링하지 않은 프로젝트라면 </li></ul><ul><ul><li>코드가 이해도 안 되고 </li></ul></ul><ul><ul><li>기능을 새로 추가하기 어렵고 </li></ul></ul><ul><ul><li>변경하려면 신경 쓸 게 많아 , 스트레스가 쌓이고 </li></ul></ul><ul><ul><li>이런 스트레스가 ... 버그를 만들고 </li></ul></ul><ul><ul><li>이렇게 추가된 버그가 야근 모드 (crunch) 의 우리를 더 괴롭게 만드는 경험이 없었는지 ? </li></ul></ul>
    9. 9. 리팩토링이 필요하다
    10. 10. 변경의 모순 (Paradox of Change) <ul><li>코드를 변경하다 보면 , 버그가 생긴다 </li></ul><ul><li>코드를 변경 ( 리팩토링 ) 하지 않으면 , 코드가 점점 복잡해져 , 버그가 생기기 쉬워진다 </li></ul><ul><li>이런 딜레마를 해결하기 위해서는 우리가 변경한 부분에 대한 확신을 가질 수 있어야 한다 </li></ul><ul><ul><li>어떻게 제대로 고친 걸 확신할 수 있을까 ? </li></ul></ul><ul><ul><li>어떻게 이전에 잘 돌아가는 것을 고장내지 않았다고 확신할 수 있을까 ? </li></ul></ul>
    11. 11. UnitTest 가 필요하다
    12. 12. 경고 <ul><li>UnitTest 를 추가하게 되면 </li></ul><ul><ul><li>여러분은 코드를 더 작성하게 됩니다 . </li></ul></ul><ul><ul><li>UnitTest 를 위한 framework 를 만들면서 버그도 많이 만드시게 될 겁니다 . </li></ul></ul><ul><ul><li>DB 나 Rendering, Multithread 같이 UnitTest 를 붙이기 어려운 분야도 많습니다 . </li></ul></ul><ul><li>그럼 왜 일을 더 해 가면서 , 버그도 더 만들어 가면서 , 낡은 코드에 UnitTest 를 붙여야 할까 ? </li></ul>
    13. 13. 작업의 모순 (Paradox of Work) <ul><li>일을 많이 하면 , 코드를 많이 고치게 된다 </li></ul><ul><ul><li>코드를 많이 고치면 , 버그도 많이 만들게 된다 </li></ul></ul><ul><ul><li>팀 내의 평판이 안 좋아진다 </li></ul></ul><ul><li>일을 적게 하면 , 코드를 거의 고치지 않는다 </li></ul><ul><ul><li>코드를 고치지 않으면 , 버그도 거의 만들지 않는다 </li></ul></ul><ul><ul><li>팀 내의 평판이 좋아진다 ?! </li></ul></ul>
    14. 14. 억울하지 않은지 ? <ul><li>홈런 타자는 삼진도 가장 많이 당하지만 , 사람들은 홈런만 기억한다 </li></ul><ul><li>프로그래머는 기능을 많이 구현하지만 , 사람들은 프로그래머의 버그를 더 기억한다 </li></ul><ul><li>일도 열심히 하고 , 버그도 적게 만들어 , 안정적인 서비스를 개발하고 , 스트레스도 적게 받자 . </li></ul>
    15. 15. 낡은 코드에 테스트 추가하기
    16. 16. 낡은 코드 변경 알고리즘 <ul><li>변경 지점 찾기 </li></ul><ul><li>테스트 할 곳 찾기 </li></ul><ul><li>의존 제거하기 </li></ul><ul><li>테스트 작성 </li></ul><ul><li>리팩토링 혹은 변경 </li></ul>
    17. 17. 변경 지점 찾기 <ul><li>2007 년 8 월 16 일 ( 목 ) ' 리니지 II 혼돈의왕좌 - 1st Throne : 더 카마엘‘ 업데이트 내용 </li></ul><ul><ul><li>PC 관련 변경 - 레벨에 따른 능력치 상승 </li></ul></ul><ul><ul><ul><li>70 레벨 이상 PC 의 전투 능력을 대폭 증가 시켰습니다 . </li></ul></ul></ul><ul><ul><ul><li>아래 사항은 PC vs 몬스터 관계에서도 동일하게 적용됩니다 . </li></ul></ul></ul><ul><ul><li>명중 , 회피 능력 </li></ul></ul><ul><ul><ul><li>70 레벨 이상 PC 의 명중 , 회피 능력이 증가 하였습니다 . </li></ul></ul></ul><ul><ul><ul><li>78 레벨 이상 PC 는 이 보너스가 대폭 상승합니다 . </li></ul></ul></ul><ul><ul><ul><li>예를 들어 , 공격자와 방어자의 레벨이 70 이상이라고 하여도 방어자의 레벨이 공격자 보다 더 높을 경우 , 방어자는 기존 보다 더 높은 확률로 회피를 하게 됩니다 . </li></ul></ul></ul>
    18. 18. 변경 지점 찾기 <ul><li>기존 코드 ( 절대로 fake code 입니다 !!!) </li></ul>class CMonster : public CActor { public: void Attack(CActor*); void BeAttacked(CActor*); }; class CBoss : public CMonster { public: void Attack(CActor*); void BeAttacked(CActor*); }; void CPlayer::Attack(CBoss* pTarget) { if (CanHit(pTarget)) { pTarget ->BeAttacked(this); } };
    19. 19. 테스트 할 곳 찾기 <ul><li>무엇을 테스트할 수 있을까 ? </li></ul><ul><ul><li>70 랩 vs 70 랩의 명중 / 회비 확률 비교 </li></ul></ul><ul><ul><li>70 랩 vs 78 랩의 명중 / 회비 확률 비교 </li></ul></ul><ul><ul><li>CanHit() 주위를 변경하자 </li></ul></ul>void CPlayer::Attack(CBoss* pTarget) { if (CanHit(pTarget)) { pTarget ->BeAttacked(this); } };
    20. 20. 의존 제거하기 <ul><li>대부분의 프로그램은 결합되어 (glued) 있다 </li></ul><ul><li>각 부분별로 독립적으로 테스트하려고 시도해 보기 전에는 , 이런 사실을 실감하지 못한다 . </li></ul>
    21. 21. 결합의 종류 <ul><li>singleton (= 전역 변수 ) </li></ul><ul><li>내부에서 다른 클래스를 초기화해서 사용 </li></ul><ul><li>구현 클래스와의 의존 </li></ul><ul><ul><li>클래스 내부에서 다른 구현 클래스 (concrete class) 를 사용한다 . </li></ul></ul><ul><li>덕분에 , new CPlayer 나 new CMonster 를 하는 것부터가 쉽지 않을 것이다 . </li></ul>
    22. 22. 의존 제거하기 <ul><li>IDE 에서 extract method, extract interface 같은 리팩토링을 잘 지원한다면 최대한 많이 활용하자 . </li></ul><ul><li>( 그러나 MSVC++ 에서와 같이 ) 그런 기능이 없다면 , 리팩토링은 직접 해야 한다 . 의존을 제거하기 위해 하는 리팩토링은 최대한 단순하게 하자 . </li></ul><ul><li>의존을 제거하는 과정에서 일시적으로 지저분한 코드가 만들어질 수 있다 . </li></ul>
    23. 23. 테스트 작성 TEST_FIXTURE(FixtureActor, 70CantHit70) { pActor1->m_Level = 70; pActor2->m_Level = 70; CHECK(!pActor1->CanHit(pActor2)); } TEST_FIXTURE(FixtureActor, 78LevelCanHit70) { pActor1->m_Level = 78; pActor2->m_Level = 70; CHECK(pActor1->CanHit(pActor2)); }
    24. 24. 리팩토링 혹은 변경 <ul><li>이제 리팩토링을 하는 도중에 , 이전에 잘 돌아가던 기능이 잘못되지 않는다는 것을 보장받기 때문에 마음 편하게 리팩토링 할 수 있다 . </li></ul><ul><li>( 사실 ) 그래도 조심스럽게 리팩토링 해야한다 . </li></ul>
    25. 25. 의존 제거하기
    26. 26. 의존 제거하기 0 단계 인터페이스 추출 (Extract Interface) 구현 추출 (Extract Implementor)
    27. 27. 인터페이스 추출 <ul><li>낡은 코드에 가장 필요한 리팩토링 </li></ul><ul><li>안전하다 . </li></ul><ul><ul><li>몇 가지 수칙만 기억하고 있다면 , 전혀 위험하지 않다 . </li></ul></ul><ul><li>금방 된다 </li></ul>
    28. 28. 인터페이스 추출 <ul><li>인터페이스 </li></ul><ul><ul><li>순수 가상 함수만 있는 클래스 </li></ul></ul><ul><ul><li>안전하게 다중 상속해서 , 사용자에게 다양한 모습으로 보여질 수 있다 </li></ul></ul>
    29. 29. 인터페이스 추출 순서 <ul><li>해당 클래스의 자식 클래스에서 이 멤버 함수를 일반 함수 (non-virtual) 로 만들어 쓰고 있는 건 없는지 확인한다 . </li></ul><ul><li>인터페이스 클래스를 하나 만든다 . </li></ul><ul><li>해당 클래스가 이 인터페이스를 상속하게 한다 . </li></ul><ul><li>해당 클래스를 참조하는 곳을 전부 새로 만든 인터페이스 이름으로 바꾼다 . </li></ul><ul><li>컴파일을 돌려서 , 어떤 함수가 필요한지를 알아본다 . </li></ul><ul><ul><li>Lean on the Compiler </li></ul></ul><ul><li>에러 나는 함수를 모두 인터페이스 쪽으로 복사한다 . 순수 가상 함수 (pure virtual) 로 만드는 걸 잊지 말자 . </li></ul>
    30. 30. 일반 함수 (Non-Virtual) 오버라이딩 문제 <ul><li>C++ 에서 인터페이스 추출할 때 , 실수하기 쉬운 부분 </li></ul>class CMonster : public CActor { public: void Attack(CActor*); void BeAttacked(CActor*); }; class CBoss : public CMonster { public: void Attack(CActor*); void BeAttacked(CActor*); }; void CPlayer::Attack(CBoss* pTarget) { if (CanHit(pTarget)) { pTarget ->BeAttacked(this); } }
    31. 31. 일반 함수 (Non-Virtual) 오버라이딩 문제 <ul><li>가상 함수로 만들면 , 자식 클래스의 똑같은 (same signature) 의 함수도 자동으로 가상 함수가 된다 . </li></ul>class CMonster : public CActor { public: void Attack(CActor*); virtual void BeAttacked(CActor*); }; class CBoss : public CMonster { public: void Attack(CActor*); void BeAttacked(CActor*); }; void CPlayer::Attack(CBoss* pTarget) { if (CanHit(pTarget)) { pTarget->BeAttacked(this); } } CBoss* pBoss = new CBoss; pBoss->BeAttack(pPlayer);
    32. 32. 일반 함수 (Non-Virtual) 오버라이딩 문제 <ul><li>해결방법 </li></ul><ul><ul><li>인터페이스를 추출하기 전에 , 해당 클래스를 상속받는 클래스가 있는지 본다 . </li></ul></ul><ul><ul><li>가상 함수로 만들려는 함수를 자식 클래스에서 일반 함수 (non-virtual) 로 선언해 놓은 건 없는지 확인한다 . </li></ul></ul><ul><ul><li>그런 게 있다면 , 아예 새로운 가상 함수를 하나 만들어서 , 그걸 호출하게 만든다 . </li></ul></ul>
    33. 33. 구현 추출 (Extract Implementor) <ul><li>인터페이스 추출이랑 완전 같은데 , 끌어 올리는 거 (pull up) 대신 끌어 내린다 (push down) 는 것만 다르다 . </li></ul>
    34. 34. 의존 제거하기 1 단계 함수에 매개변수 추가 (Parameterize Method) 함수 추출 후 오버라이딩 (Extract and Override Call)
    35. 35. 함수에 매개변수 추가 <ul><li>어떤 함수가 내부에서 다른 클래스를 생성하는 숨은 의존관계 (hidden dependency) 가 있다면 , 함수를 새로 만들어서 그 클래스를 인자로 받을 수 있게 하자 . </li></ul><ul><li>다른 곳에서는 그 함수를 호출하게 한다 . </li></ul>
    36. 36. 예 : 몬스터가 공격 당할 때 void CMonster ::Attack(CActor* pTarger) { double angerBonus = 1.0; if (m_VeryAngry) { CAngryData* pAngry = new CAngryData(this); angerBonus = pAngry->GetBonus(pTarget); } void CMonster ::Attack(CActor* pTarger) { CAngryData* pAngryData = NULL; if (m_VeryAngry) { pAngryData = new CAngryData(); } Attack(pTarget, pAngryData); void CMonster ::Attack(CActor* pTarger, CAngryData* pAngryData) { double angerBonus = 1.0; if (pAngryData) { angerBonus = pAngry->GetBonus(pTarget);
    37. 37. 함수에 매개변수 추가 - 단계 <ul><li>새로운 함수를 만들어 , 내부에서 생성하던 클래스를 인자로 만든다 . </li></ul><ul><li>원래 함수로부터 코드를 잘라내 붙이고 , 클래스 생성 부분은 제거한다 . </li></ul><ul><li>원래 함수의 코드는 제거하고 , 거기에 새 함수를 호출하는 코드를 작성하고 , 인자에 내부에서 클래스를 생성하던 코드를 써 넣는다 . </li></ul>
    38. 38. 함수 추출 후 오버라이딩 <ul><li>함수 내에서 다른 것을 호출하는 좋지 않은 의존관계가 있다면 , 이 부분을 함수로 추출한 후 , 테스트용 자식 클래스에서 이 함수를 오버라이딩한다 </li></ul>
    39. 39. 함수 추출 후 오버라이딩 - 단계 <ul><li>다른 함수 호출 부분을 새로운 멤버 함수로 추출한다 .( 형태는 그대로 유지하자 ) </li></ul><ul><li>새로운 멤버 함수를 가상 함수로 만든다 . </li></ul><ul><li>테스트용 자식클래스를 만들고 , 이 함수를 오버라이딩한다 . </li></ul>
    40. 40. 함수 추출 후 오버라이딩 - 예 void CPlayer::Attack(CActor* pTarger) { ... WSASend(m_Handle...); // 공격과 함께 데미지 표시 // 변경 후 void CPlayer::Attack(CActor* pTarger) { ... m_Socket.Send(...); // 공격과 함께 데미지 표시 class CSocket { void Send(...) { WSASend(m_Handle, ...); class CMockSocket : public CSocket { void Send(...) { // do nothing
    41. 41. 더 할 수 있다 class CMockSocket : public CSocket { void Send(int packetType) { m_PacketRecorder.Push(packetType); TEST_FIXTURE(FixtureBattle, PlayerSendBeAttackedPacket) { pMonster->Attack(pPlayer); CHECK(pPlayer->IsSentPacket(PacketType::BeAttacked)); 이런 걸 감지 변수 (sensing) 이라고 한다 .
    42. 42. 의존 제거하기 2 단계 <ul><li>Seams 이용하기 </li></ul><ul><ul><li>1. 가상함수 이용 </li></ul></ul><ul><ul><li>2. define 을 이용한 코드 변경 </li></ul></ul><ul><ul><li>3. 링크 </li></ul></ul>
    43. 43. Seams? <ul><li>뜻 : 솔기 , 이은 자리 , 흉터 </li></ul><ul><li>Seam 은 외부에서 행동을 변경할 수 있는 프로그램의 한 지점이다 </li></ul>
    44. 44. Seam 예제 void CPlayer::Logout() { g_Database.SavePlayerStatus(this); ... } CDatabase::SavePlayerStatus() 를 하기 위해서는 꼭 DB 에 접근해야 하고 , 시간이 5 초 이상 걸린다고 하면 ?
    45. 45. 가상함수 이용 - 예 CPlayer::CPlayer(CDatabase* pDatabase) : m_pDatabase(pDatabase) {} void CPlayer::Logout() { m_pDatabase->SavePlayerStatus(this); // CDatabase* CPlayer::m_pDatabase; } // 아까의 CMockSocket 과 비슷한 맥락임 . class CMockDatabase : public CDatabase { bool SavePlayerStatus(CPlayer* pPlayer) { return true; } class CMockPlayer : public CPlayer... CMockPlayer* pPlayer1 = new CMockPlayer(new CMockDatabase());
    46. 46. 가상함수 이용 <ul><li>장점 </li></ul><ul><ul><li>언어가 제공하는 기능으로 원하는 작업을 할 수 있다 </li></ul></ul><ul><ul><li>가상 함수로 만들면 , 디자인 적으로 다른 곳에도 쓸 수 있다 </li></ul></ul><ul><ul><li>가상 함수를 쓸 수 있게 하기 위해서라도 , 디자인이 향상되고 , 의존 관계가 줄어든다 . </li></ul></ul><ul><li>단점 </li></ul><ul><ul><li>기존 코드를 꽤 수정해야 한다 . </li></ul></ul><ul><ul><li>일반 함수 (Non-Virtual) 오버라이딩 문제 </li></ul></ul><ul><ul><li>객체가 pass by value 으로 전달될 경우 쓸 수 없다 . </li></ul></ul>
    47. 47. define 을 이용한 코드 변경 - 예 void CPlayer::Logout() { CDatabase::Inst().SavePlayerStatus(this); // singleton 사용 ... } class CDatabase { static CDatabase& Inst() { #ifdef __UnitTest__ if (m_bTesting) { static CMockDatabase s; return s; } #endif static CDatabase s; return s; }
    48. 48. define 을 이용한 코드 변경 <ul><li>장점 </li></ul><ul><ul><li>일반 프로그래밍 기법으로 할 수 없는 것을 할 수 있다 </li></ul></ul><ul><ul><li>성능 저하를 막고 , release code 에 UnitTest 코드가 섞이는 것을 막을 수 있다 </li></ul></ul><ul><li>단점 </li></ul><ul><ul><li>원래 코드의 의존성은 전혀 줄어들지 않았음 </li></ul></ul><ul><ul><li>release code 에서 빌드가 깨질 수 있음 </li></ul></ul><ul><ul><li>예상치 못한 버그가 생길 수 있음 </li></ul></ul><ul><ul><li>지원하지 않는 언어도 있음 </li></ul></ul>
    49. 49. 링크 - 예 class CDatabase { void SavePlayerStatus(CActor* pActor) { sql_query(“update ...”); #ifndef __UnitTest__ #pragma comment( lib, &quot;some_sql_library&quot;) #else #pragma comment( lib, &quot;fake_sql_library&quot;) #endif
    50. 50. 링크 <ul><li>장점 </li></ul><ul><ul><li>기존 코드를 하나도 안 고쳐도 된다 </li></ul></ul><ul><ul><li>외부 소프트웨어와의 의존을 제거할 수 있다 </li></ul></ul><ul><ul><ul><li>바이너리만 받은 third party 라이브러리아면 , 링크 seam 밖에 쓸 수 없다 . </li></ul></ul></ul><ul><ul><li>빌드가 빨라질 수도 있다 </li></ul></ul><ul><li>단점 </li></ul><ul><ul><li>fake 하려는 라이브러리의 인터페이스가 바뀔 때마다 fake 라이브러리의 인터페이스도 같이 바꿔줘야 한다 . </li></ul></ul>
    51. 51. 의존 제거하기 3 단계 정적 함수 노출하기 (Expose Static Method)
    52. 52. 예 : CPlayer 의 공격 코드 class CPlayer : public CActor { bool CanHit(CActor*); void Attack(CActor* pTarget); ... void CPlayer::Attack(CActor* pTarget) { if (CanHit(pTarget)) { pTarget->BeAttacked(this); } }
    53. 53. 어려운 점 <ul><li>CPlayer 는 TCP 소켓 연결을 하지 않으면 객체를 생성하기 어렵게 되어 있다 . </li></ul><ul><li>new CPlayer 를 하지 않고도 , 명중 여부를 확인할 방법이 없을까 ? </li></ul>
    54. 54. CPlayer 의 공격 코드 - 수정판 class CPlayer : public CActor { static bool CanHit (CActor* pAttacker, CActor* pTarget); void Attack(CActor* pTarget); ... void CPlayer::Attack(CActor* pTarget) { if (CanHit(this, pTarget)) { pTarget->BeAttacked(this); } TEST_FIXTURE(FixtureBattle, PlayerCanHitMonster) { CHECK(CPlayer::CanHit(pPlayer, pMonster)); pPlayer->m_Level = 40; pMonster->m_Level = 80; CHECK(!CPlayer::CanHit(pPlayer, pMonster)); class CPlayer : public CActor { bool CanHit(CActor*); void Attack(CActor* pTarget); ... void CPlayer::Attack(CActor* pTarget) { if (CanHit(pTarget)) { pTarget->BeAttacked(this); } }
    55. 55. 코드가 이상하지 않나 ? <ul><li>CanHit 을 굳이 static 으로 만들고 , this 를 넘기는 게 디자인을 망치는 건 아닐지 ? </li></ul><ul><ul><li>멤버 함수도 내부적으로 static 함수에 this 를 넘겨주고 있다 . </li></ul></ul><ul><ul><li>테스트 외에도 타격 판정을 할 때 쓸모 있게 쓸 수 있다 . </li></ul></ul><ul><ul><li>의외로 멤버 변수를 하나도 쓰지 않으면서 , static 이 아닌 멤버함수들이 많다 . </li></ul></ul><ul><ul><ul><li>이런 함수는 static 이든 , 아니든 , 부르는 곳에서는 똑같이 쓸 수 있다 . </li></ul></ul></ul>
    56. 56. 의존 제거하기 4 단계 템플릿 재정의 (Template Redefinition)
    57. 57. 예 : test 코드 실행 중 dead lock 이 걸려요 . class CPlayer : public CActor { CMutexLock m_Lock; // recursive lock 아님 void BeAttacked(CActor* pAttacker) { m_Lock.Lock(); pAttacker.Lock(); pAttacker->BeAttacked();
    58. 58. 해결 방법 class CMutexLockImpl : public ILock { void Lock() { WaitForSingleObject(...); class CDummyLockImpl : public ILock { void Lock() { // do nothing class CLockImp<typename T> { T m_Lock; void Lock() { m_Lock.Enter(); } }; typedef CLockImp<CMutexLockImpl> CMutexLock; typedef CLockImp<CDummyLockImpl> CDummyLock;
    59. 59. 더 할 수 있다 ! class CDummyLockImpl : public ILock { void Lock() { if (CDummyLockImpl::Inst().IsDeadLock()) { throw CException(); 테스트를 하면서 , deadlock 를 감지할 수 있다
    60. 60. 그 외 임의성 테스트
    61. 61. 임의성 테스트 <ul><li>타격 크리티컬 같이 random 값이 들어가는 계산은 어떻게 테스트 할 수 있을까 ? </li></ul>int GetRand() const { #if defined(_DEBUG) && defined(UnitTestDefined) if (bSettedRandomValue) { return MyTestUnit ::Inst().m_Random; } #endif return ::rand(); } TEST_FIXTURE(FixtureUser2, CheckMagicCritical){ int playerLevel = 60; const double bonus = 50.0; MyTestUnit ::Inst().m_Random = 100.0; // 무조건 성공 . CHECK_EQUAL(true, IsAttackCritical(player, playerLevel, bonus)); MyTestUnit ::Inst().m_Random = 0.0; // 무조건 실패 . CHECK_EQUAL(false, IsAttackCritical(...));
    62. 62. 다른 것들 <ul><li>Sprout Method / Class </li></ul><ul><li>Interception Points </li></ul><ul><li>Pinch Point Traps </li></ul><ul><li>Targeted Testing </li></ul><ul><li>Sensing Variable </li></ul><ul><li>Construction Test </li></ul><ul><li>Hack Points </li></ul>
    63. 63. 테스트의 종류
    64. 64. 테스트의 종류 스토리 테스트 비즈니스 의도 ( 제품 설계 ) 사용성 테스팅 탐색적 테스팅 단위 테스트 개발자 의도 ( 코드 설계 ) 특성 테스팅 보안 테스팅 부하 테스팅 조합 테스팅 … 자동 자동 수동 도구
    65. 65. 특성 테스트 <ul><li>일종의 Regression Test </li></ul><ul><li>변경하려는 클래스를 위해 가장 먼저 만드는 테스트 </li></ul><ul><li>이 테스트는 현재 상태의 특성을 나타낸다 . </li></ul><ul><li>작업하는 동안 원래 코드를 바이스 (vise) 처럼 잡아준다 . </li></ul>
    66. 66. 특성 테스트 <ul><li>어떤 종류의 테스트가 필요할까 ? </li></ul><ul><ul><li>필요한 만큼 많이 만들수록 좋다 . </li></ul></ul><ul><ul><li>클래스에 대해 자신감이 생기고 </li></ul></ul><ul><ul><li>클래스가 어떤 걸 하는지 이해하게 되고 </li></ul></ul><ul><ul><li>테스트를 추가하기 쉬워진다 . </li></ul></ul><ul><li>하지만 , </li></ul><ul><ul><li>깨질만한 부분이 있는지를 찾는 게 중요하다 . </li></ul></ul>
    67. 67. 예 : 공격 코드에 특성 테스트를 붙인다면 ? void CPlayer::BeAttacked(CActor* pAttacker) { int damage = GetDamageFromAttacker(pAttacker); TEST_FIXTURE(FixtureAttack, GetDamageFromAttacker) { pPlayer->m_Level = 40; CHECK_EQUAL(760, pPlayer->GetDamageFromAttacker(pMonster); pPlayer->m_Level = 50; CHECK_EQUAL(400, pPlayer->GetDamageFromAttacker(pMonster); pPlayer->m_Level = 60; CHECK_EQUAL(223, pPlayer->GetDamageFromAttacker(pMonster); pPlayer->m_Level = 70; CHECK_EQUAL(0, pPlayer->GetDamageFromAttacker(pMonster); }
    68. 68. 더 할 수 있다 . TEST_FIXTURE(FixtureAttack, GetDamage) { struct TestData { int m_Level; int m_Expect; }; TestData data[] = { {40, 760}, {50, 400}, {60, 223}, {70, 0}, {80, 0} }; for (int i = 0; i < sizeof(TestData[]) / sizeof(TestData); ++i) { TestData& d = data[i]; pPlayer->m_Level = d.m_Level; CHECK_EQUAL(d.m_Expect, pPlayer->GetDamager(pMonster));
    69. 69. 테스트 코드의 리팩토링 <ul><li>테스트 코드가 넘치면 관리가 안 됨 </li></ul><ul><li>팀원들이 #define 을 임의로 고치거나 해서 테스트를 하지 않으려 할 수 있음 . </li></ul>
    70. 70. 처음 테스트 붙이는 요령
    71. 71. 처음 테스트 붙이는 요령 <ul><li>정적 함수 노출하기 (Expose Static Method) 을 이용해서 쉬운 테스트부터 붙여본다 . </li></ul><ul><li>테스트 코드 안에서 여러 객체를 생성해 보고 , 어떤 의존관계가 있는지 알아본다 . </li></ul><ul><ul><li>의존관계가 가장 약한 클래스부터 , mock 객체 같은 seam 을 이용해서 테스트 코드에서 객체를 만들 수 있게 한다 . </li></ul></ul><ul><ul><li>점점 의존관계가 복잡한 클래스도 생성할 수 있게 만들어 본다 . </li></ul></ul>
    72. 72. 처음 테스트 붙이는 요령 <ul><li>변경 요청이 들어오는 기능부터 테스트를 살살 붙여본다 . </li></ul><ul><ul><li>변경 요청이 들어온 기능은 작업이 끝나면 QA 팀이 한 번 더 검사해 주기 때문에 , 좀 더 안전하다 . </li></ul></ul><ul><li>뭘 테스트 해야 할 지 모르겠다면 , 특성 테스트부터 붙여본다 . </li></ul><ul><ul><li>아무 함수나 호출해보고 , 이 때 변경되는 멤버변수값이나 , 리턴값을 테스트 결과로 쓴다 . </li></ul></ul>
    73. 73. 처음 테스트 붙이는 요령 <ul><li>낡은 코드에 UnitTest 를 붙이다 보면 문제가 생길 수 있으므로 가능하면 그나마 덜 심각한 부분부터 Test 를 붙여나간다 . </li></ul><ul><ul><li>스킬 </li></ul></ul><ul><ul><li>아이템 , 경험치 </li></ul></ul>
    74. 74. 처음 테스트 붙이는 요령 <ul><li>private method 를 public method 로 바꾼다 . </li></ul><ul><li>너무너무 그러는 게 싫다면 다음과 같이 해 보자 . </li></ul>
    75. 75. class CTest{ protected: int m_Test; void Test() {} }; class CMockTest : public CTest { public: using CTest::m_Test; // 부모 클래스의 멤버를 public 으로 쓰겠다 . using CTest::Test; }; TEST(AccessTest) { CTest a; //a.m_Test = 1; // protected 멤버 변수 접근할 수 없음 . //a.Test(); // protected 멤버 함수 접근할 수 없음 . CMockTest* pMockTest = (CMockTest*)(&a); pMockTest->m_Test = 1; // CMockTest 로 강제 캐스팅 -> 접근 가능 pMockTest->Test(); // a 가 CMockTest 객체가 아니어도 이렇게 쓸 수 있다는 점에 주의 return 0; }
    76. 76. 아니 ? 은닉화 (encapsulation) 은 ? <ul><li>private 과 protected 를 해야 안전한 거 아닌지 ? </li></ul><ul><ul><li>테스트 없는 private 보다 , 테스트가 있는 public 이 훨씬 안전한 코드다 . </li></ul></ul>
    77. 77. UnitTest 가 아닐 조건 <ul><li>Database 를 access 한다 . </li></ul><ul><li>네트워크 전송을 한다 . </li></ul><ul><li>파일을 읽고 쓴다 . </li></ul><ul><li>UnitTest 를 돌리기 위해 설정 파일을 매번 수정해 줘야 한다 . </li></ul><ul><ul><li>그럼 아무도 안 할 거다 . </li></ul></ul><ul><li>UnitTest 하나당 1 초만 걸려도 , 1000 개면 16.5 분이 걸린다 !!! </li></ul><ul><ul><li>현재는 600 개에 12 초 </li></ul></ul>
    78. 78. 프로그래머를 위한 UnitTest <ul><li>개발 속도 증가 </li></ul><ul><li>화성 탐사선 spirit </li></ul><ul><ul><li>응답 속도 14 분 ! </li></ul></ul>
    79. 79. 프로그래머를 위한 UnitTest <ul><li>빠른 feedback! </li></ul><ul><li>실수를 금방 알기 ! </li></ul><ul><ul><li>1 분 전에 만든 버그를 고치는 것과 </li></ul></ul><ul><ul><li>2 주 전에 만든 버그를 고치는 것은 전혀 다르다 </li></ul></ul>
    80. 80. Test Driven Debugging? <ul><li>일반적인 디버깅 방법은 ? </li></ul><ul><li>버그 리포트 시스템에 새로운 버그 추가 </li></ul><ul><li>게임 스크립트 데이타 받아서 컴파일 </li></ul><ul><li>서버들 빌드 후 loading </li></ul><ul><ul><li>여기까지 5~10 분은 걸림 . </li></ul></ul><ul><li>클라이언트 1 개 ~3 개 실행 </li></ul><ul><ul><li>역시나 3 분 이상 소모됨 </li></ul></ul><ul><li>재현 </li></ul><ul><ul><li>재현하기 힘든 경우라면 ? </li></ul></ul><ul><ul><li>혈맹 전쟁을 테스트하려면 ? 혈맹원 15 명 이상이 접속해야 테스트 가능 </li></ul></ul><ul><li>코드 수정 </li></ul><ul><li>3 번으로 돌아가서 확인 </li></ul>
    81. 81. Test Driven Debugging!! <ul><li>TDD 를 이용할 때 </li></ul><ul><ul><li>디버그 관리자에 새로운 버그 추가 </li></ul></ul><ul><ul><li>게임 스크립트 데이타 받아서 컴파일 </li></ul></ul><ul><ul><li>서버들 빌드 후 loading </li></ul></ul><ul><ul><ul><li>여기까지 5~10 분은 걸림 . </li></ul></ul></ul><ul><ul><ul><li>스크립트 없이 테스트 할 수 있는 경우가 많음 . </li></ul></ul></ul><ul><ul><li>클라이언트 1 개 ~3 개 실행 </li></ul></ul><ul><ul><ul><li>역시나 3 분 이상 소모됨 </li></ul></ul></ul><ul><ul><ul><li>클라이언트 없이 실행 가능 . </li></ul></ul></ul><ul><ul><li>재현 </li></ul></ul><ul><ul><ul><li>재현하기 힘든 경우라면 ? </li></ul></ul></ul><ul><ul><ul><li>혈맹 전쟁을 테스트하려면 ? 혈맹원 15 명 이상이 접속해야 테스트 가능 </li></ul></ul></ul><ul><ul><ul><li>직접 확률을 지정하거나 , 코드에서 loop 돌릴 수 있다 . </li></ul></ul></ul><ul><ul><li>코드 수정 </li></ul></ul>
    82. 82. Branch & Merge <ul><li>Branch 후 Merge 작업 </li></ul><ul><li>Merge 하면서 다른 팀원이 바꾸어 놓은 코드 때문에 버그 발생 </li></ul><ul><ul><li>1 차적으로는 지속적인 통합을 권장 </li></ul></ul><ul><ul><li>2 차적으로는 UnitTest 를 통해서 다른 팀원들에게 지켜야 할 가이드라인을 제시 </li></ul></ul>
    83. 83. UnitTest 는 극히 일부분입니다 <ul><li>이는 남을 탓할 것이 아니라 세상 사물에 관심을 가진 이들이 저 좋은 것 보기에 급급한 나머지 사람 보기를 게을리 했기 때문이다 ... 언제나 모든 것의 마지막 조각은 사람이다 . </li></ul><ul><li>조선조 강철의적 영웅담 : 공길동전 </li></ul>
    84. 84. 다른 팀과의 협조 <ul><li>QA 팀과의 협조 </li></ul><ul><ul><li>원하는 기능 리스트 얻어서 시간 날 때마다 작업해 주기 </li></ul></ul><ul><li>기획팀과의 협조 </li></ul><ul><ul><li>기획 개발용 환경 ( 서버 셋팅 등 ) 을 만들어 주기 </li></ul></ul><ul><li>그래픽팀과의 협조 </li></ul><ul><ul><li>리소스 변경을 바로 확인할 수 있고 , 변경을 쉽게 할 수 있는 Tool 제공 </li></ul></ul><ul><li>게임의 ( 그리고 소프트웨어의 ) 품질은 사람에게 달려 있습니다 . </li></ul>
    85. 85. UnitTest 결과를 email 로 축적 <ul><li>다른 팀원이 내가 설치한 테스트를 실패해서 고생하고 있을 때 바로 이메일로 확인할 수 있음 </li></ul><ul><li>테스트가 늘어남에 따라 , 테스트 전체 시간이 얼마나 늘어나는지 등을 추적할 수 있음 . </li></ul><ul><li>테스트 갯수 증가와 테스트 발생 빈도의 상관관계를 분석 중 .  </li></ul>
    86. 86. References <ul><li>Michael Feathers </li></ul><ul><ul><li>[email_address] </li></ul></ul><ul><ul><li>http://www.xpnl.org/html/Wiki/WELCXP2005.ppt </li></ul></ul><ul><ul><li>http://www.xpnl.org/html/Wiki/WELCXP20052.ppt </li></ul></ul>
    87. 87. References <ul><li>TDD, UnitTest for games in KGC 2007 </li></ul><ul><li>http://unittest-cpp.sourceforge.net/ </li></ul><ul><ul><li>UnitTest++ 소스 받는 곳 </li></ul></ul><ul><li>http://www.gamesfromwithin.com </li></ul><ul><ul><li>Noel Llopis - [email_address] </li></ul></ul><ul><ul><li>GDC2006 발표자료 </li></ul></ul><ul><li>http://andstudy.com/andwiki/wiki.php/BackwardsIsForward </li></ul><ul><ul><li>위 자료를 번역해 놓은 PPT 및 노트 </li></ul></ul>
    88. 88. Books
    89. 89. 감사합니다 .

    ×