Posts Tagged ‘Michael Feathers’

레거시 C 코드에 첫 테스트코드 넣기

Friday, March 27th, 2009

Crashing Your Way to Great Legacy C Tests

  • 원문: http://www.renaissancesoftware.net/blog/?p=27
  • 글쓴이: James Grenning (번역: 한주영)

C/C++로 되어있는 레거시 코드에 테스트를 추가하기란 꽤나 도전적인 일이다. 테스트를 고려하지 않고 설계된 코드는 당연히 테스트하기 쉽지 않을것이다. 종속성은 관리도 안되어 있고 보이지도 않을 것이다. 여기에 처음으로 테스트를 작성하기란 고통스러울 것이다. 심하게 말이다. 하지만 절망하지 말자! 첫 테스트가 가장 어려운 것이고 두번째 테스트부터는 훨씬 쉬우니까.

여러분이 레거시 코드에 테스트를 추가하려 할 때, 앞으로 무엇을 해야 할 지, 그리고 어떤 결과가 있을지를 미리 알면 여정이 수월할 것이다. 이 글에서 나는 여러분이 처음으로 C/C++코드의 일부를 테스트 하니스에 넣을 때 어떤 식으로 진행될 것인지를 알려줄 것이다.

Michael Feathers는 레거시 코드에 테스트를 추가하는 알고리즘을 다음처럼 제안한다.

  1. 수정할 지점을 파악한다.
  2. 테스트 지점을 찾는다.
  3. 종속성을 끊는다.
  4. 테스트를 작성한다.
  5. 수정하고 리팩터링한다.

Michael의 알고리즘과 함께 적용할 수 있는 또다른 방법이 있다. 우선 시나리오를 살펴보자.

여러분은 레거시 코드의 일부를 테스트하고 싶다. 여러분이 테스트에서 호출하려는 함수는 C의 구조체와 함수들이 서로 뒤엉킨 큰 덩어리의 일부이다. 이러한 Function-call data-structure free-for-all (FCDSFFA) 덩어리를 테스트 하니스에 넣어 컴파일이 되도록 하기란 쉽지 않은 일이다. 이것을 실행되도록 하기란 더욱 큰 일이다. 레거시 코드의 함수가 어떤 데이터를 사용하는지 명확하지 않고, 그래서 무엇을 초기화해야 할지도 명확하지 않다. 여러분은 이때 무엇을 초기화해야 할 지 찾아내기 위해 크래시1 를 이용할 수 있다.

크래시 테스트 방법을 시작하려면 빈 테스트 케이스와 여러분이 테스트하고 싶은 레거시 함수가 있으면 된다. 이 알고리즘을 C로 표현한다면 다음과 같을 것이다.

void addNewLegacyCtest()
{
  makeItCompile();
  makeItLink();
  while (runCrashes())
  {
    findRuntimeDependency();
    fixRuntimeDependency();
  }
  makeTestMoreMeaningful();
}
void 레거시C코드에_테스트추가()
{
  컴파일되도록하기();
  링크되도록하기();
  while (실행하여_크래시확인())
  {
    실행시간종속성찾기();
    실행시간종속성해결하기();
  }
  의미있는테스트만들기();
}

makeItCompile() 컴파일되도록하기()

makeItCompile()은 보통 해당 함수의 헤더파일을 #include하고 대상 함수를 호출하는 것으로 시작한다. 여러분에게 주어지는 것은 길게 나열된 컴파일 오류이다. 첫번째 오류부터 해결하는 것이 가장 낫다. 그 문제가 이어지는 101개의 오류를 일으킨 원인인 경우가 많기 때문이다. 한번에 하나씩 #include해라. 첫번째 오류를 계속 공략해라. 여러분의 목표는 최소한의 #include 만으로 깔끔하게 컴파일되도록 하는 것이다. 용기를 잃지 마라.

종속성이 서로 꼬여서 너무 심각한 경우에 취할 수 있는 빠른 길은 대상 함수를 호출하는 제품 코드에서 #include 부분을 복사해오는 것이다. 깔끔하게 컴파일되기는 하겠지만 이제는 #include 목록이 지저분할 것이다. 일단 테스트가 돌아가기 시작하면 #include 목록을 가지치기하도록 하자.

테스트 대상 함수를 호출하기위해 적절한 구조체와 파라미터를 함수에 전달해야 할 수도 있다. makeItCompile() 중에는 널포인터와 하드코딩된 데이터 값을 사용하는데 주저하지 마라. 구조체는 memset()을 이용하여 원샷에 0으로 초기화해라. 나중에는 좀더 의미있는 값을 전달해야 할테지만 종속성을 널포인터와 단순한 값으로 채워넣는 것만으로 금방 깔끔한 컴파일에 다다를 수 있다. 나중에, 테스트가 돌아가면, 테스트 실행이 크래시를 일으키면서 문제를 일으키는 데이터가 어떤 것인지 알려줄 것이다. (물론 그렇지 않은 경우도 있다.)

아직 TDD에 확신을 가지지 못한 사람들은 이 과정에서 용기를 잃고 makeItCompile()을 exit(FAILURE)로 조기종료하게 된다. 포기하지 마라. exit(FAILURE)는 최후의 순간까지 미루자.

makeItCompile()이 성공적으로 리턴하게되면 곧바로 makeItLink()가 시작된다.

makeItLink() 링크되도록하기()

복잡한 #include를 갖추고 파라미터와 전역 종속성을 널포인터와 무의미한 데이터로 채우고나면 여러분에게 다음 보상이 주어진다. 바로 링크 오류다. makeItLink()는 꽤 복잡할 수 있다. “unresolved external” 오류는 제품 코드 중 일부를 링크하거나 테스트 더블을 추가하여 해결할 수 있다. 자신이 Unit test를 작성하지 않는 핑계를 찾는 사람들은 makeItLink()에서 exit(FAILURE)를 하기도 한다.

runCrashes() 실행하여_크래시확인()

링크오류없이 테스트 실행파일이 만들어지고 나면 십중팔구는 실행하자마자 크래시를 일으킬 것이다. 크래시는 초기화되지 않았거나 혹은 부적절하게 초기화된 데이터에 의한 것이다. 여러분은 지금 제대로 하고 있는 중이다! 계속 나아가면 된다! 크래시는 여러분을 실행시간 종속성으로 인도할 뿐이다.

runCrashes() 가 TRUE 인 동안은 반복문에 머물러 있어라. 반복문에 머무르는 동안 여러분은 필요한 전역 변수나 파라미터에 적절한 값을 넣어 테스트 대상 코드가 문제없이 돌아가도록 하면서 실행시간 종속성을 찾아 고치게 된다. 테스트 대상 함수가 테스트 하니스 안에서 실행되고 있다! 실행시간 종속성을 찾아 고치는 과정을 좀더 깊이 들여다 보자.

findRuntimeDependency() 실행시간종속성찾기()

If you have a debugger, fire it up and visit the crash site. Inspecting for clues will likely yield the root cause of the illegal access. If you don’t have a debugger, it’s time to get one. You can also inspect the input data to find obvious problem initializations. Single stepping through the code under test can also be revealing.

여러분에게 디버거가 있다면, 디버거를 띄워 크래시 지점을 바로 찾아낼 수 있다. 단서를 들여다보면 잘못된 접근에 대한 근본 원인을 찾을 수 있을 것이다. 디버거가 없다면 지금이 디버거를 구비할 때다. 입력 변수의 값을 살펴볼 수도 있고, 한 스텝 씩 실행시켜볼 수도 있을 것이다.

findRuntimeDependency() 는 크래시의 근본원인이 밝혀지면 끝난다. Michael은 컴파일시간 종속성을 찾기 위해 ‘컴파일러에 기대기’를 제안했다. findRuntimeDependency() 는 ‘실행 환경에 기대기’를 이용한다. 프로그램을 실행시키고 OS가 제공하는 잘못된 메모리 접근을 막아주는 기능을 이용하여 실행시간 종속성을 찾아나간다.

이 방법은 임베디드이거나 임베디드가 아니거나 모두 효과적이다. 하지만 임베디드 소프트웨어의 경우에는 잘못된 메모리 접근을 검출해주는 시스템에서만 효과를 볼 수 있다. 잘못된 메모리 접근을 검출해주지 못하는 시스템에서는 여러분의 코드가 문제있는 상황에서도 아무렇지 않게 실행되었다가 나중에 문제를 일으킬 수도 있다.

크래시 테스트를 통해 테스트 케이스를 만들어갈 수 있다는 것은 임베디드 소프트웨어 테스트를 개발 환경에서 실행시켜야 하는 좋은 이유가 된다. 임베디드 소프트웨어 테스트를 개발 환경에서 실행시켜야 하는 다른 이유들이 궁금하다면 TDD for Embedded Software 를 보기 바란다.

fixRuntimeDependency() 실행시간종속성해결하기()

발견한 근본원인을 보면 빼먹은 초기화가 무엇인지 쉽게 알 수 있을 것이다. 원인은 전역 변수나 함수 포인터를 초기화하지 않았다거나 혹은 엉뚱한 값이 지정되어 있을 수 있다. 무엇을 초기화할 지 알아낸 다음 makeItCompile()과 makeItLink() 과정을 다시 진행하게 된다. 실행시간 종속성을 하나 해결하고 나면 대게는 또다른 크래시와 마주치게 된다. 다행인 것은 이러한 실행시간 종속성이라는 구멍을 모두 막고나면 크래시가 더이상 생기지 않을 때가 온다는 것이다. 그러고 나면 새로운 테스트를 추가하는 것은 금방이다.

여러분이 테스트 대상을 너무 크게  잡았다면 깔끔하게 실행되기까지 더오래 걸리고 더 많은 크래시를 보게 될 것이다. 좀더 수월한 대상을 찾아보고 그것을 선택하는 것이 낫다.

makeTestMoreMeaningful() 의미있는테스트만들기()

깊은 숨을 들이쉬자. 힘든 부분이 다 끝났다! 이제 테스트를 좀더 의미있게 만들면 된다.

보통 addNewLegacyCtest() 다음에 해야할 첫번째 일은 cutPasteRenameTweak() 이다.  이것을 더이상 추가할 테스트가 떠오르지 않을때까지 하게 된다. 부디 레거시 C 테스트를 새로 추가할 때 다음과 같이 하지 말기를 바란다.

void addMoreLegacyCtests()
{
  while (!testsAreSuffecientForCurrentNeeds())
  {
    copyPasteTweakTheLastTest();
  }
}
void 레거시C코드에_테스트_더_추가하기()
{
  while (!테스트가_충분한가())
  {
    마지막테스트_복사하여붙이고_살짝고치기();
  }
}

copyPasteTweakTheLastTest()
마지막테스트_복사하여붙이고_살짝고치기()

일단 첫번째 레거시 테스트가 돌아가게 되면 보통은 맘이 편안해지고 기쁘기도 할 것이다. 개발자들은 금새 새로운 테스트를 생각해낸다. 입력값이나 CHECK할 내용을 살짝 바꾸는 식으로 말이다. 그러니 copyPasteTweakTheLastTest() 는 자연스러운 결과이다. 이렇게 하는 것은 아무 문제없다. 다만 copyPasteTweakTheLastTest() 다음에는 반드시 리팩터링을 하기만 한다면. 만약 리팩터링을 빼먹는다면 테스트코드가 금방 엉망진창이 될 것이다.

테스트는 읽기 쉽고, 유지보수하기 쉽고, 코드만 보고 이해할 수 있어야 한다. 중복이 많다면 이런 필요성을 만족시킬 수 없다.

대신에 다음과 같은 알고리즘을 제안한다.

void addMoreLegacyCtests()
{
  while (!testsAreSufficientForCurrentNeeds())
  {
    copyPasteTweakTheLastTest();
    while (!testDifferenncesAreEvident())
    {
      inspectTheCopyPastedTests();
      if (setupStepsAreSimilar())
      {
        extractAndParameterizeTheCommonSetup();
        extractAndParameterizeTheCommonAssertions();
      }
      else
        ;//Maybe a new test case group is needed
    }
  }
}
void 레거시C코드에_테스트_더_추가하기()
{
  while (!테스트가_충분한가())
  {
    마지막테스트_복사하여붙이고_살짝고치기();
    while (!테스트간_차이점이_명확한가())
    {
      복사하여붙여넣은테스트_들여다보기();
      if (셋업이_비슷한가())
      {
        공통셋업_떼어내어_인자_넘겨받게_만들기();
        공통체크_떼어내어_인자_넘겨받게_만들기();
      }
      else
        ;//테스트 그룹을 새로 만들어야 할지도 모른다
    }
  }
}

testsAreSuffecientForCurrentNeeds() 테스트가_충분한가()

이름에서 알수 있듯이 testsAreSufficientForCurrentNeeds() 에서 여러분은 지금까지 만들어진 테스트가 충분한지를 결정하게 된다. (난 설명이 필요없는 코드를 사랑한다.) 이때 다음의 질문을 던져보자.

  • 입력값은 어떻게 달라질 수 있는가?
  • 테스트 대상 코드의 동작을 검증하려면 어떤 것을 CHECK해야 하는가?
  • 어떤 테스트가 더 필요한가?

testDifferenncesAreEvident() 테스트간_차이점이_명확한가()

처음에는 testDifferencesAreEvident()가 항상 FALSE를 리턴할 것이다. copyPasteTweakTheLastTest() 를 하고 나면 여러분이 만족할만한 새로운 테스트가 생기게 되지만, copyPasteTweakTheLastTest()를 몇번 실행하고 나면 테스트케이스마다 중복이 많을 것이다. 추가한 테스트에서 살짝 수정한 내용은 긴 초기화 코드와 여러줄에 걸친 CHECK 사이에 감춰져서 테스트케이스들 간의 차이가 드러나지 않는다. 작성한 코드의 라인 수에 따라 돈을 받는 것이 아니라면 지금이 코드를 청소할 때다. 반복문을 몇번 실행하고 나면 이제 copyPasteTweakTheLastTest()를 실행해도 간결하고 읽기 쉬운 테스트케이스를 얻게 된다.

테스트 코드를 깔끔하게 유지하는 것이 왜 중요할까? 테스트 코드는 며칠(아니 몇시간)만 지나더라도 금방 이해하기 어려워지는 경우가 많기 때문에 반드시 깔끔하게 유지해야만 한다. 여러분이 copyPasteTweakTheLastTest()를 실행하는 중에는 살짝 바꾼 내용이 여러분에게 잘 보일런지 모르지만 다른 사람들한테는 그렇지 않다. 그리고 좀 지나면 여러분이 보더라도 테스트케이스 간의 차이점이 드러나지 않을 것이다. 그래서 차이점이 분명하게 보이는 바로 지금이 중복을 없애고 테스트 코드의 가독성을 높일 수 있는 최고의 순간이다. testDifferencesAreEvident()는 중복을 리팩터링하여 공통 입력 데이터와 헬퍼 함수로 바꾼 다음에야 TRUE를 리턴하게 된다.

extractAndParameterizeTheCommonSetup()
공통셋업_떼어내어_인자_넘겨받게_만들기()

이 과정에서는 테스트 케이스마다 나타나는 공통 셋업 부분을 떼어내어 테스트 케이스들이 공유할 수 있도록 공통 변수와 헬퍼 함수를 만든다. 테스트 코드를 수정하면서 조금씩 수정할 때마다 테스트를 모두 실행키도록 한다. extractAndParameterizeTheCommonSetup()의 결과물로 새로 만들어진 초기화 코드를 실제 제품 코드에 사용할 수 있는 경우도 있다. 쓸만한 유틸리티를 만들었으니 promoteTestSetupCodeToProductionCode() 를 호출하는 것도 고려해보기 바란다.

extractAndParameterizeTheCommonAssertions()
공통체크_떼어내어_인자_넘겨받게_만들기()

이 과정은 extractAndParameterizeTheCommonSetup()과 거의 같다. 두 과정을 다음처럼 리팩터링해야 할지도 모르겠다.

  extractAndParameterizeCommon(setup);
  extractAndParameterizeCommon(assertions);

여러분이 수고를 들인만큼 보상이 따를 것이다. 이 과정을 한두번 거치고 나면 testDifferencesAreEvident() 는 계속 TRUE를 리턴할 것이다. 이렇게 깔끔하게 리팩터링된 테스트코드 위에서 작업을 한다면 여러분에게는 새로운 테스트 시나리오를 손쉽게 추가할 수 있는 도구가 생긴 셈이다. 다음번 포스트를 통해서 예제를 들어보이겠다.

여러분에게 충분한 테스트가 생겼다면, exit(SUCCESS);

이 전체 과정을 명확하게 파악하고 정리할 수 있도록 도와준 서울에 있는 나의 친구들에게 감사를 말을 전한다! 최근에 난 C를 너무 많이 하고 있는게 아닌가 싶다.

  1. 원문의 crash your way에서 crash는 시스템이 뻗는 것과 험한 길을 헤쳐나가는 것을 중의적으로 표현한 것 같다. []

Agile2008 – 목요일

Saturday, August 9th, 2008

앗, 저녁 연회 이후 늦게까지 술을 마셨더니, 정리할 시간을 놓치고 말았네요. 일단 자리 차지용 포스트를 하나 올려두고 한국에 돌아가서 업데이트 하도록 하겠습니다.

세션

- Technical debt : not to ignore it by Henrik Kniberg

- TDD Clinic : C and Legacy code

— (업데이트) —

Agile2008 – 목요일

목요일이 되니까 슬슬 피곤해지네요. 도착하는 당일은 별 문제없이 제 시간에 잠들수 있었는데, 며칠 지나니까 오히려 시차적응 문제가 생긴 것 같습니다. (아니면, 원래 오후 3시쯤에는 졸리는 걸까요? ㅎㅎ)

금요일도 세션이 오전/오후 세션이 조금 있기는 하지만, 사실상 수/목이 컨퍼런스의 절정(?)에 해당하는 것 같습니다. 목요일 저녁의 뱅킷은 마치 컨퍼런스의 피날레 축제 분위기이기도 했구요.

Waving iPhone제가 들은 세션 이야기보다 Robert C. Martin의 이야기를 먼저 할까 합니다. 뱅킷이 진행되고 각 테이블의 분위기가 어느정도 무르익었을 때 밥 아저씨의 키노트 발표가 이어졌습니다. 한마디로 그는 ‘꾼’이었습니다. 저는 키노트 발표가 왜 뱅킷에 있을까 했는데, 밥 아저씨가 발표하는 모습을 보고나니 이해하겠더라구요. 무대에 오르자마자 왜칩니다. ‘아이폰을 가진 사람들은 전부 손들어보세요.’ 조명이 꺼진 연회장 곳곳에 반짝반짝 아이폰이 올라옵니다. ‘자 다같이 음악이나 즐깁시다’ 음악이 흐르면서 다같이 아이폰으로 웨이브를~ 이미 바람잡이가 자기가 할 키노트를 해버렸으니 우리는 음악이나 즐기자면서 ㅎㅎ

밥 아저씨의 키노트에서 키워드는 ‘Clean Code‘였습니다. 자기가 책을, 그것도 훌륭한 책을 썼다면서 꺼내들고서는 능청스럽게 자랑을 하는군요. Clean Code이야기에 앞서 최근 애자일의 양 축이라 할 수 있는 XP와 Scrum을 먼저 이야기합니다. XP를 씹으면서(헉?) Scrum을 찬양하는가 싶더니, 결국은 둘 다 우리가 추구하는 것을 바라보는 양축에 불과하다는 결론을 내리고서는, XP나 Scrum할 것 없이 우리가 정말 고통을 겪는 것은 무엇때문인가라는 질문을 던집니다. 답은 물론 ‘Bad Code’ 혹은 ‘Ugly Code’죠. “소프트웨어 쟁이들인 우리에게 코드를 깔끔하게 정리할 시간이 외부에서 주어지는 날은 영영 오지 않을지도 모릅니다. 어디까지나 그것은 ‘프로’로서 우리가 가져야할 책임의 문제입니다.” 멋진 비유로 끝을 맺는데, 과거 의사라는 전문가들이 수술을 하면서도 손을 씻지 않았던 때가 있었답니다. 어느 의사가 손을 씻는 것만으로 수술후 환자가 감염될 확률이 3%, 1%까지 줄어든다는 것을 발견하고는 의학저널에 이를 발표했다고 합니다. 많은 의사들이 반대했다고 하는군요. ‘무슨 소리냐, 우리들 때문에 환자들이 감염된다니’ 그러면서 이렇게 얘기했다고 합니다. ‘우리 의사들은 너무 바빠서 손을 씻을 시간이 없다구!’ (제 부족한 영어실력으로 들은 것이라 그리 정확하지는 않습니다. ㅠ.ㅠ)

와우~ 현장의 분위기나 밥 아저씨가 좌중을 휘어잡는 모습등을 제대로 전하기란 제게 너무 어려운 일이네요.

뱅킷이 끝나고서는 ‘춤판’이 벌어졌습니다. 이번 컨퍼런스의 ‘스테이지’ 개념이 음악 페스티벌에서 빌려온 개념이라고 했었죠? 단순히 개념만 빌려오지 않고 컨퍼런스 참가자들이 직접 연주하는 음악회가 매일 열렸습니다. 목요일은 그 절정이었죠. 무대에서는 흥겨운 음악이 연주되고 사이키 조명이 켜진 홀에서는 다들 가발이나 반짝이 의상등을 입고 춤을 추면서 즐겼습니다. 저는 쿵쾅거리는 음악이 잘 맞지 않아서 반대편 조용한 홀에서 아시아 친구들과 ‘술판’을 벌였죠. ㅎㅎ

아, 오늘 뱅킷에는 또다른 주인공이 있었습니다. 한사람이 아닌 다수입니다. 바로 일본에서 온 친구들이죠. 15명 정도가 참석하여 6개 정도의 세션을 맡아 발표를 했고, 뱅킷에서는 전원이 무대에 올라 ‘Dear XP’ 라는 곡을 전세계 애자일 동지들에게 소개하기도 했습니다. 첫 날 아이스브레이킹 때와 목요일 뱅킷에서 기모노, 유가타, 한삐(?) 등을 입고 다니면서 많은 주목을 받았습니다. 무엇보다 일본의 애자일 커뮤니티의 핵심 인물인 켄지 상이 Pask Award를 수상하기도 했습니다. 그동안 외부로부터 받아들이기만 했으나, 이제는 거꾸로 자신들의 이야기를 외부에 전해주고 싶었다고 하는군요.

“Dear XP” 라이브입니다.

일본과 비교하였을때, 우리나라에서의 애자일 움직임이 더하면 더했지 덜하지는 않을거라 생각합니다만, 이렇게 외부에 드러내는 일에는 우리가 아직 서툰게 아닌가 하는 생각이 들었습니다. 그들이 주목받는 모습을 보면서 우리도 같이 할 수 있으면 좋지 않을까 부러운 생각도 많이 들었고, 반성도 하게 되더군요. 제가 우리나라를 대표해서 참가한 것은 아니지만 감히 책임감까지 느껴지더라구요. 꼭 올해의 일본처럼 할 필요는 없겠지만, 우리나라에서 진행되는 활동들을 공유하는 것으로도 충분히 의미가 있을 거라 생각합니다.

Japanese friends at Agile2008

새로운 일본 친구들과 기념촬영~

제가 들은 세션들은 제 개인적인 관심 위주로 들은 것들이라 간단하게만 언급하고 목요일 이야기를 줄일까 합니다.

오전 중에 Henrik Kniberg의 “Technical Debt” 세션을 들었습니다. Technical Debt에 어떤 식으로 대처해야 할까 하는 내용이었습니다. 지금 번역 주인 “Scrum and XP from the Trenches“의 저자이기 때문에 얼굴도 보고 인사도 할겸 들어간 세션이었죠 ^^;;

오후에는 Bas Vodde의 “TDD Clinic : C” 세션에 들어갔습니다. Michael Feathers도 있었죠. 우오옷. James Grenning의 세션과 비교하여 크게 차이는 없었습니다. Michael Feathers가 의외로 많은 6~7명의 참석자를 보고서, 올해 분위기가 괜찮은 것 같으니 내년에는 TDD in Assembly를 하면 어떻겠냐고 해서 다들 웃을 수 있었죠. (Michael이 정말로 할까요? ㅎㅎ)

Bas and Michael at Agile2008

세션을 마련해준 Michael과 Bas 두 사람입니다.

목요일 이야기는 우선 여기서 끝~.