2002년 3월, 꿈에 그리던 병역특례 회사에 첫 출근을 했다. 따뜻한 봄 날, 직업 프로그래머로써 내 생애 첫 번째 미션이 주어졌다. 사수의 서버 프로그램을 유지 보수하는 일이었다. 나에게 인수인계를 해 준 선임 프로그래머는 그 주에 회사를 그만두었고, 난 계속 죽는 서버 프로그램을 유지 보수 해야 했다. 신입 프로그래머라 실력보다는 패기가 앞섰던 시기였다. 난 지저분해 보이는 프로그램 코드를 처음부터 다시 짜야 한다고 주장했고, 실제로 그렇게 했다. 하지만 안타깝게도 내가 새로 만든 프로그램은 원래 프로그램보다 더 자주 죽었고, 현대적인 UI를 제외하고는 그다지 나아 보이지 않았다. 결과적으로 원래 프로그램을 유지 보수 하는 만큼의 시간을 더 들이고 나서야 새로운 프로그램이 정상 동작하도록 만들 수 있었다.
이 이야기를 시작한 이유는 거의 대부분의 프로그래머가 2002년의 나와 똑같은 자세로 레거시 코드를 대하기 때문이다. 이 글을 쓰기 전에 주변에 있는 10명 이상의 프로그래머에게 레거시 코드에 대한 생각과 레거시 코드를 다루는 본인만의 노하우가 있는지를 물어보았다. 정말 재미있게도 그 모든 사람이 하나같이 똑같은 이야기를 했다. 새로 만들어야지. 그 중에 한 명은 레거시란 말 자체가 이미 쓸모 없다는 뉘앙스를 내포하고 있지 않냐는 말까지 덧붙였다.
11년이 흘렀다. 그사이 난 레거시 코드 때문에 고생도 해보았고, 내가 만든 코드를 레거시 코드라고 부르는 사람들과 만나기도 했다. 그 과정 속에서 생각이 조금 바뀌었다. 레거시 코드를 나쁜 것, 버려야 할 것으로 보던 시각에서 점차적으로 힘들지만 함께 해야 하는 것이라 느끼게 된 것이다. 그리고 그렇게 바뀌게 된 배경에는 레거시 코드가 생겨나는 본질적인 구조에 대해서 알게 되었기 때문이었다. 우리에게 지금 절실하게 필요한 것은 레거시 코드를 리팩토링하는 몇 가지 테크니컬한 기법들이 아니라 그것을 바라보는 프레임을 바꾸는 일인지도 모르겠다.
레거시 코드의 탄생
레거시 코드란 오래되고 낡아서 손대기 힘든 코드를 의미한다. 그렇다면 도대체 오래되고 낡아서 손대기 힘든 코드는 어떻게 만들어지는 것일까? 단순히 오래됐기 때문일까? 아니다. 오래된 코드임에도 유지 보수하기 쉬운 코드들은 얼마든지 있다. 그렇다면 왜 어려운 것일까? 대부분의 프로그래머들은 레거시 코드가 조악하게 작성돼서 그렇다고 생각하는 경향이 있다. 하지만 이 또한 진실을 아니다. 레거시 코드는 우리가 생각했던 것과는 전혀 다른 경로로 만들어진다. 레거시 코드의 본질적인 측면을 이해하기 위해서는 이 탄생 배경을 이해하는 것이 필수적이다.
현대적인 프로그램의 코드 양은 이미 상상을 초월하는 수준으로 방대하다. 이러한 많은 코드 중에서는 프로그램의 업데이트와 함께 지속적으로 변경되는 코드들도 있지만 한 번 만들어 놓은 다음에는 변경이 거의 없는 코드들도 있다. 오래된 저장소에서 작업하고 있다면 변경 이력에 관한 저장소 통계를 내보면 의외로 재미있는 결과를 마주할 수 있을 것이다. 다름아닌 자주 변경되는 부분만 계속 변경된다는 점이다. 변경의 사각 지대에 있는 코드들은 한 번 작성된 이후 몇 년 동안 단 한 줄의 수정도 없는 경우도 많다. 레거시 코드는 바로 이러한 사각 지대에서 발생한다. 오랜 기간 수정이 발생하지 않기에 그걸 아는 프로그래머가 드물어지고 그런 과정이 굳어져서 마치 화석처럼 돼 버리면 레거시 코드가 되는 것이다.
여기서 생각해 보아야 할 점은 왜 몇 년간 단 한 번의 변경도 없었을까라는 점이다. 이는 두 가지를 의미한다. 하나는 그 코드가 본연의 목적은 잘 수행했다는 이야기고, 다른 하나는 그 코드가 관여한 부분에 요구 사항의 변경이 없었다는 것이다. 즉, 이는 다시 말하면 레거시 코드는 많은 프로그래머들의 생각처럼 단순히 잘못 작성되고 오래된 나쁜 코드가 아니라 오히려 안정된 코드라는 점이다.
외부 업체에서 제공한 라이브러리의 경우도 크게 틀리지 않다. 외부 업체에서 제공한 라이브러리가 기능을 정상적으로 수행하지 못했거나 버그가 많았다면 레거시 코드가 되기 전에 이미 많은 사람들의 관심 속에서 다른 라이브러리로 교체가 되거나 직접 작성되는 운명을 거쳤을 것이다. 하지만 제한된 기능을 안정적으로 수행하는 라이브러리의 경우에는 프로그래머들의 사각지대에 놓이게 되고 시간이 오래 흐르면 직접 만든 코드와 마찬가지로 화석처럼 굳어져 버린다. 이 경우에 직접 만든 코드보다 상황이 더 좋지 않은 이유는 라이브러리를 개발한 업체가 더 이상 해당 라이브러리를 유지 보수 하지 않는다거나 아예 사라진 경우도 발생할 수 있기 때문이다. 어쨌든 이 경우에도 직접 만든 코드와 마찬가지로 오랜 시간 본연의 기능을 안정적으로 수행했기 때문에 레거시가 됐다는 점에는 변함이 없다.
새로 만드는 이유
앞서 레거시 코드는 오래되고 낡아서 손대기 힘들다는 기존의 관점이 아닌 오히려 오랜 시간 묵묵히 자신의 역할을 다했기 때문에 살아남을 수 있었던 안정된 코드라는 관점을 제시했다. 하지만 그럼에도 현실세계의 수많은 프로그래머들은 여전히 레거시 코드를 부정하며 새로 만드는 방법 밖에는 없다고 안달한다. 왜 그런 것일까?
첫 번째 이유는 안정된 코드가 다루기 쉬운 코드라는 말은 아니기 때문이다. 프로그래밍 패러다임에도 일정한 흐름이 있고 유행이 있다. 구조화된 프로그래밍 기법이 알려지기 전까지 모든 프로그래머들은 goto 구문을 남발하면서 프로그래밍 하던 것을 당연하게 생각했었고, 객체지향 프로그래밍이 유행하기 전의 프로그래머들은 당연히 프로그램은 절차지향적으로 작성해야 한다고 굳게 믿고 있었다. 레거시 코드는 오랜 시간을 버텼고 그 코드를 처음 작성한 프로그래머와 지금 유지 보수를 맡은 프로그래머 사이에는 시차가 존재할 수 밖에 없다. 이러한 시차는 당연하게 유지 보수를 맡은 프로그래머에게는 거대한 장애물 역할을 하는 동시에, 새로 작성하는 것을 택하도록 만드는 인센티브 역할을 한다.
두 번째 이유는 코드 리딩과 리팩토링에 익숙하지 않기 때문이다. 정말 안타까운 현실인데 대다수의 프로그래머 교육 기관에서 코드 리딩을 제대로 가르치지 않는다. 그 때문에 다른 사람의 코드를 제대로 읽을 줄 아는 프로그래머를 만나기란 정말 ‘하늘에 별 따기’다. 대부분의 프로그래머가 이야기하는 잘못 작성됐기 때문에 다시 만들어야 한다는 주장은 안타깝게도 그걸 해석하는 자신의 이해력이 부족하다는 전제를 내포하고 있다. 일년에 자기 계발서 몇 권 읽는 친구에게 두꺼운 철학책을 읽으라고 권해준다면 철학책 저자의 생각이 오롯이 친구에게 전달되기 보다는 왜곡되기가 쉬운 것과 같은 이치다. 물론 그 철학책을 접한 친구의 평을 듣고 철학책의 가치를 논하기는 더더욱 어려울 것이다.
세 번째 이유는 새로 만드는 것이 기존의 코드를 유지 보수하는 작업보다 훨씬 더 재미있다는 점이다. 프로그래머들은 코드를 창조해내는 창작자들이다. 그들은 유지 보수 작업을 따분하고 지루한 작업이라고 생각한다. 새로운 코드를 써내려 가는 것을 훨씬 더 좋아한다. 유지 보수 할 수 있는 코드라고 하더라도 새로 만들고 싶어하는 또 다른 이유다.
새로 만들기의 함정
레거시 코드를 완전히 새롭게 만드는 일은 그 작업을 수행하는 프로그래머 입장에서는 공부도 되고 개인의 실력 발전도 되겠지만 제품 전체 내지는 조직의 이익에 비추어 본다면 잉여적인 투자라고 할 수 있기 때문에 좋은 선택이라고 보기는 힘들다. 이런 자본 논리 외에도 레거시 코드를 완전히 새롭게 작성하는 것에는 여러 가지 위험 요소가 있다.
“새로 만들기”라는 과업을 수행할 프로그래머가 대부분의 경우에 레거시 코드의 모든 부분을 완전하게 이해하고 작업하는 경우는 드물다는 점이 가장 큰 위험 요소다. 실제로 앞서 인터뷰를 했던 10명의 프로그래머 모두 레거시 시스템을 완전히 새롭게 작성한 후에는 꼭 “원래 있었던 이 기능이 안 되는 것 같은데요”라는 말을 들었다고 했다. 심지어는 기존에 있었던 이스터에그가 정상적으로 동작하지 않는다는 클레임을 받았던 적도 있었다고 했다. 즉, 기존의 코드를 이용하지 않고 완전히 새롭게 만드는 경우에 기존의 코드가 가지고 있었던 모든 기능을 완전하게 충족시키기란 생각보다 쉽지 않다. 뒤에서 살펴보겠지만 심지어 어떤 경우에는 버그도 기존 코드와 동일하게 가지고 있어야 한다.
새롭게 작성하는 쪽을 택하는 프로그래머의 경우 대부분 기존 코드를 보기 보다는 전체적인 기능 맥락을 보고 동일한 기능을 수행하는 새로운 코드를 작성하는 경우가 많다. 이 경우에 명세가 기존 코드의 기능을 100% 모두 충족시킨다면 상관 없겠지만 거의 대부분의 경우에 그렇지 않기 때문에 새롭게 작성한 코드는 항상 기존 코드와 100% 호환되지 않는다는 문제를 일으킨다.
만약에 앞서의 전제를 뒤집어서 만약 그 프로그래머가 100% 기존 코드의 기능을 모두 이해하고 동일한 역할을 수행하는 새로운 코드를 쓴다고 한다면 이 또한 큰 낭비일 수 밖에 없다. 왜냐하면 모든 것을 완벽하게 이해했다면 그 코드를 고치는 일이 새롭게 작성하는 일보다 시간이 덜 걸릴 수 밖에 없기 때문이다.
프로그래머들의 근거 없는 자신감은 “새로 만들기”라는 거대한 과업을 쉽게 보이게 만들어주는 강력한 인센티브 역할을 한다. 대부분의 프로그래머는 자신이 상대적으로 매우 똑똑하며 아주 특별한 존재라고 생각하는 경향이 강하다. 그래서 자신은 기존의 코드보다 훨씬 더 괜찮은 코드를 더 적은 시간을 투입하고 만들 수 있다는 착각에 빠진다. 하지만 이는 정말 말 그대로 착각일 뿐이다. 새롭게 만들어 보면 결국 기존의 프로그래머가 겪었던 시행착오를 모두 다 똑같이 겪으며 조건문들이 여기저기 덕지덕지 추가될 것이기 때문이다.
“작은 것이 아름답다”라는 철학에 매료돼 있는 프로그래머의 경우 “새로 만들기” 증상은 더 심해진다. 그들은 항상 레거시 코드를 보면서 너무 복잡하다고 투덜거린다. 하지만 실질적으로 그들에게 완전히 새롭게 작성할 기회를 주었을 때 기존의 레거시 코드보다 더 효율적이고 간단한 구조의 결과물을 만들어내는 경우는 드물다. 왜냐하면 프로그래밍 문제의 복잡도는 어떠한 코딩 테크닉 보다는 문제 자체를 재정의하는 경우에 달성되는 것이기 때문이다. 따라서 기존 문제를 바라보는 시각을 유지한 체 코드만 새롭게 쓴다고 복잡도를 낮출 수 있다고 생각하는 것은 환상이다. 현실 세계에서는 복잡한 것은 복잡할 수 밖에 없다.
물론 이런 모든 것을 뒤로할 록스타급 프로그래머를 모셔 왔다고 생각해보자. 이 경우에도 항상 새로 만들기를 적용할 수는 없다. 레거시 코드 자체가 워낙 방대한 경우도 있기 때문이다. 똑같은 내용을 동일하게 타이핑 하는 데에도 몇 년이 걸릴 정도로 방대한 레거시 코드 뭉치를 가지고 있다면 이를 새롭게 만드는 일은 프로그래머의 능력을 떠나 개인의 업무로 할 수 있는 작업은 아닐 것이기 때문이다. 그런 일이라면 “새로 만들기”는 또 다른 하나의 거대한 프로젝트가 되어야 할 것이다.
이런 모든 상황을 고려해 볼 때에 결국 “새로 만들기”는 좋은 선택도 아니고 항상 사용할 수 있는 총알도 아니다. 언젠가 반드시 한번은 레거시 코드를 직접 수정해야 하는 일을 할 수 밖에는 없다.
레거시 코드의 본질적인 어려움
결국 피할 수 없는 숙명이라면 레거시 코드를 위해서 코드 리딩과 리팩토링만 열심히 공부를 해 둔다면 되는 것일까? 그렇지는 않다. 레거시 코드를 다루기 위한 기본적인 근간에 코드 리딩과 리팩토링이 있는 것은 사실이지만 레거시 코드는 이런 일반화된 원칙과는 다르게 추가적으로 고려해야 할 부분들이 있다. 이러한 것들이 코드 리딩에 충분히 익숙하다고 하더라도 레거시 코드를 다루는 일을 어렵게 만드는 실체적 허들 역할을 한다. 이제 갓 회사에 입사한 신입 프로그래머 K의 사례를 통해서 이러한 몇 가지 상황을 만나 보도록 하자.
K는 회사에서 개발 중인 프로그램에서 사용하는 한 모듈의 유지 보수를 맡았다. 코드를 검토하던 도중 <리스트 1>에 나와 있는 것과 같은 some_func라는 함수 코드를 보게 되었다. 함수 내부에서 전역 변수인 g_stage 값을 변경하는 것으로 K는 이후 fn1이나 fn2의 작업에 g_stage 값이 영향을 미칠 것이라는 사실을 예측했다. 하지만 실제로 살펴본 fn1과 fn2는 <리스트 2>에 나타난 것과 같이 전혀 g_stage 값과는 상관이 없었다. K는 속으로 왜 이렇게 쓸데없는 코드를 추가했냐고 투덜거리며 g_stage 값을 변경하는 부분을 제거했다. 어떻게 됐을까? 아마 K가 예측할 수 없는 엄청난 일들이 벌어졌을 것이다.
이 사례는 레거시 코드에서 흔히 나타나는 전형적인 예측 불가능한 사이드 이펙트를 보여주고 있다. some_func 호출로 g_stage의 값이 변경된다는 것을 우리는 사이드 이펙트라고 부른다. 그런데 이 코드는 거기서 g_stage 값이 변경되면 무슨 일이 벌어지는지를 보여주지 않는다. 신입 프로그래머들이 흔히 레거시 코드 작업을 하면서 필요 없는 부분이 많아서 제거했다고 하는 내용들이 대다수 이런 것들이다. 물론 교과서에 등장하는 정제된 순수한 세계 속에는 이러한 일이 없겠지만 현실 세계에서 이런 코드들이 비일비재하게 등장한다. 따라서 레거시 코드의 제거는 굉장히 신중하게 해야 한다.
사이드 이펙트를 예측할 수 없는 코드를 보았을 때 K가 취할 수 있는 행동은 두 가지다. 하나는 g_stage 변수와 some_func의 g_stage 관련 조작을 동시에 제거하는 경우다. 이 경우에는 사이드 이펙트의 대상체가 같이 제거되는 것이기 때문에 동시에 제거해도 문제가 없다면 당초 K의 예측대로 이는 쓸모 없이 추가되어 있는 코드라고 할 수 있다. 이 경우에도 예외 상황은 존재하는데 만약 g_stage가 포함된 이 코드가 라이브러리의 일부 코드이며, g_stage가 외부에 노출된 변수라면 동시에 제거가 되더라도 제거하지 않는 편이 옳다. 왜냐하면 그 사이드 이펙트에 의존하는 외부 프로그램이 존재할 수 있기 때문이다.
다른 한 가지 방법은 코드 내에서 g_stage가 하는 역할을 정확하게 조사한 후에 추후에 이 코드를 볼 사람은 K와 같은 의문을 가지지 않도록 g_stage 기능을 새롭게 디자인하는 것이다. 물론 이 경우에는 모든 코드를 면밀히 살펴 보아야 하기 때문에 즉석으로 리팩토링 작업을 하는 것은 좋지 않다.
리스트 1 사이드 이펙트를 예측할 수 없는 코드 1
extern int g_stage;
void some_func(int *a, int b, int c)
{
g_stage = STAGE_BEGIN;
int d = fn1(b);
*a = fn2(d, c);
g_statge = STATE_END;
}
리스트 2 사이드 이펙트를 예측할 수 없는 코드 2
int fn1(int a) { return a+1; }
int fn2(int a, int b) { return a+b; }
K는 또 다른 레거시 코드 뭉치에서 <리스트 3>과 같은 일반적인 문자열 복사 함수의 코드 구현을 만났다. 보안 관련 책을 열심히 공부했던 K는 이런 방식의 함수 디자인이 좋지 않다고 생각해서 함수를 <리스트 4>에 나오는 코드와 같이 문자열 버퍼 크기를 전달 받도록 인터페이스를 수정했다. K는 이제 my_strcpy 함수를 호출하는 부분의 코드만 모두 변경하면 된다고 생각하고는 검색 작업을 했다. 중요한 보안 결함을 수정하는 것 같기도 해서 나름대로 뿌듯하기도 했다. 하지만 그 뿌듯함도 잠시였다. 실제로 이 작업이 그리 간단하지 않다는 것을 알 수 있었기 때문이었다. <리스트 5>와 같은 코드가 문제였다.
이 사례는 고착화된 인터페이스를 변경하는 일이 얼마나 어려운 일인지를 보여준다. 실제로 레거시 코드를 다룬 경험이 많이 없는 프로그래머들은 K처럼 인터페이스 변경을 단순한 코딩 테크닉의 변화로 생각하는 경향이 있다. 하지만 인터페이스라는 것은 단순한 코딩 테크닉 이상을 의미한다. 인터페이스란 그것을 설계한 프로그래머의 사상을 투영하는 성질이 있기 때문에 그걸 변경하는 것은 거대한 작업이 되는 경우가 많다. 이 경우에도 K가 레거시 코드에 대한 경험이 많았다면 버퍼 크기가 들어간 새로운 함수를 만들고 자신이 추가하는 코드에 대해서는 점진적으로 새로운 함수를 호출하도록 변경하는 구조로 가는 것이 좋았을 것이다.
리스트 3 일반적인 문자열 복사 함수
char *my_strcpy(char *dst, const char *src)
{
char *r = dst;
while(*dst++ = *src++)
;
return r;
}
리스트 4 버퍼 크기가 들어간 안전한 문자열 복사 함수
char *my_strcpy(char *dst, size_t size, const char *src)
{
char *r = dst;
for(size_t i=1; i<size; ++i)
{
if((*dst++ = *src++) == '\0')
return r;
}
*dst = '\0';
return r;
}
리스트 5 버퍼 크기를 전달 받지 않는 일반 함수
void some_func(char *name, const char *path)
{
// 중요한 작업들 ...
my_strcpy(name, path);
// 나머지 작업들 ...
}
마지막 사례를 살펴보자 K는 사내 공용 라이브러리를 유지 보수 하는 업무를 맡았다. 해당 코드를 살펴보던 중에 <리스트 6>에 나와 있는 것과 같은 버그가 있는 백분율 계산 함수를 만났다. K는 코드를 보는 순간 100.0을 입력한다는 것이 10.00을 입력했다는 심증을 가졌다. 주석은 그런 K의 생각을 강화시켜주고 있었다. 자신의 존재감을 온몸으로 입증하려던 K는 과감하게 GetPercentage 함수 코드의 10.00을 100.0으로 수정했다.
어떻게 됐을까? 안타깝게도 이후 상황은 K에게 좋지 않았다. K는 사실 버그를 고쳤지만 버그를 새롭게 만들어 버렸기 때문이다. 문제는 <리스트 7>과 같은 코드가 문제였다. 기존의 공용 라이브러리를 사용하던 일부 프로그래머들은 자체적으로 잘못된 계산 결과를 보정해서 사용하고 있었던 것이다. 당연히 이 코드들은 K가 볼 수 없는 곳에서 제작된 것이기 때문에 K가 미라 알고 대처하기에는 힘든 점이 있었다.
리스트 6 타이핑 실수가 있는 백분율 계산 함수
// 백분율 계산 함수
// 결과 = a / b * 100
double CalcPercentage(int a, int b)
{
return a * 10.00 / b;
}
리스트 7 버그가 있는 GetPercentage 함수의 결과를 보정하는 코드
double r = GetPercentage(10, 40) * 10;
이는 전형적인 레거시 코드의 버그 수정이 가져오는 파급 효과를 나타낸 것이다. 작위적인 것처럼 보이지만 우리 나라 회사의 소스 코드에서 흔하게 벌어지는 풍경이다. 정치가 코드에 개입해 버린 것이다. 결과를 보정해서 사용하는 프로그래머 입장에서는 결과를 보정하거나 자신만의 라이브러리를 만드는 비용보다 GetPercentage 함수를 만든 사람에게 버그 수정을 요청하는 비용이 비교도 되지 않게 높았던 것이다.
그렇다면 똑똑한 K라면 어떻게 해야 했을까? 버그가 수정된 GetPercentage2 함수를 만들고 외부에 GetPercentage 버그가 수정된 GetPercenage2 함수가 만들어졌음을 알린다. 그리고 헤더에는 GetPercentage 함수는 버그가 있으니 사용하지 말라는 경고를 출력하는 안전 장치를 추가해 놓는 것이 좋겠다.
결국 레거시 코드 작업이 코드 리딩과 리팩토링 이상의 경험이 필요한 이유는 부분을 통해서 전체를 유추해야 함에 있다. 레거시 코드를 다룰 때에 우리는 항상 전체를 다 볼 수 없다. 또한 레거시 코드들은 그들이 살아남은 역사만큼이나 다양한 장치들을 통해서 다른 컴포넌트들과 인터페이싱하고 있다는 점을 항상 염두에 두어야 한다. 그렇기 때문에 레거시 코드에 대한 수정은 가급적 작고 사소한 부분부터 점진적으로 이루어져야 한다.
레거시 코드를 다루는 전략
레거시 코드를 다루는 기본 전략은 너무나 간단하다. 컴퓨터 공학에서 어려운 문제를 풀 때 항상 등장하는 분할 정복법이다. 거대한 레거시 코드를 한번에 모두 다루기는 힘들기 때문에 그걸 작은 문제들로 분할해서 점진적으로 정복해 나가는 것이다.
분할 정복에서도 레거시 코드를 다룰 때에 가장 중요한 부분은 분할이다. 분할이란 결국 어떤 부분을 수정할지를 선택하는 것이고 그 문제를 거대한 레거시 코드에서 독립적인 부분으로 떼어내야 함을 의미한다. 우리가 수정할 부분이 거대한 시스템에 대해서 완전하게 독립적일 수 있도록 만드는 작업에서 시작해야 한다.
예를 들어 앞선 <리스트 1>에 나오는 some_func 함수의 경우에는 g_stage라는 부분이 의존적이기 때문에 바로 수정하는 것은 좋지 않다. 이 경우에 fn1과 fn2를 호출하는 로직에 변경을 가하고 싶은 경우라면 <리스트 8>에 나와 있는 것과 같이 그 부분을 별도로 떼어내서 독립적으로 만든 다음 작업하는 것이 좋다. 이렇게 떼어내게 되면 some_func2 함수는 기존의 레거시 코드와는 완전히 독립적인 코드가 된다. 별도로 테스트 할 수도 있으며 기존 코드와 결과값을 비교하는 테스트를 작성할 수도 있다.
리스트 8 fn1, fn2 호출 로직 분리
void some_func(int *a, int b, int c)
{
g_stage = STAGE_BEGIN;
some_func2(a, b, c);
g_statge = STATE_END;
}
void some_func2(int *a, int b, int c)
{
int d = fn1(b);
*a = fn2(d, c);
}
앞서도 말했지만 레거시 코드에 대한 변화는 작고 사소한 부분부터 점진적으로 이루어져야 한다는 점을 반드시 기억하자. 이는 레거시 코드에 대한 변화는 없을수록 기존 동작 방식을 잘 유지할 수 있다는 것을 의미한다.
예를 들어 우리가 해야 하는 일이 기존에 C로 만들어진 <리스트 9>와 같은 코드를 클래스로 변환하는 작업이라고 생각해 보자. 이 경우에도 기존 코드들을 해체해서 클래스로 변환시키기 보다는 <리스트 10>에 나타난 것처럼 기존 함수를 호출하는 래퍼 클래스를 만드는 것이 좋다. 기존 코드를 해체해서 새로 만드는 경우에는 수정 작업이 많은 만큼 실수할 여지도 늘어나지만 이렇게 래퍼 클래스를 만드는 경우에는 기존 코드가 하는 역할을 래퍼 클래스도 100% 동일하게 수행할 수 있기 때문이다.
리스트 9 리비전 리스트를 관리하는 레거시 코드
HANDLE CreateRevisionList();
CloseRevisionList(HANDLE h);
BOOL AddFileToRevisionList(HANDLE h, LPCWSTR path, ULONG revision);
BOOL DeleteFileFromRevisionList(HANDLE h, LPCWSTR path);
리스트 10 리비전 리스트 래퍼 클래스
class RevisionList
{
public:
HANDLE revlist_;
class CreateRevisionListError {};
RevisionList()
{
revlist_ = CreateRevisionList();
if(!revlist_)
throw CreateRevisionListError();
}
~RevisionList()
{
CloseRevisionList(revlist_);
}
BOOL AddFile(LPCWSTR path, ULONG revision)
{
return AddFileToRevisionList(revlist_, path, reivison);
}
BOOL DeleteFil(LPCWSTR path)
{
return DeleteFileFromRevisionList(revlist_, path);
}
};
오래된 미래
레거시 코드란 우리가 지금 쓰고 있는 무수히 많은 코드의 오래된 미래다. 우리가 지금 쓰고 있는 코드가 오랜 시간 동안 살아 남는다면 미래의 언젠가는 똑같이 레거시 코드가 될 수 밖에 없을 것이기 때문이다. 레거시 코드가 없는 신생 프로젝트도 마찬가지다. 프로젝트 기간이 길어지고 코드가 늘어나고 하다 보면 미래의 어느 순간에는 반드시 레거시 코드가 생겨날 수 밖에 없다.
‘레거시 코드는 다루기 힘들고 성가신 코드’라는 인식을 이제는 조금 바꿀 필요가 있을 것 같다. 레거시 코드는 항상 우리 주변에 있고, 다른 코드보다 조금 더 우리의 관심을 필요로 하는 코드일 뿐이다. 그러니 이제 쓰레기통에 버릴 궁리만 하기 보다는 레거시 코드를 이해하려는 마음가짐을 가지도록 하자. 어쩌면 레거시 코드를 다루는 가장 위대한 방법은 열린 마음가짐으로 그 코드를 꺼내서 읽고, 또 읽는 것에 있는지도 모른다.
내게 나무를 자를 8시간이 있다면, 나는 도끼를 가는데 6시간을 쓰겠다.
– 아브라함 링컨