소스 코드 복사의 위험성

Posted at 2007. 3. 1. 13:22 // in S/W개발 // by 김윤수


S/W 개발 프로젝트를 진행하다 보면 통곡할 일이 벌어지곤하는데, 이름하여 "코드를 복사해서 재사용하기" 현상입니다. 이런 일들을 자주 보고서는 소스 코드 복사가 얼마나 위험한 행동인지 빨리 얘기해야 되겠다 싶어 급한 마음에 키보드 두드려 봅니다.

소프트웨어 공학에서 오랜 기간 재사용 재사용을 외쳐 왔지만, 현장에서 가장 잘 사용되는 형태의 재사용은 코드를 복사해서 재사용하기겠죠. 특히 프로젝트 일정이 바쁘면 가장 잘 나타나는 개발자들의 나쁜 버릇 중의 하나입니다. 항간에는 "코드를 복사해서 재사용하기" 초식을 후배들에게 마치 고수의 초식인냥 진지하게 전수해 주는 선배들이 있다는 소문이 들려 오니 더 난감할 따름입니다. 이 초식을 쓰는 사람은 머지 않아 주화입마에 빠져 "걸레 코드 디버깅하기", "디버깅한 코드 또 디버깅하기", "Defect 리스트 계속 불어나기" 등등에서 헤어 나오지 못하는 무서운 초식임에도 이렇게 무분별하게 전수된다 하니 이제 쌍수를 들어 말려야 겠습니다.

그렇다면, 제가 왜 그렇게 "코드를 복사해서 재사용하기"를 그렇게 나쁘다고 할까요 ? 제가 재사용을 싫어하기 때문일까요 ? 설마~ 제가 그런 이상한 사람일리가 있나요. 얼마전부터 S/W 업계에서 재사용이 중요하다며 난리 부루스를 추는 상황에서 재사용을 반대할 리가 있겠습니까? 제가 반대하는 것은 "코드를 복사해서" 재사용하기를 반대하는 겁니다.

자~ 여러분이 S/W 개발 도중에 다음과 같은 코드를 짜게 되었다고 합시다.

void functionA(char msg[]) /* 다른 argument 도 있다고 칩니다 */
{
  char buf[128];
  sprintf(buf, "%s::%d::%s ", __FILE__, __LINE__, __FUNCTION__);
  strcat(buf, msg);
  fprintf(stdout, buf);
  ......                   /* 이 함수에서 해야할 일을 합니다. */
}


그리고 나서, 다른 함수를 작성하다 보니 비슷한 일을 할 필요가 있었습니다. 그래서, 다음과 같이 위 코드를 복사해다가 넣습니다.

void functionB(char msg[]) /* 다른 argument 도 있다고 칩니다 */
{
  char buf[128];
  sprintf(buf, "%s::%d::%s ", __FILE__, __LINE__, __FUNCTION__);
  strcat(buf, msg);
  fprintf(stdout, buf);
  ......                   /* 이 함수에서 해야할 일을 합니다. */
}


그리고, 개발이 진전됨에 따라 또 다른 함수 functionC(), functionD(), ... 이렇게 위 코드를 복사해서 쓰는 코드는 갈 수록 늘어갑니다. 설상 가상으로 여러분 후배 또는 동료가 위 코드를 보더니, 쓸만하다 싶어 또 복사해다가 씁니다. 개발하는 동안 이런 저런 테스트를 무사히 통과합니다. 시스템 테스트 단계까지 다 거쳐서 패키징된 후 시장에 드디어 입성했습니다. 아니 그런데 이게 웬 일입니까 ? 테스트할 때는 아무런 문제가 없던 코드가 시장에 나가서 고객들이 쓰기 시작하더니 하루가 멀다하고 리콜이 들어옵니다.

비상 회의 소집되고, 몇 일에 걸쳐 문제를 찾고 또 찾고 갔더니 위 코드에 문제가 있다는 걸 겨우 알아냈습니다. 그래서 functionA()를 다음과 같이 수정했습니다.

#define BUF_SZ 256

void functionA(char msg[])
{
  char buf[BUF_SZ];
  int nr;
  /* buffer overflow를 막기 위해 sprintf, strcat 대신 snprintf 및
     strncat을 사용합니다 */
  nr = snprintf(buf, BUF_SZ-1, "%s::%d::%s ",
    __FILE__, __LINE__, __FUNCTION__);
  strncat(buf, msg, (BUF_SZ-1-nr >= 0 ? BUF_SZ-1-nr : 0));
  buf[BUF_SZ-1] = '\0';  /* buffer 가 다 찬 경우-BUF_SZ-1개 만큼-에는
                          * 마지막 '\0'가 채워져 있지 않으므로 */
  fprintf(stdout, buf);
  ......                 /* 이 함수에서 해야할 일을 합니다. */
}


그리고, 다시 테스트해 봤더니 functionB()에도 복사한 코드가 문제가 있다는 걸 발견합니다. 개발하는 동안 위 코드를 몇 번 복사했다는 걸 생각해 내고는 생각나는 곳은 다 고칩니다. 그런데 이게 또 무슨 운명의 장난이란 말입니까? functionD()에도 해당하는 코드를 복사해서 썼다는 것을 깜빡한 것입니다. 이런 저런 테스트를 하는 동안 문제가 없이 통과해서, 이번에는 문제가 없을 거라며 큰소리 뻥뻥 쳐가면서 이미지를 릴리즈합니다.

아니 웬 걸. 몇 달 안가서 또 비슷한 문제가 터집니다. 입에서 'ㅆ' 욕이 막 나옵니다. 이번에도 죽을 똥 쌀 똥 피똥 다 싸가며 겨우 문제가 되는 지점을 알아 냈더니 결국 functionD()에서 복사한 코드를 고치지 않았단 것을 알아냅니다. 누굴 탓하겠습니까? 복사한 자기를 탓해야지요. 이리하여 이 제품의 코드는 안정화되고 그럭저럭 시간이 지나갑니다.

새해가 돼서, 이전 코드를 기반으로 새로운 파생 제품을 만든다고 합니다. 그런데, 이번에는 OS와 컴파일러가 달라진다네요. 별 문제 없겠거니 하면서 Target OS와 컴파일러에서 컴파일을 하려고 했더니, 이전 OS와 컴파일러에서는 잘도 컴파일 되던 게, 화면에 엄청난 에러 메시지를 내뱉으면서 컴파일이 중단됩니다. 그 엄청난 컴파일 에러 메시지들을 일일이 찾아 봤더니, __FUNCTION__ 이라는 predefined macro를 모르겠다며 난리를 치는 것입니다. 어이구 내 팔자야~ 내 자식들은 절대 기술자 못되게 한다고 몇 번씩 다짐하면서 컴파일 에러 난 곳을 일일이 찾아다니며, __FUNCTION__ macro를 쓰지 않는 것으로 다 수정합니다. 수정하는 동안 제품단가를 줄이기 위해 Target OS와 컴파일러를 수정하자고 한 상품기획팀만 내내 욕합니다.

어떻습니까? 이제 감이 잡히십니까? "코드 복사해서 재사용하기"의 가장 큰 문제는 원래 코드의 문제가 복사되는 곳에 계속해서 퍼진다는 것입니다. 물론 원래 코드에 전혀 문제가 없다면 뭐~ 괜찮겠지요. 그렇지만 위에서 복사됐던 코드도 잘 뜯어보면 몇 줄 되지 않은 코드에 상당히 많은 버그를 품고 있습니다-주로 buffer overflow. 그래서 결국 한 번 수정으로 해결할 수 있는 문제를 n 번 수정이 필요한 문제로 만들어 버린다는 거죠. 단순 수정이라면 그나마 괜찮지만, 수정을 위해 그 이전에 필요한 단계-재현, 원인 분석 등-를 생각하면 그 비용은 어마어마한 차이가 있을 것입니다.

그러니 제발~ 여러분이 만약 어떤 코드를 반복적으로 복사해서 재사용하고 있는 걸 발견하시면, *두말 없이* *바로* 그 코드를 함수 또는 상위 클래스의 메소드-성능이 문제라면 inline 함수 또는 macro 함수-로 만들고, 그 함수를 호출하는 식으로 바꾸셔야 합니다. 그리고, 여러분이 짠 함수가 공통적으로 여러 모듈에서 사용될 수 있겠다는 생각이 들면, 팀원들에게 공지 한 번 해서 이러이러한 함수 또는 클래스 만들었으니 다 같이 잘 써보자. 문제가 있으면 알려달라 이렇게 해야 합니다. 물론 상세 설계 단계에서 제대로 설계했더라면 구현 단계에서 이런 일을 할 필요는 없겠지만, 뭐~ 아직 우리 내공이 상세설계를 그 정도까지 잘하지 못하는 게 현실 아닙니까 ? 구현단계에서라도 잘 해야죠.

"코드 복사해서 재사용하기" 만행은 꼭 한 함수 또는 짧은 코드를 대상으로만 이뤄지는 게 아닙니다. 어느 정도 큰 모듈(또는 컴포넌트) 전체의 소스코드를 "복사해서" 재사용하기도 합니다. 이것도 마찬가지로 짧은 코드를 복사할 경우랑 똑같은 문제가 있습니다. 원래 모듈에 있던 문제가 복사해서 쓰는 곳에 계속해서 퍼진다는 것입니다. 만약 해당 모듈을 참조해서 쓴다면-예를 들어, 복사된 소스코드를 직접 컴파일해서 library를 만들지 않고, 해당 모듈의 소스코드 중 헤더파일만 쓰고, library는 해당 모듈이 build 되는 위치로 링크해서 쓰는 경우-원래 소스코드를 수정한 후, 해당 library만 update 하면 문제가 없을 것입니다. 한 가지 예를 들어 보죠. 이름하여 CMW(Common MiddleWare)라는 모듈이 있다고 칩시다. 이 Middleware를 여러 application에서 쓸 수 있는 핵심적인 공통 기능을 구현해 놓고 있어서 상당히 많은 application이 쓴다고 가정해 봅시다. 그리고, 각 application들-A, B, C-은 서로 별도의 프로젝트로 진행되고 있다고 생각해 봅시다.



서로 다른 application의 소스코드 관리자는 다음과 같이 build tree를 만들 수 있을 겁니다.

A--+- include
     |
     +- lib
     |
     +- src
     |
     +- cmw --+- include
              |
              +- lib
              |
              +- src

B--+- include
     |
     +- lib
     |
     +- src
     |
     +- cmw --+- include
              |
              +- lib
              |
              +- src

C--+- include
     |
     +- lib
     |
     +- src
     |
     +- cmw --+- include
              |
              +- lib
              |
              +- src

위와 같은 상황에서 cmw의 버그를 수정했다면 cmw 소스코드를 각 application 소스코드 관리자에게 릴리즈하고, 각 application 소스코드 관리자는 직접 cmw 의 변경 사항을 적용해야 할 것입니다. 소스코드 관리툴이라도 쓰고 있으면, 이런 과정은 더 복잡해집니다. application A 소스코드 관리자는 새로운 릴리즈를 잘 적용해서 문제를 해결해 놓았는데, B,C 소스코드 관리자는 실수로 해당 변경을 적용해 놓지 않았다면 어떻게 될까요 ? B와 C는 계속해서 문제점을 안고 있을 것입니다. 나중에 시스템 테스트를 하는 동안 문제가 발견된다면 또 B 개발자 C 개발자는 한참 시간을 허비한 후에야 CMW의 문제였다는 것을 알고 허탈해 할 것입니다.

만약 위와 같은 소스코드 트리를 다음과 같이 바꾼다면 어떻게 될까요 ?

CMW --+- include
      |
      +- lib
      |
      +- src

A--+- include
   |
   +- lib
   |
   +- src

B--+- include
   |
   +- lib
   |
   +- src

C--+- include
   |
   +- lib
   |
   +- src

각, A,B,C application 에서는 CMW를 참조만 하고, 소스코드를 복사하지 않는다면 CMW의 update를 A,B,C 에서는 바로 사용할 수 있게 될 것입니다.

물론, 위와 같이 특정 모듈을 공통의 소스코드 repository로부터 사용할 수 있는 경우는 소스코드를 하나의 조직에서 관리할 경우에 쉽게 가능할 것입니다. 만약 소스코드 관리 권한이 여러 조직에 흩어져 있다면-예를 들어, application A,B,C를 서로 다른 조직에서 개발한다면-어쩔 수 없이 소스코드를 복사해서 써야 하는 경우도 있을 것입니다. 그리고 해당 모듈을 사용하는 조직에서 너무 잦은 업그레이드를 원하지 않을 경우도 있을 수 있구요. 그러나, 소스코드 관리 권한이 흩어져 있는 경우라 하더라도 먼저는 소스코드를 복사하기 보다는 참조해서 쓸 수 있는 방법은 없나를 고민해 봐야 할 것입니다. 예를 들어, ClearCase 에서는 Multi-Site 기능을 활용한다던지, CVS에서는 rsync를 이용해서 원격 소스코드 repository 간에 동기를 맞추어주고, 이용하는 쪽에서는 소스코드 변경을 가하지 못하도록 하는 것도 생각해 볼 수 있을 것입니다. 아니면 소스코드를 복사하되, 참조용으로만 사용하고, 변경은 허용하지 않는 경우도 있을 것입니다-물론 이 경우는 시스템 상으로 막히는 것이 아니기 때문에 문제가 발생할 소지가 커집니다.

서로 다른 조직에서 모듈의 소스를 복사해서 재사용하는 경우에 또 다른 문제로는 유지보수 문제가 있습니다. 복사해서 사용하는 측에서 문제점을 발견하여 수정하더라도 원래의 소스코드에는 해당 문제를 수정한 내용이 반영되지 않는 다는 점입니다. 서로 다른 조직이 각각 문제를 해결해가면서 유지보수를 해감으로써 유지보수 비용이 n 배가 될 수 있습니다. 게다가 해당 모듈을 각 조직이 유지보수해 가면서 전혀 다른 소스코드로 발전해 가면서 서로 호환되지 않게 되는 문제도 발생하게 될 것입니다.

따라서, 서로 다른 조직간에도 모듈을 재사용할 경우, 복사해서 사용할 것인지는 소스코드의 관리 측면에서 충분히 검토한 후에 결정해야할 것입니다.

쓰다 보니 말이 길어졌네요. 결국 제가 하고 싶은 말은 한 가지입니다.

"웬만하면 소스코드 복사해서 재사용하지 맙시다"

그럼 여러분께 마지막으로 숙제하나 내고, 글을 마칠까 합니다.

Open Source를 재사용하는 프로젝트에서 소스코드 트리에 Open Source를 Import 시켜야할까요 말아야 할까요 ? Open Source 코드 자체 수정이 필요한 경우와 단순 재사용하는 경우를 구분해서 생각해 보시기 바랍니다

소프트웨어 관련된 저의 다른 글들도 참고로 읽어 보세요.

소프트웨어는 soft 해야 제 맛이다
Flexible한 S/W 작성하기
소스코드 복사의 위험성
C++ 이야기 첫번째: auto_ptr 템플릿 클래스 소개
C++ 이야기 두번째: auto_ptr의 두 얼굴
C++ 이야기 세번째: new와 delete
C++ 이야기 네번째: boost::shared_ptr 소개
C++ 이야기 다섯번째: 내 객체 복사하지마!
C++ 이야기 여섯번째: 기본기 다지기(bool타입에 관하여)

Daum 블로거뉴스
블로거뉴스에서 이 포스트를 추천해주세요.