내가 개발 설계에서 항상 고민하는 점은 어떻게 해야 나중에 가서 이 작업이 고민거리로 발전하는 것을 막을 수 있을까 하는 점이다.
쥐뿔도 모르던 옛날부터 눈만 높아진 지금에 이르기까지 나를 개발자로 있게 하는 원동력은 욕망과 귀차니즘이었다.
컴퓨터로 하고 싶은 것들은 여럿 있는데 어찌나 사용하기 불편하고 귀찮은 게 많던지, 꽤나 세월이 흐른 지금도 나는 버튼 하나만 누르면 내가 원하는 기능이 딱 되는 그런 환경을 꿈꾼다.
모르긴 몰라도 수많은 설계 서적들에서 도달하고 싶은 경지는 결국 나와 같지 않을까 하고 상상한다.
하나의 마법과도 같은 단어, 혹은 함수 호출, 키워드, 정의를 통해 프로그램 하나가 턱 하고 튀어나오는 그런 세계.
수많은 개발자들이 일자리를 잃게 될지도 모르지만 어릴 때부터 항상 나는 귀차니즘에 휩싸여 살아왔던 듯 싶다.
다음과 같은 (Pseudo C++) 예제를 보자.
// 1단계 : 1에서 100까지 출력
printf("1\n");
printf("2\n");
...
printf("100\n");
근성을 발휘하여 100라인을 만들어보았다(상상 속에서). 그런데 조금만 공부해도 반복문이란 것을 배운다. 컴퓨터가 좀 더 우리의 삶을 윤택하게 해줄 수 있는, 가장 잘하는 일 아닌가?
// 2단계 : 반복문을 사용하여 1에서 100까지 출력
for(int i = 1; i <= 100; ++i)
printf("%d\n", i);
좀 더 재사용성을 높여서 함수로 만들 수 있을 것 같다.
// 3단계 : 1에서 100까지 출력하는 로직을 함수화
void func()
{
for(int i = 1; i <= 100; ++i)
printf("%d\n", i);
}
자, 이제 우리는 func() 함수만 호출하면 무조건 1에서 100까지 출력할 수 있는 기능을 갖게 되었다.
여기까지는 우리가 보통 어렵지 않게(그리고 순식간에) 도달하는 영역이다.
함수를 배우고 나서 조금 지나면 Parameter/Argument라는 것을 배운다. 갑자기 함수의 활용폭이 넓어진 것 같은 기분을 느끼며 시작값 1과 종료값 100을 외부에서 줘본다.
// 4단계 : Parameter를 통해 함수의 활용폭을 넓힘
void func(int begin, int end)
{
for(int i = begin; i <= end; ++i)
printf("%d\n", i);
}
어이쿠, 갑자기 1과 100이라는 숫자가 보이지 않으니 이 함수가 어떤 값을 처리할 수 있는지 명확하게 보이지 않아 조금 불안해졌다(의미 응집성 하락). 하지만 이 작업 하나로 지금 1에서 50까지 출력해야할 곳에서도, 나중에 927에서 23598까지 출력해야할 곳이 생겨도 모두 처리할 수 있게 되어 행복도가 증가했다(활용도 증가). 이 함수는 이제 뭐든지 다 할 것 같다. 그래서 약 5000군데에서 호출하여 사용했다.
이 단계까지는 연습용 코드 몇 개만 해봐도 그렇게 어렵지 않게 겪어볼 수 있다(5000곳에서 호출하는 정도는 아니겠지만). 그런데 아무래도 여러 곳에서 사용하다보니 점점 함수의 견고함이 증명되어 신뢰도가 올라간다. 따라서 이 함수에 기능을 잘 추가하면, 적어도 기존에 돌아가던 기능은 잘 돌아가고 추가 동작도 매끄럽게 될 것 같아서 다음과 같은 요구사항도 끼워넣게 되었다.
// 5단계 : begin ~ end 사이에서 2단위로 출력
void func(int begin, int end, bool jumpTwoStep)
{
if(jumpTwoStep)
{
for(int i = begin; i <= end; i += 2)
printf("%d\n", i);
}
else
{
for(int i = begin; i <= end; ++i)
printf("%d\n", i);
}
}
아뿔싸, Parameter 하나 추가했다고 5000군데에서 호출하던 곳에서 빌드 에러가 나서 동료/친구/나 스스로에게 욕을 왕창 얻어먹고 나서 5000곳을 변경할 자신은 없으니 황급히 Default-Argument 를 끼워넣는다.
// 5.5단계 : 기존 호출되던 함수와의 호환성 보장
void func(int begin, int end, bool jumpTwoStep = false)
{
if(jumpTwoStep)
{
for(int i = begin; i <= end; i += 2)
printf("%d\n", i);
}
else
{
for(int i = begin; i <= end; ++i)
printf("%d\n", i);
}
}
그런데 내 작업을 우연히 지나가다 본 선배/파트장/팀장/라이벌/리드개발자가 이 함수는 확장성이 없다며 변경할 것을 권고했다. 하지만 아무리 생각해도 나는 단순한 작업 대신 복잡도를 올리고 앞으로 더 쓰일지 확신도 없는 코드를 힘들여 생산해내는 것에 회의적이다. 하지만 그들의 부드러운 말투가 험악한 욕설과 구타로 변하기 직전 뜻을 꺾고 그들의 조언을 받아들였다.
// 6단계 : 확장성을 위해 명료함을 희생함. 또는 복잡도를 높여 미래를 대비함
void func(int begin, int end, int step = 1)
{
for(int i = begin; i <= end; i += step)
printf("%d\n", i);
}
만들어두고 보니 이전 코드가 시작과 종료값만 처리했지, step은 처리해두지 않았다는 설계적 구멍이 있었음을 역설한다. 그리고 그것 때문에 내가 고생했다며 억울함을 역설하면서, 한편으로는 이 견고하고 확장성 있던 함수의 약점을 내가 찾아 보완했다는 것에 기쁨과 자부심을 느끼며 업무를 종료하고 그날의 턴을 마친다(음?).
설계 시에 항상 화두에 오르는 것 중 하나는 명료함 vs 확장성이 아닐까 한다. 이제 와서 다시 1~3단계의 코드를 보고 6단계의 코드와 비교해보자. 애초에 우리는 1에서 100까지 출력하기 위해 작업을 시작했다. 하지만 6단계의 함수로는 대충 무엇을 하는지는 파악했어도 정확하게 어떤 출력값이 나오는지는 저 함수를 호출하는 코드를 포함하여 전체를 다 뒤져보거나, 실행해보기 전에는 알 수 없게 되었다. 데이터(1, 100, 1)와 로직(반복문)이 분리됨으로써 활용도가 높아지고 확장성을 가지게 되었지만, 프로그램의 정확한 동작을 분석하기 위해서는 좀 더 넓은 영역의 코드를 볼 필요가 생겼다는 뜻이다.
만약 나중에 가서 func() 함수의 step 기본값을 1에서 2로 변경해야할 일이 생긴다면, 그 사람에겐 5000군데 이상의 의존성을 지닌 코드를 모두 조사하고 분석해보고 나서야 문제 없이 작업을 완료할 수 있을 것이다. 더 슬픈 사실은 그렇게 시간을 들이고 나서 "이 작업은 진행할 수 없습니다" 라고 이야기할 지도 모른다는 것.