설계의 정반합 1

개발 2013. 8. 31. 17:14

내가 개발 설계에서 항상 고민하는 점은 어떻게 해야 나중에 가서 이 작업이 고민거리로 발전하는 것을 막을 수 있을까 하는 점이다.


쥐뿔도 모르던 옛날부터 눈만 높아진 지금에 이르기까지 나를 개발자로 있게 하는 원동력은 욕망과 귀차니즘이었다.


컴퓨터로 하고 싶은 것들은 여럿 있는데 어찌나 사용하기 불편하고 귀찮은 게 많던지, 꽤나 세월이 흐른 지금도 나는 버튼 하나만 누르면 내가 원하는 기능이 딱 되는 그런 환경을 꿈꾼다.


모르긴 몰라도 수많은 설계 서적들에서 도달하고 싶은 경지는 결국 나와 같지 않을까 하고 상상한다.


하나의 마법과도 같은 단어, 혹은 함수 호출, 키워드, 정의를 통해 프로그램 하나가 턱 하고 튀어나오는 그런 세계.


수많은 개발자들이 일자리를 잃게 될지도 모르지만 어릴 때부터 항상 나는 귀차니즘에 휩싸여 살아왔던 듯 싶다.





다음과 같은 (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군데 이상의 의존성을 지닌 코드를 모두 조사하고 분석해보고 나서야 문제 없이 작업을 완료할 수 있을 것이다. 더 슬픈 사실은 그렇게 시간을 들이고 나서 "이 작업은 진행할 수 없습니다" 라고 이야기할 지도 모른다는 것.

Posted by OOJJRS
,